class MessageCallback:
    """Message callback handler.

    Message patterns or templates are used as the key to a message/callback
    tuple. For example, a message with the following attributes:

    {'code': 0x50, 'address': 1A.2B.3C, 'target': None,
    'flags': None, 'cmd1': 0x11, 'cmd2': None}

    Would match any message with:
        code == 0x50
        address == 1A.2B.3C
        target == any value
        flags == any value
        cmd1 == 0x11
        cmd2 == any value

    The above example is an inbound Standard Receive message (0x50)
    with the "Light On" message and any light level value in cmd2.
    """

    def __init__(self):
        """Init the MessageCallback class."""
        self._dict = {}

    def __len__(self):
        """Return the number of callbacks in the list."""
        return len(self._dict)

    def __iter__(self):
        """Iterate through the callback list."""
        for itm in self._dict:
            yield itm

    def __getitem__(self, key):
        """Return an item from the callback list.

        Accepts any message type as a key and returns the callbacks
        associated with that message template.
        """
        return self._dict.get(key, [])

    def __setitem__(self, key, value):
        """Set a callback method to a message key.

        Key: any message template.
        Value: callback method.
        """
        callbacks = self._dict.get(key, [])
        if isinstance(value, list):
            for callback in value:
                callbacks.append(callback)
        else:
            callbacks.append(value)
        self._dict[key] = callbacks

    def add(self, msg, callback, override=False):
        """Add a callback to the callback list.

        msg: Message template.
        callback: Callback method
        override: True - replace all existing callbacks for that template
                  False - append the list of callbacks for that message
                  Default is False
        """
        if override:
            if isinstance(callback, list):
                self._dict[msg] = callback
            else:
                self._dict[msg] = [callback]
        else:
            cb = self[msg]
            cb.append(callback)
            self._dict[msg] = cb

    def remove(self, msg, callback):
        """Remove a callback from the callback list.

        msg: Message template
        callback: Callback method to remove.

        If callback is None, all callbacks for the message template are
        removed.
        """
        if callback is None:
            self._dict.pop(msg, None)
        else:
            cb = self._dict.get(msg, [])
            try:
                cb.remove(callback)
            except ValueError:
                pass
            if cb:
                _LOGGER.debug("%d callbacks for message: %s", len(cb), msg)
                self.add(msg, cb, True)
            else:
                self._dict.pop(msg, None)
                _LOGGER.debug("Removed all callbacks for message: %s", msg)

    def get_callbacks_from_message(self, msg):
        """Return the callbacks associated with a message template."""
        callbacks = []
        for key in self._find_matching_keys(msg):
            for callback in self[key]:
                callbacks.append(callback)
        return callbacks

    def _find_matching_keys(self, msg):
        for key in self._dict:
            if key.matches_pattern(msg) and msg.matches_pattern(key):
                yield key COMMAND_LIGHT_ON_0X11_NONE = {"cmd1": 0x11, "cmd2": None} MESSAGE_ACK = 0x06 MESSAGE_NAK = 0x15 class StandardReceive(Message):
    """Insteon Standard Length Message Received.

    Message type 0x50
    """

    _code = MESSAGE_STANDARD_MESSAGE_RECEIVED_0X50
    _sendSize = MESSAGE_STANDARD_MESSAGE_RECIEVED_SIZE
    _receivedSize = MESSAGE_STANDARD_MESSAGE_RECIEVED_SIZE
    _description = "INSTEON Standard Message Received"

    def __init__(self, address, target, commandtuple, cmd2=None, flags=0x00):
        """Init the StandardReceive message class."""
        if commandtuple.get("cmd1") is not None:
            cmd1 = commandtuple["cmd1"]
            cmd2out = commandtuple["cmd2"]
        else:
            raise ValueError

        if cmd2 is not None:
            cmd2out = cmd2

        if cmd2out is None:
            raise ValueError

        self._address = Address(address)
        self._target = Address(target)
        self._messageFlags = MessageFlags(flags)
        # self._messageFlags.extended = 0
        self._cmd1 = cmd1
        self._cmd2 = cmd2out

    @classmethod
    def from_raw_message(cls, rawmessage):
        """Create message from a raw byte stream."""
        return StandardReceive(
            rawmessage[2:5],
            rawmessage[5:8],
            {"cmd1": rawmessage[9], "cmd2": rawmessage[10]},
            flags=rawmessage[8],
        )

    # pylint: disable=protected-access
    @classmethod
    def template(
        cls, address=None, target=None, commandtuple=None, cmd2=-1, flags=None
    ):
        """Create a message template used for callbacks."""
        msgraw = bytearray([0x02, cls._code])
        msgraw.extend(bytes(cls._receivedSize))
        msg = StandardReceive.from_raw_message(msgraw)

        if commandtuple:
            cmd1 = commandtuple.get("cmd1")
            cmd2out = commandtuple.get("cmd2")
        else:
            cmd1 = None
            cmd2out = None

        if cmd2 is not -1:
            cmd2out = cmd2

        msg._address = Address(address)
        msg._target = Address(target)
        msg._messageFlags = MessageFlags(flags)
        msg._cmd1 = cmd1
        msg._cmd2 = cmd2out
        return msg

    @property
    def address(self):
        """Return the address of the device."""
        return self._address

    @property
    def target(self):
        """Return the address of the target device."""
        return self._target

    @property
    def cmd1(self):
        """Return the cmd1 property of the message."""
        return self._cmd1

    @property
    def cmd2(self):
        """Return the cmd2 property of the message."""
        return self._cmd2

    @property
    def flags(self):
        """Return the message flags."""
        return self._messageFlags

    @property
    def targetLow(self):
        """Return the low byte of the target message property.

        Used in All-Link Cleanup message types.
        """
        low_byte = None
        if self.target.addr is not None and self._messageFlags.isBroadcast:
            low_byte = self.target.bytes[0]
        return low_byte

    @property
    def targetMed(self):
        """Return the middle byte of the target message property.

        Used in All-Link Cleanup message types.
        """
        med_byte = None
        if self.target.addr is not None and self._messageFlags.isBroadcast:
            med_byte = self.target.bytes[1]
        return med_byte

    @property
    def targetHi(self):
        """Return the high byte of the target message property.

        Used in All-Link Cleanup message types.
        """
        hi_byte = None
        if self.target.addr is not None and self._messageFlags.isBroadcast:
            hi_byte = self.target.bytes[2]
        return hi_byte

    def _message_properties(self):
        return [
            {"address": self._address},
            {"target": self._target},
            {"flags": self._messageFlags},
            {"cmd1": self._cmd1},
            {"cmd2": self._cmd2},
        ] class StandardSend(Message):
    """Insteon Standard Length Message Send 0x62."""

    _code = MESSAGE_SEND_STANDARD_MESSAGE_0X62
    _sendSize = MESSAGE_SEND_STANDARD_MESSAGE_SIZE
    _receivedSize = MESSAGE_SEND_STANDARD_MESSAGE_RECEIVED_SIZE
    _description = "INSTEON Standard Message Send"

    def __init__(self, address, commandtuple, cmd2=None, flags=0x00, acknak=None):
        """Init the StandardSend message class."""
        if commandtuple.get("cmd1", None) is not None:
            cmd1 = commandtuple["cmd1"]
            cmd2out = commandtuple["cmd2"]
        else:
            raise ValueError

        if cmd2 is not None:
            cmd2out = cmd2

        if cmd2out is None:
            raise ValueError

        self._address = Address(address)
        self._messageFlags = MessageFlags(flags)
        self._messageFlags.extended = 0
        self._cmd1 = cmd1
        self._cmd2 = cmd2out

        self._acknak = self._setacknak(acknak)

    @classmethod
    def from_raw_message(cls, rawmessage):
        """Create a message from a raw byte stream."""
        if (rawmessage[5] & MESSAGE_FLAG_EXTENDED_0X10) == MESSAGE_FLAG_EXTENDED_0X10:
            if len(rawmessage) >= ExtendedSend.receivedSize:
                msg = ExtendedSend.from_raw_message(rawmessage)
            else:
                msg = None
        else:
            msg = StandardSend(
                rawmessage[2:5],
                {"cmd1": rawmessage[6], "cmd2": rawmessage[7]},
                flags=rawmessage[5],
                acknak=rawmessage[8:9],
            )
        return msg

    # pylint: disable=protected-access
    @classmethod
    def template(
        cls, address=None, commandtuple=None, cmd2=-1, flags=None, acknak=None
    ):
        """Create a message template for use in callbacks."""
        msgraw = bytearray([0x02, cls._code])
        msgraw.extend(bytes(cls._receivedSize))
        msg = StandardSend.from_raw_message(msgraw)

        if commandtuple:
            cmd1 = commandtuple.get("cmd1")
            cmd2out = commandtuple.get("cmd2")
        else:
            cmd1 = None
            cmd2out = None

        if cmd2 is not -1:
            cmd2out = cmd2

        msg._address = Address(address)
        msg._messageFlags = MessageFlags(flags)
        msg._cmd1 = cmd1
        msg._cmd2 = cmd2out
        msg._acknak = acknak
        return msg

    @property
    def address(self):
        """Return the address of the device."""
        return self._address

    @property
    def cmd1(self):
        """Return cmd1 property of the message."""
        return self._cmd1

    @property
    def cmd2(self):
        """Return cmd2 property of the message."""
        return self._cmd2

    @property
    def flags(self):
        """Return the message flags."""
        return self._messageFlags

    @property
    def acknak(self):
        """Return the ACK/NAK message flag."""
        return self._acknak

    @acknak.setter
    def acknak(self, val):
        """Set the ACK/NAK byte."""
        if val in [None, 0x06, 0x15]:
            self._acknak = val
        else:
            raise ValueError

    @property
    def isack(self):
        """Test if the message is a message ACK."""
        return self._acknak is not None and self._acknak == MESSAGE_ACK

    @property
    def isnak(self):
        """Test if the message is a message NAK."""
        return self._acknak is not None and self._acknak == MESSAGE_NAK

    def _message_properties(self):
        return [
            {"address": self._address},
            {"flags": self._messageFlags},
            {"cmd1": self._cmd1},
            {"cmd2": self._cmd2},
            {"acknak": self._acknak},
        ] class ExtendedReceive(Message):
    """Insteon Extended Length Message Received.

    Message type 0x51
    """

    _code = MESSAGE_EXTENDED_MESSAGE_RECEIVED_0X51
    _sendSize = MESSAGE_EXTENDED_MESSAGE_RECEIVED_SIZE
    _receivedSize = MESSAGE_EXTENDED_MESSAGE_RECEIVED_SIZE
    _description = "INSTEON Extended Message Received"

    def __init__(self, address, target, commandtuple, userdata, cmd2=None, flags=0x10):
        """Init the ExtendedRecieve message class."""
        if commandtuple.get("cmd1", None) is not None:
            cmd1 = commandtuple["cmd1"]
            cmd2out = commandtuple["cmd2"]
        else:
            raise ValueError

        if cmd2 is not None:
            cmd2out = cmd2

        if cmd2out is None:
            raise ValueError

        self._address = Address(address)
        self._target = Address(target)
        self._messageFlags = MessageFlags(flags)
        # self._messageFlags.extended = 1
        self._cmd1 = cmd1
        self._cmd2 = cmd2out
        self._userdata = Userdata(userdata)

    @classmethod
    def from_raw_message(cls, rawmessage):
        """Create message from raw byte stream."""
        userdata = Userdata.from_raw_message(rawmessage[11:25])
        return ExtendedReceive(
            rawmessage[2:5],
            rawmessage[5:8],
            {"cmd1": rawmessage[9], "cmd2": rawmessage[10]},
            userdata,
            flags=rawmessage[8],
        )

    # pylint: disable=protected-access
    @classmethod
    def template(
        cls,
        address=None,
        target=None,
        commandtuple=None,
        userdata=None,
        cmd2=-1,
        flags=None,
    ):
        """Create message template for callbacks."""
        msgraw = bytearray([0x02, cls._code])
        msgraw.extend(bytes(cls._receivedSize))
        msg = ExtendedReceive.from_raw_message(msgraw)

        if commandtuple:
            cmd1 = commandtuple.get("cmd1")
            cmd2out = commandtuple.get("cmd2")
        else:
            cmd1 = None
            cmd2out = None

        if cmd2 is not -1:
            cmd2out = cmd2

        msg._address = Address(address)
        msg._target = Address(target)
        msg._messageFlags = MessageFlags(flags)
        msg._cmd1 = cmd1
        msg._cmd2 = cmd2out
        msg._userdata = Userdata.create_pattern(userdata)
        return msg

    @property
    def address(self):
        """Return address of sending device."""
        return self._address

    @property
    def target(self):
        """Return address of target device."""
        return self._target

    @property
    def cmd1(self):
        """Return cmd1 property of message."""
        return self._cmd1

    @property
    def cmd2(self):
        """Return cmd2 property of message."""
        return self._cmd2

    @property
    def userdata(self):
        """Return user data of extended message."""
        return self._userdata

    @property
    def flags(self):
        """Return message flags."""
        return self._messageFlags

    @property
    def targetLow(self):
        """Return the low byte of the target address field.

        Used in All-Link Cleanup messages.
        """
        low_byte = None
        if self.target is not None and self._messageFlags.isBroadcast:
            low_byte = self.target.bytes[0]
        return low_byte

    @property
    def targetMed(self):
        """Return the middle byte of the target address field.

        Used in All-Link Cleanup messages.
        """
        med_byte = None
        if self.target is not None and self._messageFlags.isBroadcast:
            med_byte = self.target.bytes[1]
        return med_byte

    @property
    def targetHi(self):
        """Return the high byte of the target address field.

        Used in All-Link Cleanup messages.
        """
        hi_byte = None
        if self.target is not None and self._messageFlags.isBroadcast:
            hi_byte = self.target.bytes[2]
        return hi_byte

    def _message_properties(self):
        return [
            {"address": self._address},
            {"target": self._target},
            {"flags": self._messageFlags},
            {"cmd1": self._cmd1},
            {"cmd2": self._cmd2},
            {"userdata": self._userdata},
        ] class ExtendedSend(Message):
    """Send an INSTEON Extended message.

    address: A device hex address in any form.
    cmd1: Required command in the cmd1 field
    cmd2: Required command in the cmd2 field
    **kwarg: otional userdata dictionary with the following keys:
        'd1' - User data byte 0 as byte or int
        'd2' - user data byte 1 as byte or int
        ...
        'd14' - user data byte 14 as byte or int
        'd1' to 'd14' are assumed to equal 0x00 unless explicitly set
    """

    _code = MESSAGE_SEND_EXTENDED_MESSAGE_0X62
    _sendSize = MESSAGE_SEND_EXTENDED_MESSAGE_SIZE
    _receivedSize = MESSAGE_SEND_EXTENDED_MESSAGE_RECEIVED_SIZE
    _description = "INSTEON Standard Message Send"

    def __init__(
        self, address, commandtuple, userdata, cmd2=None, flags=0x10, acknak=None
    ):
        """Init the ExtendedSend message class."""
        if commandtuple.get("cmd1", None) is not None:
            cmd1 = commandtuple["cmd1"]
            cmd2out = commandtuple["cmd2"]
        else:
            raise ValueError

        if cmd2 is not None:
            cmd2out = cmd2

        if cmd2out is None:
            raise ValueError

        self._address = Address(address)
        self._messageFlags = MessageFlags(flags)
        self._messageFlags.extended = 1
        self._cmd1 = cmd1
        self._cmd2 = cmd2out
        self._userdata = Userdata(userdata)
        self._acknak = self._setacknak(acknak)

    @classmethod
    def from_raw_message(cls, rawmessage):
        """Create a message from a raw byte stream."""
        userdata_dict = Userdata(rawmessage[8:22])
        return ExtendedSend(
            rawmessage[2:5],
            {"cmd1": rawmessage[6], "cmd2": rawmessage[7]},
            userdata_dict,
            flags=rawmessage[5],
            acknak=rawmessage[22:23],
        )

    # pylint: disable=protected-access
    @classmethod
    def template(
        cls,
        address=None,
        commandtuple=None,
        userdata=None,
        cmd2=-1,
        flags=None,
        acknak=None,
    ):
        """Create a message template used for callbacks."""
        msgraw = bytearray([0x02, cls._code])
        msgraw.extend(bytes(cls._receivedSize))
        msg = ExtendedSend.from_raw_message(msgraw)

        if commandtuple:
            cmd1 = commandtuple.get("cmd1")
            cmd2out = commandtuple.get("cmd2")
        else:
            cmd1 = None
            cmd2out = None

        if cmd2 is not -1:
            cmd2out = cmd2

        msg._address = Address(address)
        msg._messageFlags = MessageFlags(flags)
        msg._messageFlags.extended = 1
        msg._cmd1 = cmd1
        msg._cmd2 = cmd2out
        msg._userdata = Userdata.template(userdata)
        msg._acknak = acknak
        return msg

    @property
    def address(self):
        """Return the address of the device."""
        return self._address

    @property
    def cmd1(self):
        """Return the cmd1 property of the message."""
        return self._cmd1

    @property
    def cmd2(self):
        """Return the cmd2 property of the message."""
        return self._cmd2

    @property
    def userdata(self):
        """Return the extended message user data."""
        return self._userdata

    @property
    def flags(self):
        """Return the message flags."""
        return self._messageFlags

    @property
    def acknak(self):
        """Return the ACK/NAK byte."""
        return self._acknak

    @acknak.setter
    def acknak(self, val):
        """Set the ACK/NAK byte."""
        if val in [None, 0x06, 0x15]:
            self._acknak = val
        else:
            raise ValueError

    @property
    def isack(self):
        """Test if message is an ACK."""
        return self._acknak is not None and self._acknak == MESSAGE_ACK

    @property
    def isnak(self):
        """Test if message is a NAK."""
        return self._acknak is not None and self._acknak == MESSAGE_NAK

    def set_checksum(self):
        """Set byte 14 of the userdata to a checksum value."""
        data_sum = self.cmd1 + self.cmd2
        for i in range(1, 14):
            data_sum += self._userdata["d{:d}".format(i)]
        chksum = 0xFF - (data_sum & 0xFF) + 1
        self._userdata["d14"] = chksum & 0xff

    def set_crc(self):
        """Set Userdata[13] and Userdata[14] to the CRC value."""
        data = self.bytes[6:20]
        crc = int(0)
        for b in data:
            # pylint: disable=unused-variable
            for bit in range(0, 8):
                fb = b & 0x01
                fb = fb ^ 0x01 if (crc & 0x8000) else fb
                fb = fb ^ 0x01 if (crc & 0x4000) else fb
                fb = fb ^ 0x01 if (crc & 0x1000) else fb
                fb = fb ^ 0x01 if (crc & 0x0008) else fb
                crc = ((crc << 1) | fb) & 0xFFFF
                b = b >> 1
        self._userdata["d13"] = (crc >> 8) & 0xFF
        self._userdata["d14"] = crc & 0xFF

    def _message_properties(self):
        return [
            {"address": self._address},
            {"flags": self._messageFlags},
            {"cmd1": self._cmd1},
            {"cmd2": self._cmd2},
            {"userdata": self._userdata},
            {"acknak": self._acknak},
        ] class AllLinkRecordResponse(Message):
    """INSTEON ALL-Link Record Response.

    Message type 0x57
    """

    _code = MESSAGE_ALL_LINK_RECORD_RESPONSE_0X57
    _sendSize = MESSAGE_ALL_LINK_RECORD_RESPONSE_SIZE
    _receivedSize = MESSAGE_ALL_LINK_RECORD_RESPONSE_SIZE
    _description = "INSTEON ALL-Link Record Response"

    def __init__(self, flags, group, address, linkdata1, linkdata2, linkdata3):
        """Init the AllLinkRecordResponse Class."""
        self._controlFlags = flags
        self._group = group
        self._address = Address(address)
        self._linkdata1 = linkdata1
        self._linkdata2 = linkdata2
        self._linkdata3 = linkdata3

    @classmethod
    def from_raw_message(cls, rawmessage):
        """Create message from raw byte stream."""
        return AllLinkRecordResponse(
            rawmessage[2],
            rawmessage[3],
            rawmessage[4:7],
            rawmessage[7],
            rawmessage[8],
            rawmessage[9],
        )

    @property
    def controlFlags(self):
        """Return the link record control flags."""
        return self._controlFlags

    @property
    def group(self):
        """Return the link record group."""
        return self._group

    @property
    def address(self):
        """Return the device address."""
        return self._address

    @property
    def linkdata1(self):
        """Return the first link data field."""
        return self._linkdata1

    @property
    def linkdata2(self):
        """Return the second link data field."""
        return self._linkdata2

    @property
    def linkdata3(self):
        """Return the third link data field."""
        return self._linkdata3

    @property
    def isRecordinuse(self):
        """Test if the link record is in use."""
        return (
            self._controlFlags & CONTROL_FLAG_RECORD_IN_USE
        ) == CONTROL_FLAG_RECORD_IN_USE

    @property
    def isController(self):
        """Test if the link group is a controller."""
        return (self._controlFlags & CONTROL_FLAG_CONTROLLER) == CONTROL_FLAG_CONTROLLER

    @property
    def isSlave(self):
        """Test if the link group is a slave or responder."""
        return not self.isController

    def _message_properties(self):
        return [
            {"controlFlags": self._controlFlags},
            {"group": self._group},
            {"address": self._address},
            {"linkdata1": self._linkdata1},
            {"linkdata2": self._linkdata2},
            {"linkdata3": self._linkdata3},
        ] class GetImInfo(Message):
    """INSTEON Get Insteon Modem Info Message.

    Message type 0x60
    """

    _code = MESSAGE_GET_IM_INFO_0X60
    _sendSize = MESSAGE_GET_IM_INFO_SIZE
    _receivedSize = MESSAGE_GET_IM_INFO_RECEIVED_SIZE
    _description = "INSTEON Get Insteon Modem Info Message Received"

    def __init__(self, address=None, cat=None, subcat=None, firmware=None, acknak=None):
        """Init the GetImInfo Class."""
        self._address = Address(address)
        self._category = cat
        self._subcategory = subcat
        self._firmware = firmware
        self._acknak = self._setacknak(acknak)

    @classmethod
    def from_raw_message(cls, rawmessage):
        """Create message from raw byte stream."""
        return GetImInfo(
            rawmessage[2:5], rawmessage[5], rawmessage[6], rawmessage[7], rawmessage[8]
        )

    @property
    def address(self):
        """Return the device address."""
        return self._address

    @property
    def category(self):
        """Return the IM device category."""
        return self._category

    @property
    def subcategory(self):
        """Return the IM device subcategory."""
        return self._subcategory

    @property
    def firmware(self):
        """Return the IM device firmware version."""
        return self._firmware

    @property
    def acknak(self):
        """Return the ACK/NAK byte."""
        return self._acknak

    @property
    def isack(self):
        """Test if this is an ACK message."""
        return self._acknak is not None and self._acknak == MESSAGE_ACK

    @property
    def isnak(self):
        """Test if this is a NAK message."""
        return self._acknak is not None and self._acknak == MESSAGE_NAK

    def _message_properties(self):
        return [
            {"address": self._address},
            {"category": self._category},
            {"subcategory": self._subcategory},
            {"firmware": self._firmware},
            {"acknak": self._acknak},
        ] class GetNextAllLinkRecord(Message):
    """Insteon Get Next All Link Record Message.

    Message type 0x6A
    """

    _code = MESSAGE_GET_NEXT_ALL_LINK_RECORD_0X6A
    _sendSize = MESSAGE_GET_NEXT_ALL_LINK_RECORD_SIZE
    _receivedSize = MESSAGE_GET_NEXT_ALL_LINK_RECORD_RECEIVED_SIZE
    _description = "Insteon Get Next All Link Record Message"

    def __init__(self, acknak=None):
        """Init the GetNextAllLinkRecord Class."""
        self._acknak = self._setacknak(acknak)

    @classmethod
    def from_raw_message(cls, rawmessage):
        """Create message from raw byte stream."""
        return GetNextAllLinkRecord(rawmessage[2:3])

    @property
    def acknak(self):
        """Return the ACK/NAK byte."""
        return self._acknak

    @property
    def isack(self):
        """Test if this is an ACK message."""
        return self._acknak is not None and self._acknak == MESSAGE_ACK

    @property
    def isnak(self):
        """Test if this is a NAK message."""
        return self._acknak is not None and self._acknak == MESSAGE_NAK

    def _message_properties(self):
        return [{"acknak": self._acknak}] class Userdata:
    """Extended Message User Data Type."""

    def __init__(self, userdata=None):
        """Init the Userdata Class."""
        self._userdata = self.normalize(self.create_empty(0x00), userdata)

    def __len__(self):
        """Init Userdata Class."""
        return len(self._userdata)

    def __str__(self):
        """Return string representation of user data."""
        return self.human

    def __iter__(self):
        """Iterate through the user data bytes."""
        for itm in self._userdata:
            yield itm

    def __getitem__(self, key):
        """Return a single byte of the user data."""
        return self._userdata.get(key)

    def __setitem__(self, key, val):
        """Set a user data element."""
        self._userdata[key] = val

    def __eq__(self, other):
        """Test if the current user data equals another user data instance."""
        isequal = False
        if isinstance(other, Userdata):
            for key in self._userdata:
                if self._userdata[key] == other[key]:
                    isequal = True
                else:
                    isequal = False
                    break
        return isequal

    def __ne__(self, other):
        """Test if the current user data is not equal to another instance."""
        return bool(self != other)

    @property
    def human(self):
        """Emit the address in human-readible format (AA.BB.CC)."""
        strout = ""
        first = True
        for i in range(0, 28, 2):
            if first:
                first = False
            else:
                strout = strout + "."
            strout = strout + self.hex[i : i + 2]
        return strout

    @property
    def hex(self):
        """Emit the address in bare hex format."""
        return binascii.hexlify(self.bytes).decode()

    @property
    def bytes(self):
        """Emit the address in bytes format."""
        byteout = bytearray()
        for i in range(1, 15):
            key = "d" + str(i)
            if self._userdata[key] is not None:
                byteout.append(self._userdata[key])
            else:
                byteout.append(0x00)
        return byteout

    @classmethod
    def from_raw_message(cls, rawmessage):
        """Create a user data instance from a raw byte stream."""
        empty = cls.create_empty(0x00)
        userdata_dict = cls.normalize(empty, rawmessage)
        return Userdata(userdata_dict)

    @classmethod
    def create_pattern(cls, userdata):
        """Create a user data instance with all values the same."""
        empty = cls.create_empty(None)
        userdata_dict = cls.normalize(empty, userdata)
        return Userdata(userdata_dict)

    @classmethod
    def create(cls):
        """Create an empty user data instance."""
        empty = cls.create_empty(0x00)
        return Userdata(empty)

    @classmethod
    def template(cls, userdata):
        """Create a template instance used for message callbacks."""
        ud = Userdata(cls.normalize(cls.create_empty(None), userdata))
        return ud

    def matches_pattern(self, other):
        """Test if the current instance matches a template instance."""
        ismatch = False
        if isinstance(other, Userdata):
            for key in self._userdata:
                if self._userdata[key] is None or other[key] is None:
                    ismatch = True
                elif self._userdata[key] == other[key]:
                    ismatch = True
                else:
                    ismatch = False
                    break
        return ismatch

    def get(self, key):
        """Return a single byte of the user data."""
        return self[key]

    def to_dict(self):
        """Return userdata as a dict object."""
        return self._userdata

    @classmethod
    def _dict_to_dict(cls, empty, userdata):
        if isinstance(userdata, dict):
            for key in userdata:
                if key in [
                    "d1",
                    "d2",
                    "d3",
                    "d4",
                    "d5",
                    "d6",
                    "d7",
                    "d8",
                    "d9",
                    "d10",
                    "d11",
                    "d12",
                    "d13",
                    "d14",
                ]:
                    empty[key] = userdata[key]
        return empty

    @classmethod
    def _bytes_to_dict(cls, empty, userdata):
        if len(userdata) == 14:
            for i in range(1, 15):
                key = "d{}".format(i)
                empty[key] = userdata[i - 1]
        else:
            raise ValueError
        return empty

    @classmethod
    def create_empty(cls, val=0x00):
        """Create an empty Userdata object.

        val: value to fill the empty user data fields with (default is 0x00)
        """
        userdata_dict = {}
        for i in range(1, 15):
            key = "d{}".format(i)
            userdata_dict.update({key: val})
        return userdata_dict

    @classmethod
    def normalize(cls, empty, userdata):
        """Return normalized user data as a dictionary.

        empty: an empty dictionary
        userdata: data in the form of Userdata, dict or None
        """
        if isinstance(userdata, Userdata):
            return userdata.to_dict()
        if isinstance(userdata, dict):
            return cls._dict_to_dict(empty, userdata)
        if isinstance(userdata, (bytes, bytearray)):
            return cls._bytes_to_dict(empty, userdata)
        if userdata is None:
            return empty
        raise ValueError class MockCallbacks():
    """Mock callback class to support device and state testing."""

    def __init__(self):
        """Init the MockCallbacks Class."""
        self.callbackvalue1 = None
        self.callbackvalue2 = None
        self.callbackvalue3 = None
        self.callbackvalue4 = None
        self.callbackvalue5 = None
        self.callbackvalue6 = None
        self.callbackvalue7 = None
        self.callbackvalue8 = None
        self.callbackvalue9 = None

    def callbackmethod1(self, addr, state, value):
        """Receive notice of callback method 1."""
        self._report_callback(1, addr, state, value)
        self.callbackvalue1 = value

    def callbackmethod2(self, addr, state, value):
        """Receive notice of callback method 2."""
        self._report_callback(2, addr, state, value)
        self.callbackvalue2 = value

    def callbackmethod3(self, addr, state, value):
        """Receive notice of callback method 3."""
        self._report_callback(3, addr, state, value)
        self.callbackvalue3 = value

    def callbackmethod4(self, addr, state, value):
        """Receive notice of callback method 5."""
        self._report_callback(4, addr, state, value)
        self.callbackvalue4 = value

    def callbackmethod5(self, addr, state, value):
        """Receive notice of callback method 5."""
        self._report_callback(5, addr, state, value)
        self.callbackvalue5 = value

    def callbackmethod6(self, addr, state, value):
        """Receive notice of callback method 6."""
        self._report_callback(6, addr, state, value)
        self.callbackvalue6 = value

    def callbackmethod7(self, addr, state, value):
        """Receive notice of callback method 7."""
        self._report_callback(7, addr, state, value)
        self.callbackvalue7 = value

    def callbackmethod8(self, addr, state, value):
        """Receive notice of callback method 8."""
        self._report_callback(8, addr, state, value)
        self.callbackvalue8 = value

    def callbackmethod9(self, addr, state, value):
        """Receive notice of callback method 9."""
        _LOGGER.debug('Called method 9 callback')
        self.callbackvalue9 = value

    @staticmethod
    def _report_callback(callback, addr, state, value):
        _LOGGER.debug('Called method %d for address %s group %s value %s',
                      callback, addr, state, value)
import logging
from insteonplm.messagecallback import MessageCallback
from insteonplm.constants import (COMMAND_LIGHT_ON_0X11_NONE,
                                  MESSAGE_ACK,
                                  MESSAGE_NAK)
from insteonplm.messages.standardReceive import StandardReceive
from insteonplm.messages.standardSend import StandardSend
from insteonplm.messages.extendedReceive import ExtendedReceive
from insteonplm.messages.extendedSend import ExtendedSend
from insteonplm.messages.allLinkRecordResponse import AllLinkRecordResponse
from insteonplm.messages.getIMInfo import GetImInfo
from insteonplm.messages.getNextAllLinkRecord import GetNextAllLinkRecord
from insteonplm.messages.userdata import Userdata
from .mockCallbacks import MockCallbacks
"""Test message callbacks."""



_LOGGER = logging.getLogger(__name__)


def test_messagecallback_basic():
    """Test messagecallback basic."""
    callbacks = MessageCallback()
    callbacktest1 = "test callback 1"

    msg_template = StandardReceive.template(
        commandtuple=COMMAND_LIGHT_ON_0X11_NONE, flags=0x80)
    callbacks[msg_template] = callbacktest1

    msg = StandardReceive('1a2b3c', '4d5e6f', COMMAND_LIGHT_ON_0X11_NONE,
                          cmd2=0xff, flags=0x80)

    callback1 = callbacks.get_callbacks_from_message(msg)

    assert len(callback1) == 1
    assert callback1[0] == callbacktest1


def test_messagecallback_msg():
    """Test messagecallback msg."""
    callbacks = MessageCallback()
    callbacktest = "test callback"
    address = '1a2b3c'
    target = '4d5e6f'

    template_on = StandardReceive.template(
        commandtuple=COMMAND_LIGHT_ON_0X11_NONE)
    callbacks.add(template_on, callbacktest)
    msg1 = StandardReceive(address, target, COMMAND_LIGHT_ON_0X11_NONE,
                           cmd2=0x00)
    msg2 = StandardReceive(address, target, COMMAND_LIGHT_ON_0X11_NONE,
                           cmd2=0xff)

    callback1 = callbacks.get_callbacks_from_message(msg1)
    callback2 = callbacks.get_callbacks_from_message(msg2)

    assert callback1[0] == callbacktest
    assert callback2[0] == callbacktest


# pylint: disable=too-many-locals
def test_messagecallback_acknak():
    """Test messagecallback acknak."""
    callbacks = MessageCallback()
    callbacktest1 = "test callback 1"
    callbacktest2 = "test callback 2"
    callbacktest3 = "test callback 3"
    callbacktest4 = "test callback 4"
    address = '1a2b3c'

    template_address = StandardSend.template(address=address)
    template_address_ack = StandardSend.template(address=address,
                                                 acknak=MESSAGE_ACK)
    template_empty = StandardSend.template()
    template_nak = StandardSend.template(acknak=MESSAGE_NAK)
    callbacks.add(template_address, callbacktest1)
    callbacks.add(template_address_ack, callbacktest2)
    callbacks.add(template_empty, callbacktest3)
    callbacks.add(template_nak, callbacktest4)

    msg1 = StandardSend(address, COMMAND_LIGHT_ON_0X11_NONE, cmd2=0xcd)

    msg2 = StandardSend('222222', COMMAND_LIGHT_ON_0X11_NONE, cmd2=0xff)