Пример #1
0
    def __init__(self,
                 plm,
                 address,
                 cat,
                 subcat,
                 product_key=0x00,
                 description='',
                 model=''):
        """Initialize the Device class."""
        self.log = logging.getLogger(__name__)

        self._plm = plm

        self._address = Address(address)
        self._cat = cat
        self._subcat = subcat
        if self._subcat is None:
            self._subcat = 0x00
        self._product_key = product_key
        if self._product_key is None:
            self._product_key = 0x00
        self._description = description
        self._model = model

        self._last_communication_received = datetime.datetime(1, 1, 1, 1, 1, 1)
        self._recent_messages = asyncio.Queue(loop=self._plm.loop)
        self._product_data_in_aldb = False
        self._stateList = StateList()
        self._send_msg_lock = asyncio.Lock(loop=self._plm.loop)
        self._sent_msg_wait_for_directACK = {}
        self._directACK_received_queue = asyncio.Queue(loop=self._plm.loop)
        self._message_callbacks = MessageCallback()
        self._aldb = ALDB(self._send_msg, self._plm.loop, self._address)

        self._register_messages()
Пример #2
0
    def __init__(self,
                 loop=None,
                 connection_lost_callback=None,
                 workdir=None,
                 poll_devices=True,
                 load_aldb=True):
        """Protocol handler that handles all status and changes on PLM."""
        self._loop = loop
        self._connection_lost_callback = connection_lost_callback

        self._buffer = asyncio.Queue(loop=self._loop)
        self._recv_queue = deque([])
        self._send_queue = asyncio.Queue(loop=self._loop)
        self._acknak_queue = asyncio.Queue(loop=self._loop)
        self._next_all_link_rec_nak_retries = 0
        self._aldb_devices = {}
        self._devices = LinkedDevices(loop, workdir)
        self._poll_devices = poll_devices
        self._load_aldb = load_aldb
        self._write_transport_lock = asyncio.Lock(loop=self._loop)
        self._message_callbacks = MessageCallback()
        self._x10_address = None

        # Callback lists
        self._cb_load_all_link_db_done = []
        self._cb_device_not_active = []

        super().__init__(self, '000000', 0x03, None, None, '', '')

        self.transport = None

        self._register_message_handlers()
        self._writer_task = None
        self._restart_writer = False
        self.restart_writing()
def test_extended_ack():
    """Test extended ack."""
    callbacks = MockCallbacks()
    callbacks.callbackvalue1 = "Callback 1"
    callbacks.callbackvalue2 = "Callback 2"
    message_callbacks = MessageCallback()
    address = '1a2b3c'

    template_ext_ack = ExtendedSend.template(address, acknak=MESSAGE_ACK)
    template_std_ack = StandardSend.template(address, acknak=MESSAGE_ACK)

    message_callbacks.add(template_ext_ack, callbacks.callbackvalue1)
    message_callbacks.add(template_std_ack, callbacks.callbackvalue2)
    extmsg = ExtendedSend(address,
                          COMMAND_LIGHT_ON_0X11_NONE, {'d1': 0x02},
                          cmd2=0xff,
                          acknak=MESSAGE_ACK)
    stdmsg = StandardSend(address,
                          COMMAND_LIGHT_ON_0X11_NONE,
                          cmd2=0xff,
                          acknak=MESSAGE_ACK)
    result1 = message_callbacks.get_callbacks_from_message(extmsg)
    result2 = message_callbacks.get_callbacks_from_message(stdmsg)

    assert result2 == [callbacks.callbackvalue2]
    assert result1 == [callbacks.callbackvalue1]
def test_message_callback_extended():
    """Test message callback extended."""
    callbacks = MessageCallback()
    callbacktest = "test callback"
    address = '1a2b3c'
    target = '4d5e6f'

    template_ext_on = ExtendedReceive.template(
        commandtuple=COMMAND_LIGHT_ON_0X11_NONE,
        userdata=Userdata({'d1': 0x02}))
    callbacks.add(template_ext_on, callbacktest)
    msg1 = ExtendedReceive(address,
                           target,
                           COMMAND_LIGHT_ON_0X11_NONE,
                           Userdata({'d1': 0x02}),
                           cmd2=0xff)
    msg2 = ExtendedReceive(address,
                           target,
                           COMMAND_LIGHT_ON_0X11_NONE,
                           Userdata({
                               'd1': 0x03,
                               'd2': 0x02
                           }),
                           cmd2=0xff)

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

    assert callback1[0] == callbacktest
    assert not callback2
Пример #5
0
 def __init__(self, loop=None):
     """Initialize the MockPLM class."""
     self.log = logging.getLogger()
     self.sentmessage = ''
     self._message_callbacks = MessageCallback()
     self.loop = loop
     self.devices = LinkedDevices()
Пример #6
0
 def __init__(self, plm, housecode, unitcode):
     """Initialize the X10Device class."""
     self._address = Address.x10(housecode, unitcode)
     self._plm = plm
     self._description = "Generic X10 device"
     self._model = ''
     self._aldb = ALDB(None, None, self._address, version=ALDBVersion.Null)
     self._message_callbacks = MessageCallback()
     self._stateList = StateList()
     self._send_msg_lock = asyncio.Lock(loop=self._plm.loop)
     self.log = logging.getLogger()
def test_misc_messages():
    """Test misc messages."""
    callbacks = MessageCallback()
    callbacktest1 = "test callback 1"
    callbacktest2 = "test callback 2"
    callbacktest3 = "test callback 3"

    msgtemplate1 = AllLinkRecordResponse(None, None, None, None, None, None)
    msgtemplate2 = GetImInfo()
    msgtemplate3 = GetNextAllLinkRecord(acknak=MESSAGE_NAK)
    callbacks.add(msgtemplate1, callbacktest1)
    callbacks.add(msgtemplate2, callbacktest2)
    callbacks.add(msgtemplate3, callbacktest3)

    msg1 = AllLinkRecordResponse(0x00, 0x01, '1a2b3c', 0x01, 0x02, 0x03)
    msg2 = GetImInfo()
    msg3 = GetNextAllLinkRecord(acknak=MESSAGE_ACK)
    msg4 = GetNextAllLinkRecord(acknak=MESSAGE_NAK)

    callback_list1 = callbacks.get_callbacks_from_message(msg1)
    callback_list2 = callbacks.get_callbacks_from_message(msg2)
    callback_list3 = callbacks.get_callbacks_from_message(msg3)
    callback_list4 = callbacks.get_callbacks_from_message(msg4)

    assert callback_list1[0] == callbacktest1
    assert callback_list2[0] == callbacktest2
    assert not callback_list3
    assert callback_list4[0] == callbacktest3
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
Пример #9
0
class MockPLM(object):
    """Mock PLM class for testing devices."""
    def __init__(self, loop=None):
        """Initialize the MockPLM class."""
        self.log = logging.getLogger()
        self.sentmessage = ''
        self._message_callbacks = MessageCallback()
        self.loop = loop
        self.devices = LinkedDevices()

    @property
    def message_callbacks(self):
        """Return the message callback list."""
        return self._message_callbacks

    def send_msg(self, msg, wait_nak=True, wait_timeout=2):
        """Send a message mock routine."""
        self.sentmessage = msg.hex

    def message_received(self, msg):
        """Fake a message being received by the PLM."""
        if hasattr(msg, 'address'):
            device = self.devices[msg.address.id]
            if device:
                device.receive_message(msg)
            else:
                self.log.info('Received message for unknown device %s',
                              msg.address)
        for callback in (
                self._message_callbacks.get_callbacks_from_message(msg)):
            callback(msg)
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
def test_delete_callback():
    """Test delete callback."""
    callbacks = MessageCallback()
    callbacktest1 = "test callback 1"
    callbacktest2 = "test callback 2"
    callbacktest3 = "test callback 3"

    callbacks.add(StandardSend.template(), callbacktest1)
    callbacks.add(StandardSend.template(), callbacktest2)
    callbacks.add(StandardSend.template(acknak=MESSAGE_NAK), callbacktest3)

    msg = StandardSend('333333', COMMAND_LIGHT_ON_0X11_NONE, cmd2=0xff,
                       acknak=MESSAGE_NAK)

    callback_list = callbacks.get_callbacks_from_message(msg)
    assert len(callback_list) == 3

    callbacks.remove(StandardSend.template(), callbacktest2)
    callback_list = callbacks.get_callbacks_from_message(msg)
    assert len(callback_list) == 2
Пример #12
0
class MockPLM():
    """Mock PLM class for testing devices."""

    def __init__(self, loop=None):
        """Init the MockPLM class."""
        self.sentmessage = ''
        self._message_callbacks = MessageCallback()
        self.loop = loop
        self.devices = LinkedDevices()

    @property
    def message_callbacks(self):
        """Return the message callback list."""
        return self._message_callbacks

    # pylint: disable=unused-argument
    def send_msg(self, msg, wait_nak=True, wait_timeout=2):
        """Send a message mock routine."""
        _LOGGER.debug('TX: %s:%s', id(msg), msg)
        self.sentmessage = msg.hex

    def message_received(self, msg):
        """Fake a message being received by the PLM."""
        _LOGGER.debug('RX: %s:%s', id(msg), msg)
        if hasattr(msg, 'address'):
            device = self.devices[msg.address.id]
            if device:
                device.receive_message(msg)
            else:
                _LOGGER.info('Received message for unknown device %s',
                             msg.address)
        for callback in (
                self._message_callbacks.get_callbacks_from_message(msg)):
            callback(msg)

    # pylint: disable=unused-argument
    def start_all_linking(self, linkcode, group):
        """Fake start all linking."""
        self.sentmessage = b'02112233445566'
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)
    msg3 = StandardSend('333333',
                        COMMAND_LIGHT_ON_0X11_NONE,
                        cmd2=0xff,
                        acknak=MESSAGE_NAK)
    msg4 = StandardSend('444444',
                        COMMAND_LIGHT_ON_0X11_NONE,
                        cmd2=0xff,
                        acknak=MESSAGE_ACK)

    _LOGGER.debug('Getting callbacks for message 1')
    callback1 = callbacks.get_callbacks_from_message(msg1)
    _LOGGER.debug('Getting callbacks for message 2')
    callback2 = callbacks.get_callbacks_from_message(msg2)
    _LOGGER.debug('Getting callbacks for message 3')
    callback3 = callbacks.get_callbacks_from_message(msg3)
    _LOGGER.debug('Getting callbacks for message 4')
    callback4 = callbacks.get_callbacks_from_message(msg4)

    assert len(callback1) == 4
    assert len(callback2) == 2
    assert len(callback3) == 2
    assert len(callback4) == 1
Пример #14
0
class IM(Device, asyncio.Protocol):
    """The Insteon PLM IP control protocol handler.

        This class is expected to be wrapped inside a Connection class object
        which will maintain the socket and handle auto-reconnects.

            :param connection_lost_callback:
                called when connection is lost to device (optional)
            :param loop:
                asyncio event loop (optional)
            :param workdir:
                Working directory name to save device information (optional)

            :type: connection_lost_callback:
                callable
            :type loop:
                asyncio.loop
            :type workdir:
                string - valid directory path
        """

    def __init__(self, loop=None, connection_lost_callback=None,
                 workdir=None, poll_devices=True, load_aldb=True):
        """Protocol handler that handles all status and changes on PLM."""
        self._loop = loop
        self._connection_lost_callback = connection_lost_callback

        self._buffer = asyncio.Queue(loop=self._loop)
        self._recv_queue = deque([])
        self._send_queue = asyncio.Queue(loop=self._loop)
        self._acknak_queue = asyncio.Queue(loop=self._loop)
        self._aldb_response_queue = {}
        self._devices = LinkedDevices(loop, workdir)
        self._poll_devices = poll_devices
        self._load_aldb = load_aldb
        self._write_transport_lock = asyncio.Lock(loop=self._loop)
        self._message_callbacks = MessageCallback()
        self._x10_address = None

        # Callback lists
        self._cb_load_all_link_db_done = []
        self._cb_device_not_active = []

        super().__init__(self, '000000', 0x03, None, None, '', '')

        self.log = logging.getLogger(__name__)
        self.transport = None

        self._register_message_handlers()

    # public properties
    @property
    def devices(self):
        """Return the list of devices linked to the IM."""
        return self._devices

    @property
    def loop(self):
        """Return the asyncio loop."""
        return self._loop

    @property
    def message_callbacks(self):
        """Return the list of message callbacks."""
        return self._message_callbacks

    # asyncio.protocol interface methods
    def connection_made(self, transport):
        """Called when asyncio.Protocol establishes the network connection."""
        raise NotImplementedError

    def data_received(self, data):
        """Called when asyncio.Protocol detects received data from network."""
        self.log.debug("Starting: data_received")
        self.log.debug('Received %d bytes from PLM: %s',
                       len(data), binascii.hexlify(data))
        self._buffer.put_nowait(data)
        asyncio.ensure_future(self._peel_messages_from_buffer(),
                              loop=self._loop)

        self.log.debug("Finishing: data_received")

    def connection_lost(self, exc):
        """Called when asyncio.Protocol loses the network connection."""
        if exc is None:
            self.log.warning('eof from modem?')
        else:
            self.log.warning('Lost connection to modem: %s', exc)

        self.transport = None

        if self._connection_lost_callback:
            self._connection_lost_callback()

    # Methods used to trigger callbacks for specific events
    def add_device_callback(self, callback):
        """Register a callback for when a matching new device is seen."""
        self.devices.add_device_callback(callback)

    def add_all_link_done_callback(self, callback):
        """Register a callback to be invoked when the ALDB is loaded."""
        self.log.debug('Added new callback %s ',
                       callback)
        self._cb_load_all_link_db_done.append(callback)

    def add_device_not_active_callback(self, callback):
        """Register callback to be invoked when a device is not repsonding."""
        self.log.debug('Added new callback %s ',
                       callback)
        self._cb_device_not_active.append(callback)

    # Public methods
    def poll_devices(self):
        """Request status updates from each device."""
        for addr in self.devices:
            device = self.devices[addr]
            if not device.address.is_x10:
                device.async_refresh_state()

    def send_msg(self, msg, wait_nak=True, wait_timeout=WAIT_TIMEOUT):
        """Place a message on the send queue for sending.

        Message are sent in the order they are placed in the queue.
        """
        self.log.debug("Starting: send_msg")
        write_message_coroutine = self._write_message_from_send_queue()
        wait_info = {'msg': msg,
                     'wait_nak': wait_nak,
                     'wait_timeout': wait_timeout}
        self._send_queue.put_nowait(wait_info)
        asyncio.ensure_future(write_message_coroutine, loop=self._loop)
        self.log.debug("Ending: send_msg")

    def start_all_linking(self, mode, group):
        """Put the IM into All-Linking mode.

        Puts the IM into All-Linking mode for 4 minutes.

        Parameters:
            mode: 0 | 1 | 3 | 255
                  0 - PLM is responder
                  1 - PLM is controller
                  3 - Device that initiated All-Linking is Controller
                255 = Delete All-Link
            group: All-Link group number (0 - 255)
        """
        msg = StartAllLinking(mode, group)
        self.send_msg(msg)

    def add_x10_device(self, housecode, unitcode, feature='OnOff'):
        """Add an X10 device based on a feature description.

        Current features are:
        - OnOff
        - Dimmable
        - Sensor
        - AllUnitsOff
        - AllLightsOn
        - AllLightsOff
        """
        device = insteonplm.devices.create_x10(self, housecode,
                                               unitcode, feature)
        if device:
            self.devices[device.address.id] = device
        return device

    def monitor_mode(self):
        """Put the Insteon Modem in monitor mode."""
        msg = SetIMConfiguration(0x40)
        self.send_msg(msg)

    @asyncio.coroutine
    def _setup_devices(self):
        yield from self.devices.load_saved_device_info()
        self.log.debug('Found %d saved devices',
                       len(self.devices.saved_devices))
        self._get_plm_info()
        self.devices.add_known_devices(self)
        self._load_all_link_database()

    @asyncio.coroutine
    def _write_message_from_send_queue(self):
        if not self._write_transport_lock.locked():
            self.log.debug('Aquiring write lock')
            yield from self._write_transport_lock.acquire()
            while True:
                # wait for an item from the queue
                try:
                    with async_timeout.timeout(WAIT_TIMEOUT):
                        msg_info = yield from self._send_queue.get()
                        msg = msg_info.get('msg')
                        wait_nak = msg_info.get('wait_nak')
                        wait_timeout = msg_info.get('wait_timeout')
                except asyncio.TimeoutError:
                    self.log.debug('No new messages received.')
                    break
                # process the item
                self.log.debug('Writing message: %s', msg)
                write_bytes = msg.bytes
                if hasattr(msg, 'acknak') and msg.acknak:
                    write_bytes = write_bytes[:-1]
                if self.transport:
                    self.log.debug('Transport is open')
                    self.transport.write(msg.bytes)
                else:
                    self.log.debug("Transport is not open. Cannot write")
                if wait_nak:
                    self.log.debug('Waiting for ACK or NAK message')
                    is_nak = False
                    try:
                        with async_timeout.timeout(ACKNAK_TIMEOUT):
                            while True:
                                acknak = yield from self._acknak_queue.get()
                                if msg.matches_pattern(acknak):
                                    self.log.debug('ACK or NAK received')
                                    self.log.debug(acknak)
                                    is_nak = acknak.isnak
                                break
                    except asyncio.TimeoutError:
                        self.log.debug('No ACK or NAK message received.')
                        is_nak = True
                    if is_nak:
                        self._handle_nak(msg)
                yield from asyncio.sleep(wait_timeout, loop=self._loop)
            self._write_transport_lock.release()

    def _get_plm_info(self):
        """Request PLM Info."""
        self.log.info('Requesting PLM Info')
        msg = GetImInfo()
        self.send_msg(msg, wait_nak=True, wait_timeout=.5)

    def _load_all_link_database(self):
        """Load the ALL-Link Database into object."""
        self.log.debug("Starting: _load_all_link_database")
        self.devices.state = 'loading'
        self._get_first_all_link_record()
        self.log.debug("Ending: _load_all_link_database")

    def _get_first_all_link_record(self):
        """Request first ALL-Link record."""
        self.log.debug("Starting: _get_first_all_link_record")
        self.log.info('Requesting ALL-Link Records')
        msg = GetFirstAllLinkRecord()
        self.send_msg(msg, wait_nak=True, wait_timeout=.5)
        self.log.debug("Ending: _get_first_all_link_record")

    def _get_next_all_link_record(self):
        """Request next ALL-Link record."""
        self.log.debug("Starting: _get_next_all_link_record")
        self.log.debug("Requesting Next All-Link Record")
        msg = GetNextAllLinkRecord()
        self.send_msg(msg, wait_nak=True, wait_timeout=.5)
        self.log.debug("Ending: _get_next_all_link_record")

    # Inbound message handlers sepcific to the IM
    def _register_message_handlers(self):
        template_assign_all_link = StandardReceive.template(
            commandtuple=COMMAND_ASSIGN_TO_ALL_LINK_GROUP_0X01_NONE)
        template_all_link_response = AllLinkRecordResponse(None, None, None,
                                                           None, None, None)
        template_get_im_info = GetImInfo()
        template_next_all_link_rec = GetNextAllLinkRecord(acknak=MESSAGE_NAK)
        template_all_link_complete = AllLinkComplete(None, None, None,
                                                     None, None, None)
        template_x10_send = X10Send(None, None, MESSAGE_ACK)
        template_x10_received = X10Received(None, None)

        self._message_callbacks.add(
            template_assign_all_link,
            self._handle_assign_to_all_link_group)

        self._message_callbacks.add(
            template_all_link_response,
            self._handle_all_link_record_response)

        self._message_callbacks.add(
            template_get_im_info,
            self._handle_get_plm_info)

        self._message_callbacks.add(
            template_next_all_link_rec,
            self._handle_get_next_all_link_record_nak)

        self._message_callbacks.add(
            template_all_link_complete,
            self._handle_assign_to_all_link_group)

        self._message_callbacks.add(
            template_x10_send,
            self._handle_x10_send_receive)

        self._message_callbacks.add(
            template_x10_received,
            self._handle_x10_send_receive)

    @asyncio.coroutine
    def _peel_messages_from_buffer(self):
        self.log.debug("Starting: _peel_messages_from_buffer")
        lastlooplen = 0
        worktodo = True
        buffer = bytearray()
        while worktodo:
            buffer.extend(self._unpack_buffer())
            if len(buffer) < 2:
                worktodo = False
                break
            self.log.debug('Total buffer: %s', binascii.hexlify(buffer))
            msg, buffer = insteonplm.messages.create(buffer)

            if msg is not None:
                self.log.debug('Msg buffer: %s', msg.hex)
                self._recv_queue.appendleft(msg)

            self.log.debug('Post buffer: %s', binascii.hexlify(buffer))
            if len(buffer) < 2:
                self.log.debug('Buffer too short to have a message')
                worktodo = False
                break

            if len(buffer) == lastlooplen:
                self.log.debug("Buffer size did not change wait for more data")
                worktodo = False
                break

            lastlooplen = len(buffer)
        if len(buffer) > 0:
            buffer.extend(self._unpack_buffer())
            self._buffer.put_nowait(buffer)

        self.log.debug('Messages in queue: %d', len(self._recv_queue))
        worktodo = True
        while worktodo:
            try:
                msg = self._recv_queue.pop()
                self.log.debug('Processing message %s', msg)
                callbacks = \
                    self._message_callbacks.get_callbacks_from_message(msg)
                if hasattr(msg, 'isack') or hasattr(msg, 'isnak'):
                    self._acknak_queue.put_nowait(msg)
                if hasattr(msg, 'address'):
                    device = self.devices[msg.address.hex]
                    if device:
                        device.receive_message(msg)
                for callback in callbacks:
                    self._loop.call_soon(callback, msg)
            except IndexError:
                self.log.debug('Last item in self._recv_queue reached.')
                worktodo = False

        self.log.debug("Finishing: _peel_messages_from_buffer")

    def _unpack_buffer(self):
        buffer = bytearray()
        while not self._buffer.empty():
            buffer.extend(self._buffer.get_nowait())
        return buffer

    def _handle_assign_to_all_link_group(self, msg):
        cat = 0xff
        subcat = 0
        product_key = 0
        if msg.code == StandardReceive.code and msg.flags.isBroadcast:
            self.log.debug('Received broadcast ALDB group assigment request.')
            cat = msg.targetLow
            subcat = msg.targetMed
            product_key = msg.targetHi
            self._add_device_from_prod_data(msg.address, cat,
                                            subcat, product_key)
        elif msg.code == AllLinkComplete.code:
            if msg.linkcode in [0, 1, 3]:
                self.log.debug('Received ALDB complete response.')
                cat = msg.category
                subcat = msg.subcategory
                product_key = msg.firmware
                self._add_device_from_prod_data(msg.address, cat,
                                                subcat, product_key)
                self._update_aldb_records(msg.linkcode, msg.address, msg.group)
            else:
                self.log.debug('Received ALDB delete response.')
                self._update_aldb_records(msg.linkcode, msg.address, msg.group)

    def _add_device_from_prod_data(self, address, cat, subcat, product_key):
        self.log.debug('Received Device ID with address: %s  '
                       'cat: 0x%x  subcat: 0x%x', address, cat, subcat)
        device = self.devices.create_device_from_category(
            self, address, cat, subcat, product_key)
        if device:
            if self.devices[device.id] is None:
                self.devices[device.id] = device
                self.log.info('Device with id %s added to device list.',
                              device.id)
        else:
            self.log.error('Device %s not in the IPDB.',
                           Address(address).human)
        self.log.info('Total Devices Found: %d', len(self.devices))

    def _update_aldb_records(self, linkcode, address, group):
        """Refresh the IM and device ALDB records."""
        device = self.devices[Address(address).id]
        if device and device.aldb.status in [ALDBStatus.LOADED,
                                             ALDBStatus.PARTIAL]:
            for mem_addr in device.aldb:
                rec = device.aldb[mem_addr]
                if linkcode in [0, 1, 3]:
                    if rec.control_flags.is_high_water_mark:
                        self.log.info('Removing HWM recordd %04x', mem_addr)
                        device.aldb.pop(mem_addr)
                    elif not rec.control_flags.is_in_use:
                        self.log.info('Removing not in use recordd %04x',
                                      mem_addr)
                        device.aldb.pop(mem_addr)
                else:
                    if rec.address == self.address and rec.group == group:
                        self.log.info('Removing record %04x with addr %s and '
                                      'group %d', mem_addr, rec.address,
                                      rec.group)
                        device.aldb.pop(mem_addr)
            device.read_aldb()
            device.aldb.add_loaded_callback(self._refresh_aldb())

    def _refresh_aldb(self):
        self.aldb.clear()
        self._load_all_link_database()

    def _handle_standard_or_extended_message_received(self, msg):
        device = self.devices[msg.address.id]
        if device is not None:
            device.receive_message(msg)

    def _handle_all_link_record_response(self, msg):
        self.log.debug('Found all link record for device %s', msg.address.hex)
        cat = msg.linkdata1
        subcat = msg.linkdata2
        product_key = msg.linkdata3
        rec_num = len(self._aldb)
        self._aldb[rec_num] = ALDBRecord(rec_num, msg.controlFlags,
                                         msg.group, msg.address,
                                         cat, subcat, product_key)
        if self.devices[msg.address.id] is None:
            self.log.debug('ALDB Data: address %s data1: %02x '
                           'data1: %02x data3: %02x',
                           msg.address.hex, cat, subcat, product_key)

            # Get a device from the ALDB based on cat, subcat and product_key
            device = self.devices.create_device_from_category(
                self, msg.address, cat, subcat, product_key)

            # If a device is returned and that device is of a type tha stores
            # the product data in the ALDB record we can use that as the device
            # type for this record. Otherwise we need to request the device ID.
            if device is not None:
                if device.prod_data_in_aldb or \
                        self.devices.has_override(device.address.id) or \
                        self.devices.has_saved(device.address.id):
                    if self.devices[device.id] is None:
                        self.devices[device.id] = device
                        self.log.info('Device with id %s added to device list '
                                      'from ALDB data.',
                                      device.id)
        # Check again that the device is not alreay added, otherwise queue it
        # up for Get ID request
        if self.devices[msg.address.id] is None:
            unknowndevice = self.devices.create_device_from_category(
                self, msg.address.hex, None, None, None)
            self._aldb_response_queue[msg.address.id] = {
                'device': unknowndevice, 'retries': 0}

        self._get_next_all_link_record()

    def _handle_get_next_all_link_record_nak(self, msg):
        # When the last All-Link record is reached the PLM sends a NAK
        self._aldb.status = ALDBStatus.LOADED
        self.log.debug('All-Link device records found in ALDB: %d',
                       len(self._aldb_response_queue))

        # Remove records for devices found in the ALDB
        # or in previous calls to _handle_get_next_all_link_record_nak
        for addr in self.devices:
            try:
                self._aldb_response_queue.pop(addr)
            except KeyError:
                pass

        staleaddr = []
        for addr in self._aldb_response_queue:
            retries = self._aldb_response_queue[addr]['retries']
            if retries < 5:
                self._aldb_response_queue[addr]['device'].id_request()
                self._aldb_response_queue[addr]['retries'] = retries + 1
            else:
                self.log.warning('Device %s found in the ALDB not responding.',
                                 addr)
                self.log.warning('It is being removed from the device list. '
                                 'If this device')
                self.log.warning('is still active you can add it to the '
                                 'device_override')
                self.log.warning('configuration.')
                staleaddr.append(addr)

                for callback in self._cb_device_not_active:
                    callback(Address(addr))

        for addr in staleaddr:
            self._aldb_response_queue.pop(addr)

        num_devices_not_added = len(self._aldb_response_queue)

        if num_devices_not_added > 0:
            # Schedule _handle_get_next_all_link_record_nak to run again later
            # if some devices did not respond
            delay = num_devices_not_added * 3
            self._loop.call_later(delay,
                                  self._handle_get_next_all_link_record_nak,
                                  None)
        else:
            self.devices.save_device_info()
            while len(self._cb_load_all_link_db_done) > 0:
                callback = self._cb_load_all_link_db_done.pop()
                callback()
            if self._poll_devices:
                self._loop.call_soon(self.poll_devices)
        self.log.debug('Ending _handle_get_next_all_link_record_nak')

    def _handle_nak(self, msg):
        if msg.code == GetFirstAllLinkRecord.code or \
           msg.code == GetNextAllLinkRecord.code:
            return
        self.log.debug('No response or NAK message received for message')
        self.log.debug(msg)
        self.send_msg(msg)

    def _handle_get_plm_info(self, msg):
        from insteonplm.devices import ALDB
        self.log.debug('Starting _handle_get_plm_info')
        from insteonplm.devices.ipdb import IPDB
        ipdb = IPDB()
        self._address = msg.address
        self._cat = msg.category
        self._subcat = msg.subcategory
        self._product_key = msg.firmware
        product = ipdb[[self._cat, self._subcat]]
        self._description = product.description
        self._model = product.model
        self._aldb = ALDB(self._send_msg, self._plm.loop, self._address)

        self.log.debug('Ending _handle_get_plm_info')

    # X10 Device methods
    def x10_all_units_off(self, housecode):
        """Send the X10 All Units Off command."""
        if isinstance(housecode, str):
            housecode = housecode.upper()
        else:
            raise TypeError('Housecode must be a string')
        msg = X10Send.command_msg(housecode, X10_COMMAND_ALL_UNITS_OFF)
        self.send_msg(msg)
        self._x10_command_to_device(housecode, X10_COMMAND_ALL_UNITS_OFF, msg)

    def x10_all_lights_off(self, housecode):
        """Send the X10 All Lights Off command."""
        msg = X10Send.command_msg(housecode, X10_COMMAND_ALL_LIGHTS_OFF)
        self.send_msg(msg)
        self._x10_command_to_device(housecode, X10_COMMAND_ALL_LIGHTS_OFF, msg)

    def x10_all_lights_on(self, housecode):
        """Send the X10 All Lights Off command."""
        msg = X10Send.command_msg(housecode, X10_COMMAND_ALL_LIGHTS_ON)
        self.send_msg(msg)
        self._x10_command_to_device(housecode, X10_COMMAND_ALL_LIGHTS_ON, msg)

    def _handle_x10_send_receive(self, msg):
        housecode_byte, unit_command_byte = rawX10_to_bytes(msg.rawX10)
        housecode = byte_to_housecode(housecode_byte)
        if msg.flag == 0x00:
            unitcode = byte_to_unitcode(unit_command_byte)
            self._x10_address = Address.x10(housecode, unitcode)
            if self._x10_address:
                device = self.devices[self._x10_address.id]
                if device:
                    device.receive_message(msg)
        else:
            self._x10_command_to_device(housecode, unit_command_byte, msg)

    def _x10_command_to_device(self, housecode, command, msg):
        if isinstance(housecode, str):
            housecode = housecode.upper()
        else:
            raise TypeError('Housecode must be a string')
        if x10_command_type(command) == X10CommandType.DIRECT:
            if self._x10_address and self.devices[self._x10_address.id]:
                if self._x10_address.x10_housecode == housecode:
                    self.devices[self._x10_address.id].receive_message(msg)
        else:
            for id in self.devices:
                if self.devices[id].address.is_x10:
                    if (self.devices[id].address.x10_housecode == housecode):
                        self.devices[id].receive_message(msg)
        self._x10_address = None
Пример #15
0
class IM(Device, asyncio.Protocol):
    """Handle the Insteon PLM IP control protocol.

    This class is expected to be wrapped inside a Connection class object
    which will maintain the socket and handle auto-reconnects.

    Parameters:
        connection_lost_callback: (optional, callable) called when connection
        is lost to device as defined by asyncio.Protocol

        loop: (optional, asyncio.loop) asyncio event loop

        workdir: (optional, string) Working directory name to save device
        information

        poll_devices: (optional, bool) indicates if the modem should poll the
        devices for status after startup, default is True

        load_aldb; (optional, bool) indicates if the modem should load the
        All-Link Database on startup, default is True

    """
    def __init__(self,
                 loop=None,
                 connection_lost_callback=None,
                 workdir=None,
                 poll_devices=True,
                 load_aldb=True):
        """Protocol handler that handles all status and changes on PLM."""
        self._loop = loop
        self._connection_lost_callback = connection_lost_callback

        self._buffer = asyncio.Queue(loop=self._loop)
        self._recv_queue = deque([])
        self._send_queue = asyncio.Queue(loop=self._loop)
        self._acknak_queue = asyncio.Queue(loop=self._loop)
        self._next_all_link_rec_nak_retries = 0
        self._aldb_devices = {}
        self._devices = LinkedDevices(loop, workdir)
        self._poll_devices = poll_devices
        self._load_aldb = load_aldb
        self._write_transport_lock = asyncio.Lock(loop=self._loop)
        self._message_callbacks = MessageCallback()
        self._x10_address = None

        # Callback lists
        self._cb_load_all_link_db_done = []
        self._cb_device_not_active = []

        super().__init__(self, '000000', 0x03, None, None, '', '')

        self.transport = None

        self._register_message_handlers()
        self._writer_task = None
        self._restart_writer = False
        self.restart_writing()

    # public properties
    @property
    def devices(self):
        """Return the list of devices linked to the IM."""
        return self._devices

    @property
    def loop(self):
        """Return the asyncio loop."""
        return self._loop

    @property
    def message_callbacks(self):
        """Return the list of message callbacks."""
        return self._message_callbacks

    # asyncio.protocol interface methods
    def connection_made(self, transport):
        """Complete the network connection.

        Called when asyncio.Protocol establishes the network connection.
        """
        raise NotImplementedError

    def data_received(self, data):
        """Receive data from the protocol.

        Called when asyncio.Protocol detects received data from network.
        """
        _LOGGER.debug("Starting: data_received")
        _LOGGER.debug('Received %d bytes from PLM: %s', len(data),
                      binascii.hexlify(data))
        self._buffer.put_nowait(data)
        asyncio.ensure_future(self._peel_messages_from_buffer(),
                              loop=self._loop)

        _LOGGER.debug("Finishing: data_received")

    def connection_lost(self, exc):
        """Reestablish the connection to the transport.

        Called when asyncio.Protocol loses the network connection.
        """
        if exc is None:
            _LOGGER.warning('End of file received from Insteon Modem')
        else:
            _LOGGER.warning('Lost connection to Insteon Modem: %s', exc)

        self.transport = None
        asyncio.ensure_future(self.pause_writing(), loop=self.loop)

        if self._connection_lost_callback:
            self._connection_lost_callback()

    # Methods used to trigger callbacks for specific events
    def add_device_callback(self, callback):
        """Register a callback for when a matching new device is seen."""
        self.devices.add_device_callback(callback)

    def add_all_link_done_callback(self, callback):
        """Register a callback to be invoked when the ALDB is loaded."""
        _LOGGER.debug('Added new callback %s ', callback)
        self._cb_load_all_link_db_done.append(callback)

    def add_device_not_active_callback(self, callback):
        """Register callback to be invoked when a device is not responding."""
        _LOGGER.debug('Added new callback %s ', callback)
        self._cb_device_not_active.append(callback)

    # Public methods
    def poll_devices(self):
        """Request status updates from each device."""
        for addr in self.devices:
            device = self.devices[addr]
            if not device.address.is_x10:
                device.async_refresh_state()

    def send_msg(self, msg, wait_nak=True, wait_timeout=WAIT_TIMEOUT):
        """Place a message on the send queue for sending.

        Message are sent in the order they are placed in the queue.
        """
        msg_info = MessageInfo(msg=msg,
                               wait_nak=wait_nak,
                               wait_timeout=wait_timeout)
        _LOGGER.debug("Queueing msg: %s", msg)
        self._send_queue.put_nowait(msg_info)

    def start_all_linking(self, mode, group):
        """Put the IM into All-Linking mode.

        Puts the IM into All-Linking mode for 4 minutes.

        Parameters:
            mode: 0 | 1 | 3 | 255
                  0 - PLM is responder
                  1 - PLM is controller
                  3 - Device that initiated All-Linking is Controller
                255 = Delete All-Link
            group: All-Link group number (0 - 255)

        """
        msg = StartAllLinking(mode, group)
        self.send_msg(msg)

    def add_x10_device(self, housecode, unitcode, feature='OnOff'):
        """Add an X10 device based on a feature description.

        Current features are:
        - OnOff
        - Dimmable
        - Sensor
        - AllUnitsOff
        - AllLightsOn
        - AllLightsOff
        """
        device = insteonplm.devices.create_x10(self, housecode, unitcode,
                                               feature)
        if device:
            self.devices[device.address.id] = device
        return device

    def monitor_mode(self):
        """Put the Insteon Modem in monitor mode."""
        msg = SetIMConfiguration(0x40)
        self.send_msg(msg)

    def device_not_active(self, addr):
        """Handle inactive devices."""
        self.aldb_device_handled(addr)
        for callback in self._cb_device_not_active:
            callback(addr)

    def aldb_device_handled(self, addr):
        """Remove device from ALDB device list."""
        if isinstance(addr, Address):
            remove_addr = addr.id
        else:
            remove_addr = addr
        try:
            self._aldb_devices.pop(remove_addr)
            _LOGGER.debug('Removed ALDB device %s', remove_addr)
        except KeyError:
            _LOGGER.debug('Device %s not in ALDB device list', remove_addr)
        _LOGGER.debug('ALDB device count: %d', len(self._aldb_devices))

    def manage_aldb_record(self, control_code, control_flags, group, address,
                           data1, data2, data3):
        """Update an IM All-Link record.

        Control Code values:
        - 0x00 Find First Starting at the top of the ALDB, search for the
        first ALL-Link Record matching the <ALL-Link Group> and <ID> in bytes
        5 – 8. The search ignores byte 4, <ALL-Link Record Flags>. You will
        receive an ACK at the end of the returned message if such an ALL-Link
        Record exists, or else a NAK if it doesn’t.  If the record exists, the
        IM will return it in an ALL-Link Record Response (0x51) message.

        - 0x01 Find Next Search for the next ALL-Link Record following the one
        found using <Control Code> 0x00 above.  This allows you to find both
        Controller and Responder records for a given <ALL-Link Group> and
        <ID>. Be sure to use the same <ALL-Link Group> and <ID> (bytes 5 – 8)
        as you used for <Control Code> 0x00. You will receive an ACK at the
        end of the returned message if another matching ALL-Link Record
        exists, or else a NAK if it doesn’t.  If the record exists, the IM
        will return it in an ALL-Link Record Response (0x51) message.

        - 0x20 Modify First Found or Add Modify an existing or else add a new
        ALL-Link Record for either a Controller or Responder. Starting at the
        top of the ALDB, search for the first ALL-Link Record matching the
        <ALL-Link Group> and <ID> in bytes 5 – 8.  The search ignores byte 4,
        <ALL-Link Record Flags>. If such an ALL-Link Record exists, overwrite
        it with the data in bytes 4 – 11; otherwise, create a new ALL-Link
        Record using bytes 4 – 11. Note that the IM will copy
        <ALL-Link Record Flags> you supplied in byte 4 below directly into the
        <ALL-Link Record Flags> byte of the ALL-Link Record in an ALDB-L
        (linear) database.  Use caution, because you can damage an ALDB-L if
        you misuse this Command.  For instance, if you zero the
        <ALL-Link Record Flags> byte in the first ALL-Link Record, the IM’s
        ALDB-L database will then appear empty.

        - 0x40 Modify First Controller Found or Add Modify an existing or else
        add a new Controller (master) ALL-Link Record. Starting at the top of
        the ALDB, search for the first ALL-Link Controller Record matching the
        <ALL-Link Group> and <ID> in bytes 5 – 8.  An ALL-Link Controller
        Record has bit 6 of its <ALL-Link Record Flags> byte set to 1. If such
        a Controller ALL-Link Record exists, overwrite it with the data in
        bytes 5 – 11; otherwise, create a new ALL-Link Record using bytes
        5 – 11.  In either case, the IM will set bit 6 of the
        <ALL-Link Record Flags> byte in the ALL-Link Record to 1 to indicate
        that the record is for a Controller.

        - 0x41 Modify First Responder Found or Add Modify an existing or else
        add a new Responder (slave) ALLLink Record. Starting at the top of the
        ALDB, search for the first ALL-Link Responder Record matching the
        <ALL-Link Group> and <ID> in bytes 5 – 8.  An ALL-Link Responder
        Record has bit 6 of its <ALL-Link Record Flags> byte cleared to 0. If
        such a Responder ALL-Link Record exists, overwrite it with the data in
        bytes 5 – 11; otherwise, create a new ALL-Link Record using bytes
        5 – 11.  In either case, The IM will clear bit 6 of the
        <ALL-Link Record Flags> byte in the ALL-Link Record to 0 to indicate
        that the record is for a Responder.

        - 0x80 Delete First Found Delete an ALL-Link Record. Starting at the
        top of the ALDB, search for the first ALL-Link Record matching the
        <ALL-Link Group> and <ID> in bytes 5 – 8.  The search ignores byte 4,
        <ALL-Link Record Flags>. You will receive an ACK at the end of the
        returned message if such an ALL-Link Record existed and was deleted,
        or else a NAK no such record exists.
        """
        msg = ManageAllLinkRecord(control_code, control_flags, group, address,
                                  data1, data2, data3)
        self.send_msg(msg)

    async def pause_writing(self):
        """Pause writing."""
        self._restart_writer = False
        if self._writer_task:
            self._send_queue.put_nowait(None)
        await asyncio.sleep(.1)

    # pylint: disable=unused-argument
    def restart_writing(self, task=None):
        """Resume writing."""
        if self._restart_writer and not self._write_transport_lock.locked():
            self._writer_task = asyncio.ensure_future(
                self._get_message_from_send_queue(), loop=self._loop)
            self._writer_task.add_done_callback(self.restart_writing)

    async def close(self):
        """Close all writers for all devices for a clean shutdown."""
        await self.pause_writing()
        await asyncio.sleep(0, loop=self._loop)

    def trigger_group_on(self, group):
        """Trigger an All-Link Group on."""
        from .messages.standardSend import StandardSend
        from .constants import COMMAND_LIGHT_ON_0X11_NONE
        target = Address(bytearray([0x00, 0x00, group]))
        flags = 0xcf
        msg = StandardSend(target,
                           COMMAND_LIGHT_ON_0X11_NONE,
                           cmd2=0xff,
                           flags=flags)
        self.send_msg(msg)
        dev_list = self._find_scene(group)
        _LOGGER.debug('Scene %d turned on', group)
        for addr in dev_list:
            device = self._devices[addr.id]
            flags = 0x4f
            msg = StandardSend(device.address,
                               COMMAND_LIGHT_ON_0X11_NONE,
                               cmd2=group,
                               flags=flags)
            self.send_msg(msg)
            if hasattr(device, 'async_refresh_state'):
                _LOGGER.debug('Checking status of device %s', addr.human)
                device.async_refresh_state()

    def trigger_group_off(self, group):
        """Trigger an All-Link Group off."""
        from .messages.standardSend import StandardSend
        from .constants import COMMAND_LIGHT_OFF_0X13_0X00
        target = Address(bytearray([0x00, 0x00, group]))
        flags = 0xcf
        msg = StandardSend(target, COMMAND_LIGHT_OFF_0X13_0X00, flags=flags)
        self.send_msg(msg)
        dev_list = self._find_scene(group)
        _LOGGER.debug('Scene %d turned off', group)
        for addr in dev_list:
            device = self._devices[addr.id]
            flags = 0x4f
            msg = StandardSend(device.address,
                               COMMAND_LIGHT_OFF_0X13_0X00,
                               cmd2=group,
                               flags=flags)
            self.send_msg(msg)
            if hasattr(device, 'async_refresh_state'):
                _LOGGER.debug('Checking status of device %s', addr.human)
                device.async_refresh_state()

    def _find_scene(self, group):
        """Identify all devices that are part of a scene."""
        device_list = []
        for rec_num in self._aldb:
            rec = self._aldb[rec_num]
            _LOGGER.debug('Checking record for scene: %s', rec)
            if (rec.control_flags.is_controller and rec.group == group):
                if rec.address not in device_list:
                    device_list.append(rec.address)
        for addr in self._devices:
            device = self._devices[addr]
            aldb = device.aldb
            for mem_addr in aldb:
                rec = aldb[mem_addr]
                _LOGGER.debug('Checking record for scene: %s', rec)
                if (rec.control_flags.is_in_use and rec.group == group
                        and rec.address == self._address):
                    if rec.address not in device_list:
                        device_list.append(device.address)
        return device_list

    async def _setup_devices(self):
        await self.devices.load_saved_device_info()
        _LOGGER.debug('Found %d saved devices',
                      len(self.devices.saved_devices))
        self._get_plm_info()
        self.devices.add_known_devices(self)

        # Comment out the following lines for testing
        if self._load_aldb:
            self._load_all_link_database()
        else:
            self._complete_setup()

        _LOGGER.debug('Ending _setup_devices in IM')

    # pylint: disable=broad-except
    async def _get_message_from_send_queue(self):
        _LOGGER.debug('Starting Insteon Modem write message from send queue')
        _LOGGER.debug('Aquiring write lock')
        await self._write_transport_lock.acquire()
        while self._restart_writer:
            # wait for an item from the queue
            msg_info = await self._send_queue.get()
            if msg_info is None:
                self._restart_writer = False
                return
            message_sent = False
            try:
                while not message_sent:
                    message_sent = await self._write_message(msg_info)
                await asyncio.sleep(msg_info.wait_timeout, loop=self._loop)
            except asyncio.CancelledError:
                _LOGGER.info('Stopping Insteon Modem writer due to '
                             'CancelledError')
                self._restart_writer = False
            except GeneratorExit:
                _LOGGER.error('Stopping Insteon Modem writer due to '
                              'GeneratorExit')
                self._restart_writer = False
            except Exception as e:
                _LOGGER.error('Restarting Insteon Modem writer due to %s',
                              str(e))
                _LOGGER.error('MSG: %s', str(msg_info.msg))
                self._restart_writer = True
        if self._write_transport_lock.locked():
            self._write_transport_lock.release()
        _LOGGER.debug('Ending Insteon Modem write message from send queue')

    def _get_plm_info(self):
        """Request PLM Info."""
        _LOGGER.info('Requesting Insteon Modem Info')
        msg = GetImInfo()
        self.send_msg(msg, wait_nak=True, wait_timeout=.5)

    async def _write_message(self, msg_info: MessageInfo):
        _LOGGER.debug('TX: %s', msg_info.msg)
        is_sent = False
        if not self.transport.is_closing():
            self.transport.write(msg_info.msg.bytes)
            if msg_info.wait_nak:
                _LOGGER.debug('Waiting for ACK or NAK message')
                is_sent = await self._wait_ack_nak(msg_info.msg)
            else:
                is_sent = True
        else:
            _LOGGER.debug("Transport is not open, waiting 5 seconds")
            is_sent = False
            await asyncio.sleep(5, loop=self._loop)
        return is_sent

    async def _wait_ack_nak(self, msg):
        is_sent = False
        is_ack_nak = False
        try:
            with async_timeout.timeout(ACKNAK_TIMEOUT):
                while not is_ack_nak:
                    acknak = await self._acknak_queue.get()
                    is_ack_nak = self._msg_is_ack_nak(msg, acknak)
                    is_sent = self._msg_is_sent(acknak)
        except asyncio.TimeoutError:
            _LOGGER.debug('No ACK or NAK message received.')
            is_sent = False
        return is_sent

    # pylint: disable=no-self-use
    def _msg_is_ack_nak(self, msg, acknak):
        if not hasattr(acknak, 'isack'):
            return False
        if msg.matches_pattern(acknak):
            _LOGGER.debug('ACK or NAK received')
            return True
        return False

    # pylint: disable=no-self-use
    def _msg_is_sent(self, acknak):
        # All Link record NAK is a valid last record response
        # However, we want to retry 3 times to make sure
        # it is a valid last record response and not a true NAK
        if ((acknak.code == GetFirstAllLinkRecord.code
             or acknak.code == GetNextAllLinkRecord.code) and acknak.isnak):
            return True

        return acknak.isack

    def _load_all_link_database(self):
        """Load the ALL-Link Database into object."""
        _LOGGER.debug("Starting: _load_all_link_database")
        self.devices.state = 'loading'
        self._get_first_all_link_record()
        _LOGGER.debug("Ending: _load_all_link_database")

    def _get_first_all_link_record(self):
        """Request first ALL-Link record."""
        _LOGGER.debug("Starting: _get_first_all_link_record")
        _LOGGER.info('Requesting ALL-Link Records')
        if self.aldb.status == ALDBStatus.LOADED:
            self._next_all_link_rec_nak_retries = 3
            self._handle_get_next_all_link_record_nak(None)
            return
        self.aldb.clear()
        self._next_all_link_rec_nak_retries = 0
        msg = GetFirstAllLinkRecord()
        self.send_msg(msg, wait_nak=True, wait_timeout=.5)
        _LOGGER.debug("Ending: _get_first_all_link_record")

    def _get_next_all_link_record(self):
        """Request next ALL-Link record."""
        _LOGGER.debug("Starting: _get_next_all_link_record")
        _LOGGER.debug("Requesting Next All-Link Record")
        msg = GetNextAllLinkRecord()
        self.send_msg(msg, wait_nak=True, wait_timeout=.5)
        _LOGGER.debug("Ending: _get_next_all_link_record")

    def _new_device_added(self, device):
        self.aldb_device_handled(device.address.id)
        if self._poll_devices:
            device.async_refresh_state()

    # Inbound message handlers sepcific to the IM
    def _register_message_handlers(self):
        template_all_link_response = AllLinkRecordResponse(
            None, None, None, None, None, None)
        template_get_im_info = GetImInfo()
        template_next_all_link_rec = GetNextAllLinkRecord(acknak=MESSAGE_NAK)
        template_x10_send = X10Send(None, None, MESSAGE_ACK)
        template_x10_received = X10Received(None, None)

        # self._message_callbacks.add(
        #    template_assign_all_link,
        #    self._handle_assign_to_all_link_group)

        self._message_callbacks.add(template_all_link_response,
                                    self._handle_all_link_record_response)

        self._message_callbacks.add(template_get_im_info,
                                    self._handle_get_plm_info)

        self._message_callbacks.add(template_next_all_link_rec,
                                    self._handle_get_next_all_link_record_nak)

        self._message_callbacks.add(template_x10_send,
                                    self._handle_x10_send_receive)

        self._message_callbacks.add(template_x10_received,
                                    self._handle_x10_send_receive)

    async def _peel_messages_from_buffer(self):
        lastlooplen = 0
        worktodo = True
        buffer = bytearray()
        while worktodo:
            buffer.extend(self._unpack_buffer())
            if len(buffer) < 2:
                worktodo = False
                break
            _LOGGER.debug('Total buffer: %s', binascii.hexlify(buffer))
            msg, buffer = insteonplm.messages.create(buffer)

            if msg is not None:
                # _LOGGER.debug('Msg buffer: %s', msg.hex)
                self._recv_queue.appendleft(msg)

            # _LOGGER.debug('Post buffer: %s', binascii.hexlify(buffer))
            if len(buffer) < 2:
                _LOGGER.debug('Buffer too short to have a message')
                worktodo = False
                break

            if len(buffer) == lastlooplen:
                _LOGGER.debug("Buffer size did not change wait for more data")
                worktodo = False
                break

            lastlooplen = len(buffer)
        if buffer:
            buffer.extend(self._unpack_buffer())
            self._buffer.put_nowait(buffer)

        _LOGGER.debug('Messages in queue: %d', len(self._recv_queue))
        worktodo = True
        while worktodo:
            try:
                self._process_recv_queue()
            except IndexError:
                _LOGGER.debug('Last item in self._recv_queue reached.')
                worktodo = False

    def _process_recv_queue(self):
        msg = self._recv_queue.pop()
        _LOGGER.debug('RX: %s', msg)
        callbacks = self._message_callbacks.get_callbacks_from_message(msg)
        if hasattr(msg, 'isack') or hasattr(msg, 'isnak'):
            self._acknak_queue.put_nowait(msg)
        if hasattr(msg, 'address'):
            device = self.devices[msg.address.hex]
            if device:
                device.receive_message(msg)
            else:
                try:
                    device = self._aldb_devices[msg.address.id]
                    if device:
                        device.receive_message(msg)
                except KeyError:
                    pass
        for callback in callbacks:
            self._loop.call_soon(callback, msg)

    def _unpack_buffer(self):
        buffer = bytearray()
        while not self._buffer.empty():
            buffer.extend(self._buffer.get_nowait())
        return buffer

    def _refresh_aldb(self):
        self.aldb.clear()
        self._load_all_link_database()

    def _handle_all_link_record_response(self, msg):
        _LOGGER.debug('Found all link record for device %s', msg.address.hex)
        cat = msg.linkdata1
        subcat = msg.linkdata2
        product_key = msg.linkdata3
        rec_num = len(self._aldb)
        self._aldb[rec_num] = ALDBRecord(rec_num, msg.controlFlags, msg.group,
                                         msg.address, cat, subcat, product_key)
        if self.devices[msg.address.id] is None:
            _LOGGER.debug(
                'ALDB Data: address %s data1: %02x '
                'data1: %02x data3: %02x', msg.address.hex, cat, subcat,
                product_key)

            # Get a device from the ALDB based on cat, subcat and product_key
            device = self.devices.create_device_from_category(
                self, msg.address, cat, subcat, product_key)

            # If a device is returned and that device is of a type tha stores
            # the product data in the ALDB record we can use that as the device
            # type for this record. Otherwise we need to request the device ID.
            if device is not None:
                if device.prod_data_in_aldb or \
                        self.devices.has_override(device.address.id) or \
                        self.devices.has_saved(device.address.id):
                    if self.devices[device.id] is None:
                        self.devices[device.id] = device
                        _LOGGER.debug(
                            'Device with id %s added to device list '
                            'from ALDB data.', device.id)

        # Check again that the device is not already added, otherwise queue it
        # up for Get ID request
        if not self.devices[msg.address.id]:
            _LOGGER.debug('Found new device %s', msg.address.id)
            unknowndevice = self.devices.create_device_from_category(
                self, msg.address.hex, None, None, None)
            self._aldb_devices[msg.address.id] = unknowndevice

        self._next_all_link_rec_nak_retries = 0
        self._get_next_all_link_record()

    def _handle_get_next_all_link_record_nak(self, msg):
        # When the last All-Link record is reached the PLM sends a NAK
        if self._next_all_link_rec_nak_retries < 3:
            self._next_all_link_rec_nak_retries += 1
            self._get_next_all_link_record()
            return
        self._aldb.status = ALDBStatus.LOADED
        _LOGGER.debug('All-Link device records found in ALDB: %d',
                      len(self._aldb_devices))

        while self._cb_load_all_link_db_done:
            callback = self._cb_load_all_link_db_done.pop()
            callback()

        self._get_device_info()

    def _get_device_info(self):
        _LOGGER.debug('Starting _get_device_info')
        # Remove saved records for devices found in the ALDB
        for addr in self.devices:
            self.aldb_device_handled(addr)

        self._complete_setup()

        self.devices.add_device_callback(self._new_device_added)

        for addr in self._aldb_devices:
            _LOGGER.debug('Getting device info for %s', Address(addr).human)
            self._aldb_devices[addr].id_request()

        _LOGGER.debug('Ending _get_device_info')

    def _complete_setup(self):
        self.devices.save_device_info()
        if self._poll_devices:
            self._loop.call_soon(self.poll_devices)

    def _handle_nak(self, msg_info):
        if msg_info.msg.code == GetFirstAllLinkRecord.code or \
           msg_info.msg.code == GetNextAllLinkRecord.code:
            return
        _LOGGER.debug('No response or NAK message received')
        self.send_msg(msg_info.msg, msg_info.wait_nak, msg_info.wait_timeout)

    def _handle_get_plm_info(self, msg):
        from insteonplm.devices import ALDB
        _LOGGER.debug('Starting _handle_get_plm_info')
        from insteonplm.devices.ipdb import IPDB
        ipdb = IPDB()
        self._address = msg.address
        self._cat = msg.category
        self._subcat = msg.subcategory
        self._product_key = msg.firmware
        product = ipdb[[self._cat, self._subcat]]
        self._description = product.description
        self._model = product.model
        self._aldb = ALDB(self._send_msg, self._plm.loop, self._address)

        _LOGGER.debug('Ending _handle_get_plm_info')

    # X10 Device methods
    def x10_all_units_off(self, housecode):
        """Send the X10 All Units Off command."""
        if isinstance(housecode, str):
            housecode = housecode.upper()
        else:
            raise TypeError('Housecode must be a string')
        msg = X10Send.command_msg(housecode, X10_COMMAND_ALL_UNITS_OFF)
        self.send_msg(msg)
        self._x10_command_to_device(housecode, X10_COMMAND_ALL_UNITS_OFF, msg)

    def x10_all_lights_off(self, housecode):
        """Send the X10 All Lights Off command."""
        msg = X10Send.command_msg(housecode, X10_COMMAND_ALL_LIGHTS_OFF)
        self.send_msg(msg)
        self._x10_command_to_device(housecode, X10_COMMAND_ALL_LIGHTS_OFF, msg)

    def x10_all_lights_on(self, housecode):
        """Send the X10 All Lights Off command."""
        msg = X10Send.command_msg(housecode, X10_COMMAND_ALL_LIGHTS_ON)
        self.send_msg(msg)
        self._x10_command_to_device(housecode, X10_COMMAND_ALL_LIGHTS_ON, msg)

    def _handle_x10_send_receive(self, msg):
        housecode_byte, unit_command_byte = rawX10_to_bytes(msg.rawX10)
        housecode = byte_to_housecode(housecode_byte)
        if msg.flag == 0x00:
            unitcode = byte_to_unitcode(unit_command_byte)
            self._x10_address = Address.x10(housecode, unitcode)
            if self._x10_address:
                device = self.devices[self._x10_address.id]
                if device:
                    device.receive_message(msg)
        else:
            self._x10_command_to_device(housecode, unit_command_byte, msg)

    def _x10_command_to_device(self, housecode, command, msg):
        if isinstance(housecode, str):
            housecode = housecode.upper()
        else:
            raise TypeError('Housecode must be a string')
        if x10_command_type(command) == X10CommandType.DIRECT:
            if self._x10_address and self.devices[self._x10_address.id]:
                if self._x10_address.x10_housecode == housecode:
                    self.devices[self._x10_address.id].receive_message(msg)
        else:
            for addr in self.devices:
                if self.devices[addr].address.is_x10:
                    if self.devices[addr].address.x10_housecode == housecode:
                        self.devices[addr].receive_message(msg)
        self._x10_address = None
Пример #16
0
class X10Device(object):
    """X10 device class."""
    def __init__(self, plm, housecode, unitcode):
        """Initialize the X10Device class."""
        self._address = Address.x10(housecode, unitcode)
        self._plm = plm
        self._description = "Generic X10 device"
        self._model = ''
        self._aldb = ALDB(None, None, self._address, version=ALDBVersion.Null)
        self._message_callbacks = MessageCallback()
        self._stateList = StateList()
        self._send_msg_lock = asyncio.Lock(loop=self._plm.loop)
        self.log = logging.getLogger()

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

    @property
    def description(self):
        """Return the INSTEON device description."""
        return self._description

    @property
    def model(self):
        """Return the INSTEON device model number."""
        return self._model

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

    @property
    def states(self):
        """Return the device states/groups."""
        return self._stateList

    @property
    def aldb(self):
        """Return the device All-Link Database."""
        return self._aldb

    # Send / Receive message processing
    def receive_message(self, msg):
        self.log.debug('Starting X10Device.receive_message')
        if hasattr(msg, 'isack') and msg.isack:
            self.log.debug('Got Message ACK')
            if self._send_msg_lock.locked():
                self._send_msg_lock.release()
        callbacks = self._message_callbacks.get_callbacks_from_message(msg)
        self.log.debug('Found %d callbacks for msg %s', len(callbacks), msg)
        for callback in callbacks:
            self.log.debug('Scheduling msg callback: %s', callback)
            self._plm.loop.call_soon(callback, msg)
        self._last_communication_received = datetime.datetime.now()
        self.log.debug('Ending Device.receive_message')

    def _send_msg(self, msg, wait_ack=True):
        self.log.debug('Starting Device._send_msg')
        write_message_coroutine = self._process_send_queue(msg, wait_ack)
        asyncio.ensure_future(write_message_coroutine, loop=self._plm.loop)
        self.log.debug('Ending Device._send_msg')

    @asyncio.coroutine
    def _process_send_queue(self, msg, wait_ack):
        self.log.debug('Starting Device._process_send_queue')
        yield from self._send_msg_lock
        if self._send_msg_lock.locked():
            self.log.debug("Lock is locked from yeild from")

        self._plm.send_msg(msg, wait_timeout=2)
        if not wait_ack:
            self._send_msg_lock.release()
        self.log.debug('Ending Device._process_send_queue')
Пример #17
0
class Device(object):
    """INSTEON Device Class."""
    def __init__(self,
                 plm,
                 address,
                 cat,
                 subcat,
                 product_key=0x00,
                 description='',
                 model=''):
        """Initialize the Device class."""
        self.log = logging.getLogger(__name__)

        self._plm = plm

        self._address = Address(address)
        self._cat = cat
        self._subcat = subcat
        if self._subcat is None:
            self._subcat = 0x00
        self._product_key = product_key
        if self._product_key is None:
            self._product_key = 0x00
        self._description = description
        self._model = model

        self._last_communication_received = datetime.datetime(1, 1, 1, 1, 1, 1)
        self._recent_messages = asyncio.Queue(loop=self._plm.loop)
        self._product_data_in_aldb = False
        self._stateList = StateList()
        self._send_msg_lock = asyncio.Lock(loop=self._plm.loop)
        self._sent_msg_wait_for_directACK = {}
        self._directACK_received_queue = asyncio.Queue(loop=self._plm.loop)
        self._message_callbacks = MessageCallback()
        self._aldb = ALDB(self._send_msg, self._plm.loop, self._address)

        self._register_messages()

    # Public properties
    @property
    def address(self):
        """Return the INSTEON device address."""
        return self._address

    @property
    def cat(self):
        """Return the INSTEON device category."""
        return self._cat

    @property
    def subcat(self):
        """Return the INSTEON device subcategory."""
        return self._subcat

    @property
    def product_key(self):
        """Return the INSTEON product key."""
        return self._product_key

    @property
    def description(self):
        """Return the INSTEON device description."""
        return self._description

    @property
    def model(self):
        """Return the INSTEON device model number."""
        return self._model

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

    @property
    def states(self):
        """Return the device states/groups."""
        return self._stateList

    @property
    def prod_data_in_aldb(self):
        """Return if the PLM use the ALDB data to setup the device.

        True if Product data (cat, subcat) is stored in the PLM ALDB.
        False if product data must be aquired via a Device ID message or from a
        Product Data Request command.

        The method of linking determines if product data in the ALDB,
        therefore False is the default. The common reason to store product data
        in the ALDB is for one way devices or battery opperated devices where
        the ability to send a command request is limited.
        """
        return self._product_data_in_aldb

    @property
    def aldb(self):
        return self._aldb

    # Public Methods
    def async_refresh_state(self):
        """Request each state to provide status update."""
        for state in self._stateList:
            self._stateList[state].async_refresh_state()

    def id_request(self):
        """Request a device ID from a device."""
        msg = StandardSend(self.address, COMMAND_ID_REQUEST_0X10_0X00)
        self._plm.send_msg(msg)

    def product_data_request(self):
        """Request product data from a device.

        Not supported by all devices.
        Required after 01-Feb-2007.
        """
        msg = StandardSend(self._address,
                           COMMAND_PRODUCT_DATA_REQUEST_0X03_0X00)
        self._send_msg(msg)

    def assign_to_all_link_group(self, group=0x01):
        """Assign a device to an All-Link Group.

        The default is group 0x01.
        """
        msg = StandardSend(self._address,
                           COMMAND_ASSIGN_TO_ALL_LINK_GROUP_0X01_NONE,
                           cmd2=group)
        self._send_msg(msg)

    def delete_from_all_link_group(self, group):
        """Delete a device to an All-Link Group."""
        msg = StandardSend(self._address,
                           COMMAND_DELETE_FROM_ALL_LINK_GROUP_0X02_NONE,
                           cmd2=group)
        self._send_msg(msg)

    def fx_username(self):
        """Get FX Username.

        Only required for devices that support FX Commands.
        FX Addressee responds with an ED 0x0301 FX Username Response message
        """
        msg = StandardSend(self._address, COMMAND_FX_USERNAME_0X03_0X01)
        self._send_msg(msg)

    def device_text_string_request(self):
        """Get FX Username.

        Only required for devices that support FX Commands.
        FX Addressee responds with an ED 0x0301 FX Username Response message.
        """
        msg = StandardSend(self._address, COMMAND_FX_USERNAME_0X03_0X01)
        self._send_msg(msg)

    def enter_linking_mode(self, group=0x01):
        """Tell a device to enter All-Linking Mode.

        Same as holding down the Set button for 10 sec.
        Default group is 0x01.

        Not supported by i1 devices.
        """
        msg = StandardSend(self._address,
                           COMMAND_ENTER_LINKING_MODE_0X09_NONE,
                           cmd2=group)
        self._send_msg(msg)

    def enter_unlinking_mode(self, group):
        """Unlink a device from an All-Link group.

        Not supported by i1 devices.
        """
        msg = StandardSend(self._address,
                           COMMAND_ENTER_UNLINKING_MODE_0X0A_NONE,
                           cmd2=group)
        self._send_msg(msg)

    def get_engine_version(self):
        """Get the device engine version."""
        msg = StandardSend(self._address,
                           COMMAND_GET_INSTEON_ENGINE_VERSION_0X0D_0X00)
        self._send_msg(msg)

    def ping(self):
        """Ping a device."""
        msg = StandardSend(self._address, COMMAND_PING_0X0F_0X00)
        self._send_msg(msg)

    def read_aldb(self, mem_addr=0x0000, num_recs=0):
        """Read the device All-Link Database."""
        if self._aldb.version == ALDBVersion.Null:
            self.log.info('Device does not contain an ALDB')
        else:
            self.log.info('Reading ALDB for device %s', self._address)
            asyncio.ensure_future(self._aldb.load(mem_addr, num_recs),
                                  loop=self._plm.loop)
            self._aldb.add_loaded_callback(self._aldb_loaded_callback)

    def write_aldb(self,
                   mem_addr: int,
                   mode: str,
                   group: int,
                   target,
                   data1=0x00,
                   data2=0x00,
                   data3=0x00):
        """Write to the device All-Link Database.

        Paramters:
            Required:
            mode:   r - device is a responder of target
                    c - device is a controller of target
            group:  Link group
            target: Address of the other device

            Optional:
            data1:  Device dependant
            data2:  Device dependant
            data3:  Device dependant
        """
        if isinstance(mode, str) and mode.lower() in ['c', 'r']:
            pass
        else:
            self.log.info('mode: %s', mode)
            raise ValueError("Mode must be 'c' or 'r'")
        if isinstance(group, int):
            pass
        else:
            raise ValueError("Group must be an integer")

        target_addr = Address(target)

        self.log.info('calling aldb write_record')
        self._aldb.write_record(mem_addr, mode, group, target_addr, data1,
                                data2, data3)
        self._aldb.add_loaded_callback(self._aldb_loaded_callback)

    def del_aldb(self, mem_addr: int):
        """Delete an All-Link Database record."""
        self._aldb.del_record(mem_addr)
        self._aldb.add_loaded_callback(self._aldb_loaded_callback)

    def _handle_aldb_record_received(self, msg):
        self._aldb.record_received(msg)

    def _handle_pre_nak(self, msg):
        self.async_refresh_state()

    def _register_messages(self):
        ext_msg_aldb_record = ExtendedReceive.template(
            address=self._address,
            commandtuple=COMMAND_EXTENDED_READ_WRITE_ALDB_0X2F_0X00,
            userdata=Userdata.template({'d2': 1}),
            flags=MessageFlags.template(
                messageType=MESSAGE_TYPE_DIRECT_MESSAGE, extended=1))
        std_msg_pre_nak = StandardReceive.template(flags=MessageFlags.template(
            messageType=MESSAGE_FLAG_DIRECT_MESSAGE_NAK_0XA0),
                                                   cmd2=0xfc)
        ext_msg_pre_nak = ExtendedReceive.template(flags=MessageFlags.template(
            messageType=MESSAGE_FLAG_DIRECT_MESSAGE_NAK_0XA0),
                                                   cmd2=0xfc)
        self._message_callbacks.add(ext_msg_aldb_record,
                                    self._handle_aldb_record_received)
        self._message_callbacks.add(std_msg_pre_nak, self._handle_pre_nak)
        self._message_callbacks.add(ext_msg_pre_nak, self._handle_pre_nak)

    # Send / Receive message processing
    def receive_message(self, msg):
        self.log.debug('Starting Device.receive_message')
        if hasattr(msg, 'isack') and msg.isack:
            self.log.debug('Got Message ACK')
            if self._sent_msg_wait_for_directACK.get('callback') is not None:
                self.log.debug('Look for direct ACK')
                coro = self._wait_for_direct_ACK()
                asyncio.ensure_future(coro, loop=self._plm.loop)
            else:
                self.log.debug('DA queue: %s',
                               self._sent_msg_wait_for_directACK)
                self.log.debug('Message ACK with no callback')
        if (hasattr(msg, 'flags') and hasattr(msg.flags, 'isDirectACK')
                and msg.flags.isDirectACK):
            self.log.debug('Got Direct ACK message')
            if self._send_msg_lock.locked():
                self._directACK_received_queue.put_nowait(msg)
            else:
                self.log.debug('But Direct ACK not expected')
        if not self._is_duplicate(msg):
            callbacks = self._message_callbacks.get_callbacks_from_message(msg)
            for callback in callbacks:
                self.log.debug('Scheduling msg callback: %s', callback)
                self._plm.loop.call_soon(callback, msg)
        else:
            self.log.debug('msg is duplicate')
            self.log.debug(msg)
        self._last_communication_received = datetime.datetime.now()
        self.log.debug('Ending Device.receive_message')

    def _is_duplicate(self, msg):
        if msg.code not in [
                MESSAGE_STANDARD_MESSAGE_RECEIVED_0X50,
                MESSAGE_EXTENDED_MESSAGE_RECEIVED_0X51
        ]:
            return False

        recent_messages = []
        while not self._recent_messages.empty():
            recent_message = self._recent_messages.get_nowait()
            if recent_message:
                msg_received = recent_message.get("received")
                if msg_received >= (datetime.datetime.now() -
                                    datetime.timedelta(0, 0, 500000)):
                    recent_messages.append(recent_message)
        if not recent_messages:
            self._save_recent_message(msg)
            return False

        for recent_message in recent_messages:
            prev_msg = recent_message.get('msg')
            self._recent_messages.put_nowait(recent_message)
            prev_cmd1 = prev_msg.cmd1
            if prev_msg.flags.isAllLinkBroadcast:
                prev_group = prev_msg.target.bytes[2]
            elif prev_msg.flags.isAllLinkCleanup:
                prev_group = prev_msg.cmd2

            if msg.flags.isAllLinkCleanup or msg.flags.isAllLinkBroadcast:
                cmd1 = msg.cmd1
                if msg.flags.isAllLinkBroadcast:
                    group = msg.target.bytes[2]
                else:
                    group = msg.cmd2
                if prev_cmd1 == cmd1 and prev_group == group:
                    return True
                else:
                    self._save_recent_message(msg)
            else:
                self._save_recent_message(msg)
                return False

    def _save_recent_message(self, msg):
        recent_message = {"msg": msg, "received": datetime.datetime.now()}
        self._recent_messages.put_nowait(recent_message)

    def _send_msg(self, msg, callback=None, on_timeout=False):
        self.log.debug('Starting Device._send_msg')
        write_message_coroutine = self._process_send_queue(
            msg, callback, on_timeout)
        asyncio.ensure_future(write_message_coroutine, loop=self._plm.loop)
        self.log.debug('Ending Device._send_msg')

    @asyncio.coroutine
    def _process_send_queue(self, msg, callback=None, on_timeout=False):
        self.log.debug('Starting Device._process_send_queue')
        yield from self._send_msg_lock
        if self._send_msg_lock.locked():
            self.log.debug("Lock is locked from yeild from")
        if callback:
            self._sent_msg_wait_for_directACK = {
                'msg': msg,
                'callback': callback,
                'on_timeout': on_timeout
            }
        self._plm.send_msg(msg)
        self.log.debug('Ending Device._process_send_queue')

    @asyncio.coroutine
    def _wait_for_direct_ACK(self):
        self.log.debug('Starting Device._wait_for_direct_ACK')
        msg = None
        while True:
            # wait for an item from the queue
            try:
                with async_timeout.timeout(DIRECT_ACK_WAIT_TIMEOUT):
                    msg = yield from self._directACK_received_queue.get()
                    break
            except asyncio.TimeoutError:
                self.log.debug('No direct ACK messages received.')
                break
        self.log.debug('Releasing lock')
        self._send_msg_lock.release()
        if msg or self._sent_msg_wait_for_directACK.get('on_timeout'):
            callback = self._sent_msg_wait_for_directACK.get('callback', None)
            if callback is not None:
                callback(msg)
        self._sent_msg_wait_for_directACK = {}
        self.log.debug('Ending Device._wait_for_direct_ACK')

    def _aldb_loaded_callback(self):
        self._plm.devices.save_device_info()