Skip to content

Commit

Permalink
Merge pull request #24 from hostcc/feature/alarm-callback
Browse files Browse the repository at this point in the history
Alarm callback support
  • Loading branch information
hostcc authored Nov 30, 2022
2 parents 84b3f79 + 4bf9370 commit 55ca0ae
Show file tree
Hide file tree
Showing 9 changed files with 95 additions and 14 deletions.
4 changes: 2 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,17 +36,17 @@
'Topic :: System :: Hardware',
'License :: OSI Approved :: MIT License',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3 :: Only',
],

keywords='g90, alarm, protocol',
package_dir={'': 'src'},
packages=find_packages(where='src'),
python_requires='>=3.6, <4',
python_requires='>=3.7, <4',
install_requires=[],

extras_require={
Expand Down
2 changes: 1 addition & 1 deletion sonar-project.properties
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
sonar.python.version=3.6, 3.7, 3.8, 3.9
sonar.python.version=3.7, 3.8, 3.9, 3.10
sonar.python.coverage.reportPaths=coverage.xml
sonar.python.pylint.reportPaths=pylint.txt
sonar.python.flake8.reportPaths=flake8.txt
Expand Down
16 changes: 16 additions & 0 deletions src/pyg90alarm/alarm.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ def __init__(self, host, port=REMOTE_PORT, sock=None,
self._sensor_cb = None
self._armdisarm_cb = None
self._door_open_close_cb = None
self._alarm_cb = None
self._reset_occupancy_interval = reset_occupancy_interval
self._alert_config = None
self._sms_alert_when_armed = False
Expand Down Expand Up @@ -535,6 +536,20 @@ def armdisarm_callback(self):
def armdisarm_callback(self, value):
self._armdisarm_cb = value

@property
def alarm_callback(self):
"""
Get or set device alarm callback, the callback is invoked when
device alarm triggers.
:type: .. py:function:: ()(sensor_idx: int, sensor_name: str)
"""
return self._alarm_cb

@alarm_callback.setter
def alarm_callback(self, value):
self._alarm_cb = value

async def listen_device_notifications(self, sock=None):
"""
Starts internal listener for device notifications/alerts.
Expand All @@ -546,6 +561,7 @@ async def listen_device_notifications(self, sock=None):
sensor_cb=self._internal_sensor_cb,
door_open_close_cb=self._internal_door_open_close_cb,
armdisarm_cb=self._internal_armdisarm_cb,
alarm_cb=self._alarm_cb,
sock=sock)
await self._notifications.listen()

Expand Down
17 changes: 14 additions & 3 deletions src/pyg90alarm/device_notifications.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,13 +93,14 @@ class G90DeviceNotificationProtocol:
:meta private:
"""
def __init__(self, armdisarm_cb=None, sensor_cb=None,
door_open_close_cb=None):
door_open_close_cb=None, alarm_cb=None):
"""
tbd
"""
self._armdisarm_cb = armdisarm_cb
self._sensor_cb = sensor_cb
self._door_open_close_cb = door_open_close_cb
self._alarm_cb = alarm_cb

def connection_made(self, transport):
"""
Expand Down Expand Up @@ -169,6 +170,11 @@ def _handle_alert(self, addr, alert):
G90Callback.invoke(self._armdisarm_cb, state)
return

if alert.type == G90AlertTypes.ALARM:
_LOGGER.debug('Alarm: %s', alert.zone_name)
G90Callback.invoke(self._alarm_cb, alert.event_id, alert.zone_name)
return

_LOGGER.warning('Unknown alert received from %s:%s:'
' type %s, data %s',
addr[0], addr[1], alert.type, alert)
Expand Down Expand Up @@ -227,21 +233,26 @@ class G90DeviceNotifications:
tbd
"""
def __init__(self, port=12901, armdisarm_cb=None, sensor_cb=None,
door_open_close_cb=None, sock=None):
door_open_close_cb=None, alarm_cb=None, sock=None):
# pylint: disable=too-many-arguments
self._notification_transport = None
self._port = port
self._armdisarm_cb = armdisarm_cb
self._sensor_cb = sensor_cb
self._door_open_close_cb = door_open_close_cb
self._alarm_cb = alarm_cb
self._sock = sock

def proto_factory(self):
"""
tbd
"""
return G90DeviceNotificationProtocol(
self._armdisarm_cb, self._sensor_cb, self._door_open_close_cb)
armdisarm_cb=self._armdisarm_cb,
sensor_cb=self._sensor_cb,
door_open_close_cb=self._door_open_close_cb,
alarm_cb=self._alarm_cb
)

async def listen(self):
"""
Expand Down
14 changes: 12 additions & 2 deletions src/pyg90alarm/entities/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -363,14 +363,24 @@ async def set_enabled(self, value):
'Refreshing sensor at index=%s, position in protocol list=%s',
self.index, self._proto_idx
)
sensors = self.parent.paginated_result(
sensors_result = self.parent.paginated_result(
G90Commands.GETSENSORLIST,
start=self._proto_idx, end=self._proto_idx
)
sensors = [x async for x in sensors_result]

# Abort if sensor is not found
if not sensors:
_LOGGER.error(
'Sensor index=%s not found when attempting to set its'
' enable/disable state',
self.index,
)
return

# Compare actual sensor data from what the sensor has been instantiated
# from, and abort the operation if out-of-band changes are detected.
_sensor_pos, sensor_data = [x async for x in sensors][0]
_sensor_pos, sensor_data = sensors[0]
if self._protocol_incoming_data_kls(
*sensor_data
) != self._protocol_data:
Expand Down
13 changes: 8 additions & 5 deletions src/pyg90alarm/paginated_result.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,11 @@ async def process(self):
' latter', self._end, cmd.total)
self._end = cmd.total

_LOGGER.debug('Retrieved %i records in the iteration,'
' %i available in total, target end'
' record number is %i',
cmd.count, cmd.total, self._end)

# Produce the resulting records for the consumer
for idx, data in enumerate(cmd.result):
# Protocol uses one-based indexes, `start` implies that so no
Expand All @@ -107,11 +112,9 @@ async def process(self):

# End the loop if we processed same number of sensors as in the
# pagination header (or attempted to process more than that by
# an error)
_LOGGER.debug('Retrieved %i records in the iteration,'
' %i available in total, target end'
' record number is %i',
cmd.count, cmd.total, self._end)
# an error), or no records have been received
if not cmd.count:
break
if cmd.start + cmd.count - 1 >= self._end:
break
# Move to the next page for another iteration
Expand Down
26 changes: 26 additions & 0 deletions tests/test_alarm.py
Original file line number Diff line number Diff line change
Expand Up @@ -563,6 +563,32 @@ async def test_sensor_unsupported_disable(self):
b'ISTART[102,102,[102,[1,10]]]IEND\0',
])

async def test_sensor_disable_sensor_not_found_on_refresh(self):
g90 = G90Alarm(host='mocked', port=12345, sock=self.socket_mock)
self.socket_mock.recvfrom.side_effect = [
(
b'ISTART[102,'
b'[[2,1,2],'
b'["Night Light1",11,0,138,0,0,33,0,0,17,1,0,""],'
b'["Night Light2",10,0,138,0,0,33,0,0,17,1,0,""]'
b']]IEND\0',
('mocked', 12345)
),
(
b'ISTART[102,[[2,2,0]]]IEND\0',
('mocked', 12345)
),
]

sensors = await g90.get_sensors()
self.assertEqual(sensors[1].enabled, True)
await sensors[1].set_enabled(False)
self.assertEqual(sensors[1].enabled, True)
self.assert_callargs_on_sent_data([
b'ISTART[102,102,[102,[1,10]]]IEND\0',
b'ISTART[102,102,[102,[2,2]]]IEND\0',
])

async def test_device_unsupported_disable(self):
g90 = G90Alarm(host='mocked', port=12345, sock=self.socket_mock)
self.socket_mock.recvfrom.side_effect = [
Expand Down
15 changes: 15 additions & 0 deletions tests/test_notifications.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,3 +221,18 @@ async def test_doorbell_callback(self):
await asyncio.wait([future], timeout=0.1)
notifications.close()
door_open_close_cb.assert_called_once_with(111, 'Doorbell', True)

async def test_alarm_callback(self):
future = self.loop.create_future()
alarm_cb = MagicMock()
alarm_cb.side_effect = lambda *args: future.set_result(True)
notifications = G90DeviceNotifications(
alarm_cb=alarm_cb, sock=self.socket_mock)
await notifications.listen()
asynctest.set_read_ready(self.socket_mock, self.loop)
self.socket_mock.recvfrom.return_value = (
b'[208,[3,11,1,1,"Hall","DUMMYGUID",1630876128,0,[""]]]\0',
('mocked', 12345))
await asyncio.wait([future], timeout=0.1)
notifications.close()
alarm_cb.assert_called_once_with(11, 'Hall')
2 changes: 1 addition & 1 deletion tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ commands =
pylint --output-format=parseable --output=pylint.txt src/pyg90alarm
# Ensure only traces for in-repository module is processed, not for one
# installed by `tox` (see above for more details)
coverage run --source=src/pyg90alarm --parallel-mode -m unittest -v tests
coverage run --source=src/pyg90alarm --parallel-mode -m unittest -v tests []
commands_post =
# Show the `pylint` report to the standard output, to ease fixing the issues reported
cat pylint.txt
Expand Down

0 comments on commit 55ca0ae

Please sign in to comment.