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()
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
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()
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
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
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
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
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
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')
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()