Skip to content

Commit baf3408

Browse files
committedDec 2, 2019
Adds ALL-Link Group updates from external controllers
* Resolves nugget#114 * Increases ALDB reading timeout to 120s * Handles reception of ALL-Link group broadcasts and cleanups correctly updating each responding device * Requires device ALDB scan to get devices updated correctly; will work OK without the scan but individual devices should be hidden on the UI because they will not follow the scene correctly. * Requires an update to the Home Assistant Insteon platform
1 parent b514906 commit baf3408

File tree

8 files changed

+227
-31
lines changed

8 files changed

+227
-31
lines changed
 

Diff for: ‎insteonplm/devices/__init__.py

+117-15
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@
2727
MESSAGE_TYPE_ALL_LINK_CLEANUP,
2828
MESSAGE_FLAG_DIRECT_MESSAGE_NAK_0XA0,
2929
MESSAGE_STANDARD_MESSAGE_RECEIVED_0X50,
30-
MESSAGE_EXTENDED_MESSAGE_RECEIVED_0X51
31-
)
30+
MESSAGE_EXTENDED_MESSAGE_RECEIVED_0X51,
31+
MESSAGE_TYPE_ALL_LINK_BROADCAST)
3232
from insteonplm.messagecallback import MessageCallback
3333
from insteonplm.messages.allLinkComplete import AllLinkComplete
3434
from insteonplm.messages.extendedReceive import ExtendedReceive
@@ -43,7 +43,7 @@
4343
DIRECT_ACK_WAIT_TIMEOUT = 3
4444
ALDB_RECORD_TIMEOUT = 10
4545
ALDB_RECORD_RETRIES = 20
46-
ALDB_ALL_RECORD_TIMEOUT = 30
46+
ALDB_ALL_RECORD_TIMEOUT = 120
4747
ALDB_ALL_RECORD_RETRIES = 5
4848

4949

@@ -215,9 +215,9 @@ def ALL_Link_cleanup(self, group, cmd_tuple):
215215
self.address.human, group)
216216
flags = MessageFlags.template(MESSAGE_TYPE_ALL_LINK_CLEANUP, 0, 3, 3)
217217
msg = StandardSend(self._address, cmd_tuple, cmd2=group, flags=flags)
218-
self._send_msg(msg, self._handle_ALL_Link_cleanup_ack)
218+
self._send_msg(msg, self._handle_ALL_Link_cleanup)
219219

220-
def _handle_ALL_Link_cleanup_ack(self, msg):
220+
def _handle_ALL_Link_cleanup(self, msg):
221221
_LOGGER.debug('Received ALL-Link Cleanup ACK from %s; ALL-Link group '
222222
'0x%x; looking for responder states',
223223
msg.address.human, msg.cmd2)
@@ -226,16 +226,10 @@ def _handle_ALL_Link_cleanup_ack(self, msg):
226226
responders = self._find_group_responder_states(msg.target, msg.cmd2)
227227

228228
for responder in responders:
229-
_LOGGER.debug('Calling %s:0x%x:handle_ALL_Link_cleanup_ack',
229+
_LOGGER.debug('Calling %s:0x%02x:handle_ALL_Link_cleanup',
230230
self._address.human, responder)
231-
if hasattr(self.states[responder], 'handle_ALL_Link_cleanup_ack'):
232-
self.states[responder].handle_ALL_Link_cleanup_ack(
233-
msg, responders[responder])
234-
else:
235-
_LOGGER.warning('Device %s:0x%x has no '
236-
'handle_ALL_Link_cleanup_ack() method. '
237-
'Cannot perform ALL-Link cleanup',
238-
self._address.human, responder)
231+
self.states[responder].handle_ALL_Link_cleanup(
232+
msg, responders[responder])
239233

240234
def _find_group_responder_states(self, ctl, ctl_group):
241235
_LOGGER.debug('Looking for responder to controller %s:0x%x',
@@ -729,11 +723,21 @@ def _register_messages(self):
729723
self._message_callbacks.add(template_all_link_complete,
730724
self._handle_all_link_complete)
731725

726+
template_All_Link_broadcast = StandardReceive.template(
727+
flags=MessageFlags.template(MESSAGE_TYPE_ALL_LINK_CLEANUP, None))
728+
self._message_callbacks.add(template_All_Link_broadcast,
729+
self._handle_All_Link_broadcast)
730+
731+
template_All_Link_cleanup = StandardReceive.template(
732+
flags=MessageFlags.template(MESSAGE_TYPE_ALL_LINK_BROADCAST, None))
733+
self._message_callbacks.add(template_All_Link_cleanup,
734+
self._handle_All_Link_broadcast)
735+
732736
# Send / Receive message processing
733737
def receive_message(self, msg):
734738
"""Receive a messages sent to this device."""
735739
_LOGGER.debug('Starting Device.receive_message for %s',
736-
msg.address.human)
740+
self.address.human)
737741
if hasattr(msg, 'isack') and msg.isack:
738742
_LOGGER.debug('Got Message ACK %s', id(msg))
739743
if self._sent_msg_wait_for_directACK.get('callback') is not None:
@@ -800,8 +804,24 @@ def _is_duplicate(self, msg):
800804
if msg.matches_pattern(prev_msg):
801805
ret_val = True
802806

807+
# Add current message to recent list
803808
self._recent_messages.put_nowait(
804809
{"msg": msg, "received": datetime.datetime.now()})
810+
811+
# If an ALL-Link Broadcast was successfully received the ALL-Link
812+
# cleanup should be considered a duplicate if not a 0x06 status report
813+
if(msg.flags.isAllLinkBroadcast and msg.cmd1 != MESSAGE_ACK):
814+
# Fabricate duplicate all-link cleanup
815+
dup_target = self._plm.address
816+
dup_group = msg.targetHi
817+
dup_flags = MessageFlags.template(MESSAGE_TYPE_ALL_LINK_CLEANUP,
818+
0, 3, 3)
819+
dup_msg = StandardReceive(msg.address, dup_target,
820+
{'cmd1': msg.cmd1, 'cmd2': dup_group},
821+
flags=dup_flags)
822+
self._recent_messages.put_nowait(
823+
{"msg": dup_msg, "received": datetime.datetime.now()})
824+
805825
return ret_val
806826

807827
def _send_msg(self, msg, callback=None, on_timeout=False):
@@ -870,6 +890,88 @@ async def _wait_for_direct_ACK(self):
870890
def _aldb_loaded_callback(self):
871891
self._plm.devices.save_device_info()
872892

893+
def _handle_All_Link_broadcast(self, msg):
894+
"""Process ALL-Link broadcast or cleanup received from devices.
895+
896+
Parameters:
897+
msg.address: controller's address
898+
msg.targetHi: group triggered on controller for broadcast msg
899+
msg.cmd2: group triggers on controller for clean up msg
900+
901+
Searches for controller's address and group triggered in this devices
902+
ALDB to find this devices responding group.
903+
904+
"""
905+
if msg.cmd1 != MESSAGE_ACK: # Ignore 0x06 status reports
906+
907+
# Don't bother searching the ALDB if there isn't at least one
908+
# state that can respond to controllers
909+
responder = False
910+
for state in self._stateList:
911+
if self._stateList[state].is_responder:
912+
responder = True
913+
break
914+
915+
if responder:
916+
ctl_group = msg.targetHi
917+
_LOGGER.debug('_handle_All_Link_broadcast msg: %s for '
918+
'device %s', id(msg), self.address.human)
919+
if not ctl_group:
920+
# Not a broadcast message so must be a cleanup
921+
ctl_group = msg.cmd2
922+
923+
resp_groups = self._find_responder_groups(msg.address,
924+
ctl_group)
925+
for resp_group in resp_groups:
926+
if self._stateList[resp_group]:
927+
self._stateList[resp_group].handle_ALL_Link_cleanup(
928+
msg, resp_groups[resp_group])
929+
else:
930+
_LOGGER.warning('ALDB Responder record maps to '
931+
'nonexistent state')
932+
933+
def _find_responder_groups(self, ctl_address, ctl_group):
934+
"""Identify which PLM group responds to ctl / ctl_group.
935+
936+
Parameters:
937+
ctl_address: address object of controller that sent ALL-Link command
938+
ctl_group: group that was triggered on the controller
939+
940+
Returns:
941+
dictionary of all responding groups on this device
942+
key: responding group on this device from data3
943+
value: recall_level from data1
944+
945+
NOTE: The IM class overrides this method for the PLM's underlying
946+
device object.
947+
948+
"""
949+
_LOGGER.debug('_find_responder_groups: looking for ctl: %s, '
950+
'ctl_group: 0x%02x on device %s', ctl_address.human,
951+
ctl_group, self.address.human)
952+
groups = {}
953+
if self._aldb:
954+
for rec_num in self._aldb:
955+
rec = self._aldb[rec_num]
956+
if(rec.control_flags.is_responder and
957+
rec.group == ctl_group and
958+
rec.address == ctl_address):
959+
# Recall level is stored in data1
960+
# This device's group is stored in data3
961+
data3 = rec.data3 if rec.data3 != 0 else 1
962+
groups[data3] = rec.data1
963+
_LOGGER.debug('Found %d responders on device %s', len(groups),
964+
self.address.human)
965+
else:
966+
if self._aldb.status == ALDBStatus.LOADED:
967+
_LOGGER.warning('No responders found; ALDB is empty %s',
968+
self.address.human)
969+
else:
970+
_LOGGER.warning('No responders found; ALDB is not loaded %s',
971+
self.address.human)
972+
973+
return groups
974+
873975

874976
# pylint: disable=too-many-instance-attributes
875977
class X10Device():

Diff for: ‎insteonplm/plm.py

+59-5
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
X10CommandType,
1414
X10_COMMAND_ALL_UNITS_OFF,
1515
X10_COMMAND_ALL_LIGHTS_ON,
16-
X10_COMMAND_ALL_LIGHTS_OFF)
16+
X10_COMMAND_ALL_LIGHTS_OFF,
17+
MESSAGE_STANDARD_MESSAGE_RECEIVED_0X50)
1718
from insteonplm.address import Address
1819
from insteonplm.devices import Device, ALDBRecord, ALDBStatus
1920
from insteonplm.linkedDevices import LinkedDevices
@@ -650,10 +651,20 @@ async def _peel_messages_from_buffer(self):
650651
def _process_recv_queue(self):
651652
msg = self._recv_queue.pop()
652653
_LOGGER.debug('RX: %s:%s', id(msg), msg)
653-
callbacks = self._message_callbacks.get_callbacks_from_message(msg)
654654
if hasattr(msg, 'isack') or hasattr(msg, 'isnak'):
655655
self._acknak_queue.put_nowait(msg)
656-
if hasattr(msg, 'address'):
656+
657+
if(msg.code == MESSAGE_STANDARD_MESSAGE_RECEIVED_0X50 and
658+
(msg.flags.isAllLinkBroadcast or msg.flags.isAllLinkCleanup)):
659+
# Each device should look at the responder entries in its ALDB
660+
# to see if the device should respond to this sender/sender group
661+
# Responding means updating subscribers with the correct level
662+
if msg.cmd1 != MESSAGE_ACK: # not 0x06 status report
663+
for device in self.devices:
664+
if self.devices[device]:
665+
self.devices[device].receive_message(msg)
666+
667+
elif hasattr(msg, 'address'):
657668
device = self.devices[msg.address.hex]
658669
if device:
659670
device.receive_message(msg)
@@ -664,8 +675,51 @@ def _process_recv_queue(self):
664675
device.receive_message(msg)
665676
except KeyError:
666677
pass
667-
for callback in callbacks:
668-
self._loop.call_soon(callback, msg)
678+
679+
# This check is a hack because message processing is not isolated
680+
# between IM and Device (nugget/python-insteonplm#203)
681+
if(msg.code != MESSAGE_STANDARD_MESSAGE_RECEIVED_0X50 or not
682+
(msg.flags.isAllLinkBroadcast or msg.flags.isAllLinkCleanup)):
683+
callbacks = self._message_callbacks.get_callbacks_from_message(msg)
684+
for callback in callbacks:
685+
_LOGGER.debug('Scheduling msg callback: %s', callback)
686+
self._loop.call_soon(callback, msg)
687+
688+
def _find_responder_groups(self, ctl_address, ctl_group):
689+
"""Identify which PLM group responds to ctl / ctl_group.
690+
691+
Parameters:
692+
ctl_address: address object of controller that sent ALL-Link command
693+
ctl_group: group that was triggered on the controller
694+
695+
Returns:
696+
dictionary of all responding groups on this device
697+
key: responding group on this device from data3
698+
value: recall_level from data1
699+
700+
NOTE: This overrides the same method in Device because the PLM requires
701+
different logic for finding the responder groups. The PLM does not
702+
store the local responding group in its responder records (devices do
703+
this). The only option is to look for all matching "controller"
704+
entries. This assumes if PLM is controller it must also be responder
705+
for the ALL-Link group.
706+
707+
"""
708+
_LOGGER.debug('_find_responder_groups: looking for ctl: %s, '
709+
'ctl_group: 0x%x on device %s', ctl_address.human,
710+
ctl_group, self.address.human)
711+
groups = {}
712+
for rec_num in self._aldb:
713+
rec = self._aldb[rec_num]
714+
if (rec.control_flags.is_controller and rec.data1 == ctl_group and
715+
rec.address == ctl_address):
716+
# Recall level is always "on" for PLM responders
717+
groups[rec.group] = 0xff
718+
719+
_LOGGER.debug('Found %d responders on device %s', len(groups),
720+
self.address.human)
721+
722+
return groups
669723

670724
def _unpack_buffer(self):
671725
buffer = bytearray()

Diff for: ‎insteonplm/states/__init__.py

+9-2
Original file line numberDiff line numberDiff line change
@@ -116,12 +116,19 @@ def register_updates(self, callback):
116116
_LOGGER.debug("Registered callback for state: %s", self._stateName)
117117
self._observer_callbacks.append(callback)
118118

119+
# pylint: disable=unused-argument
120+
def handle_ALL_Link_cleanup(self, msg, recall_level):
121+
"""Update the state's subscribers with the new level or fixed level."""
122+
_LOGGER.warning('State %s has no handle_ALL_Link_cleanup() method. '
123+
'Cannot perform ALL-Link cleanup',
124+
self.__class__.__name__)
125+
119126
def _update_subscribers(self, val):
120127
"""Save state value and notify listeners of the change."""
121128
self._value = val
122129
for callback in self._observer_callbacks:
123-
_LOGGER.debug('_update_subscribers state %s:0x%x updating '
124-
'subscribers with level 0x%x', self.address.human,
130+
_LOGGER.debug('_update_subscribers state %s:0x%02x updating '
131+
'subscribers with level 0x%02x', self.address.human,
125132
self.group, val)
126133

127134
callback(self._address, self._group, val)

Diff for: ‎insteonplm/states/allLinkGroup.py

+30-3
Original file line numberDiff line numberDiff line change
@@ -139,12 +139,40 @@ def dim(self):
139139
self._ALL_Link_cleanup_method(self.group,
140140
COMMAND_LIGHT_DIM_ONE_STEP_0X16_0X00)
141141

142+
def handle_ALL_Link_cleanup(self, msg, recall_level):
143+
"""Update the state's subscribers with the new level or fixed level."""
144+
level = 255
145+
if msg.cmd1 == COMMAND_LIGHT_ON_0X11_NONE.get('cmd1', None):
146+
level = recall_level
147+
elif msg.cmd1 == COMMAND_LIGHT_ON_FAST_0X12_NONE.get('cmd1', None):
148+
level = 255
149+
elif msg.cmd1 == COMMAND_LIGHT_OFF_0X13_0X00.get('cmd1', None):
150+
level = 0
151+
elif msg.cmd1 == COMMAND_LIGHT_OFF_FAST_0X14_0X00.get('cmd1', None):
152+
level = 0
153+
else:
154+
_LOGGER.error('AlL-Link_cleanup device %s:0x%02x command '
155+
'unknown 0x%02x', msg.address.human, self.group,
156+
msg.cmd1)
157+
158+
_LOGGER.debug('AlL-Link_cleanup device %s:0x%02x updating '
159+
'subscribers with level 0x%x from command 0x%02x',
160+
self.address.human, self.group, level, msg.cmd1)
161+
162+
self._update_subscribers(level)
163+
164+
# pylint: disable=unused-argument
142165
def _on_message_received(self, msg):
143-
cmd2 = msg.cmd2 if msg.cmd2 else 255
144-
self._update_subscribers(cmd2)
166+
_LOGGER.debug('AlL-Link_cleanup received device %s:0x%x updating '
167+
'subscribers with level 0x%x from command 0x%x',
168+
self.address.human, self.group, 0xff, msg.cmd1)
169+
self._update_subscribers(0xff)
145170

146171
# pylint: disable=unused-argument
147172
def _off_message_received(self, msg):
173+
_LOGGER.debug('AlL-Link_cleanup received device %s:0x%x updating '
174+
'subscribers with level 0x%x from command 0x%x',
175+
self.address.human, self.group, 0x00, msg.cmd1)
148176
self._update_subscribers(0x00)
149177

150178
# pylint: disable=unused-argument
@@ -159,5 +187,4 @@ def _send_status_request(self):
159187
self._status_message_received)
160188

161189
def _status_message_received(self, msg):
162-
_LOGGER.debug("DimmableSwitch status message received called")
163190
self._update_subscribers(msg.cmd2)

Diff for: ‎insteonplm/states/dimmable.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -237,8 +237,8 @@ def _status_message_received(self, msg):
237237
_LOGGER.debug("DimmableSwitch status message received called")
238238
self._update_subscribers(msg.cmd2)
239239

240-
def handle_ALL_Link_cleanup_ack(self, msg, recall_level):
241-
"""Update the state's subscribers with the new level.
240+
def handle_ALL_Link_cleanup(self, msg, recall_level):
241+
"""Update the state's subscribers with the new level or a fixed level.
242242
243243
For the ALL-Link recall message, the recall level is set during the
244244
devices ALDB discovery. If ALDB has not been read, the default level

Diff for: ‎insteonplm/states/onOff.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,8 @@ def _status_message_received(self, msg):
8282
else:
8383
self._update_subscribers(0xff)
8484

85-
def handle_ALL_Link_cleanup_ack(self, msg, recall_level):
86-
"""Update the states subscribers with the new level.
85+
def handle_ALL_Link_cleanup(self, msg, recall_level):
86+
"""Update the states subscribers with the new level or a fixed level.
8787
8888
For the ALL-Link recall message, the recall level is set during the
8989
devices ALDB discovery. If ALDB has not been read, the default level

Diff for: ‎insteonplm/tools.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ def async_new_device_callback(self, device):
125125
# pylint: disable=no-self-use
126126
def async_state_change_callback(self, addr, state, value):
127127
"""Log the state change."""
128-
_LOGGING.info('Device %s state 0x%x value is changed to %s',
128+
_LOGGING.info('Device %s state 0x%02x value is changed to %s',
129129
addr.human, state, value)
130130

131131
def async_aldb_loaded_callback(self):
@@ -775,7 +775,6 @@ async def do_load_aldb(self, args):
775775
addr = params[0]
776776
except IndexError:
777777
_LOGGING.error('Device address required.')
778-
self.do_help('load_aldb')
779778

780779
try:
781780
clear_prior = params[1]

0 commit comments

Comments
 (0)