pyg90alarm 2.0.1__tar.gz → 2.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. {pyg90alarm-2.0.1 → pyg90alarm-2.1.1}/PKG-INFO +10 -9
  2. {pyg90alarm-2.0.1 → pyg90alarm-2.1.1}/README.rst +9 -8
  3. {pyg90alarm-2.0.1 → pyg90alarm-2.1.1}/src/pyg90alarm/__init__.py +8 -3
  4. {pyg90alarm-2.0.1 → pyg90alarm-2.1.1}/src/pyg90alarm/alarm.py +21 -46
  5. {pyg90alarm-2.0.1 → pyg90alarm-2.1.1}/src/pyg90alarm/entities/device.py +8 -8
  6. {pyg90alarm-2.0.1 → pyg90alarm-2.1.1}/src/pyg90alarm/entities/sensor.py +211 -42
  7. pyg90alarm-2.1.1/src/pyg90alarm/local/config.py +158 -0
  8. {pyg90alarm-2.0.1 → pyg90alarm-2.1.1}/src/pyg90alarm.egg-info/PKG-INFO +10 -9
  9. {pyg90alarm-2.0.1 → pyg90alarm-2.1.1}/src/pyg90alarm.egg-info/SOURCES.txt +3 -1
  10. {pyg90alarm-2.0.1 → pyg90alarm-2.1.1}/tests/test_alarm.py +10 -284
  11. pyg90alarm-2.1.1/tests/test_config.py +52 -0
  12. pyg90alarm-2.1.1/tests/test_sensor.py +410 -0
  13. pyg90alarm-2.0.1/src/pyg90alarm/local/config.py +0 -59
  14. {pyg90alarm-2.0.1 → pyg90alarm-2.1.1}/.github/CODEOWNERS +0 -0
  15. {pyg90alarm-2.0.1 → pyg90alarm-2.1.1}/.github/workflows/main.yml +0 -0
  16. {pyg90alarm-2.0.1 → pyg90alarm-2.1.1}/.gitignore +0 -0
  17. {pyg90alarm-2.0.1 → pyg90alarm-2.1.1}/.pylintrc +0 -0
  18. {pyg90alarm-2.0.1 → pyg90alarm-2.1.1}/.readthedocs.yaml +0 -0
  19. {pyg90alarm-2.0.1 → pyg90alarm-2.1.1}/LICENSE +0 -0
  20. {pyg90alarm-2.0.1 → pyg90alarm-2.1.1}/MANIFEST.in +0 -0
  21. {pyg90alarm-2.0.1 → pyg90alarm-2.1.1}/docs/.DS_Store +0 -0
  22. {pyg90alarm-2.0.1 → pyg90alarm-2.1.1}/docs/.gitignore +0 -0
  23. {pyg90alarm-2.0.1 → pyg90alarm-2.1.1}/docs/api-docs.rst +0 -0
  24. {pyg90alarm-2.0.1 → pyg90alarm-2.1.1}/docs/cloud-protocol.rst +0 -0
  25. {pyg90alarm-2.0.1 → pyg90alarm-2.1.1}/docs/conf.py +0 -0
  26. {pyg90alarm-2.0.1 → pyg90alarm-2.1.1}/docs/index.rst +0 -0
  27. {pyg90alarm-2.0.1 → pyg90alarm-2.1.1}/docs/local-protocol.rst +0 -0
  28. {pyg90alarm-2.0.1 → pyg90alarm-2.1.1}/docs/requirements.txt +0 -0
  29. {pyg90alarm-2.0.1 → pyg90alarm-2.1.1}/pyproject.toml +0 -0
  30. {pyg90alarm-2.0.1 → pyg90alarm-2.1.1}/setup.cfg +0 -0
  31. {pyg90alarm-2.0.1 → pyg90alarm-2.1.1}/setup.py +0 -0
  32. {pyg90alarm-2.0.1 → pyg90alarm-2.1.1}/sonar-project.properties +0 -0
  33. {pyg90alarm-2.0.1 → pyg90alarm-2.1.1}/src/pyg90alarm/callback.py +0 -0
  34. {pyg90alarm-2.0.1 → pyg90alarm-2.1.1}/src/pyg90alarm/cloud/__init__.py +0 -0
  35. {pyg90alarm-2.0.1 → pyg90alarm-2.1.1}/src/pyg90alarm/cloud/const.py +0 -0
  36. {pyg90alarm-2.0.1 → pyg90alarm-2.1.1}/src/pyg90alarm/cloud/messages.py +0 -0
  37. {pyg90alarm-2.0.1 → pyg90alarm-2.1.1}/src/pyg90alarm/cloud/notifications.py +0 -0
  38. {pyg90alarm-2.0.1 → pyg90alarm-2.1.1}/src/pyg90alarm/cloud/protocol.py +0 -0
  39. {pyg90alarm-2.0.1 → pyg90alarm-2.1.1}/src/pyg90alarm/const.py +0 -0
  40. {pyg90alarm-2.0.1 → pyg90alarm-2.1.1}/src/pyg90alarm/definitions/__init__.py +0 -0
  41. {pyg90alarm-2.0.1 → pyg90alarm-2.1.1}/src/pyg90alarm/definitions/sensors.py +0 -0
  42. {pyg90alarm-2.0.1 → pyg90alarm-2.1.1}/src/pyg90alarm/entities/__init__.py +0 -0
  43. {pyg90alarm-2.0.1 → pyg90alarm-2.1.1}/src/pyg90alarm/entities/base_entity.py +0 -0
  44. {pyg90alarm-2.0.1 → pyg90alarm-2.1.1}/src/pyg90alarm/entities/base_list.py +0 -0
  45. {pyg90alarm-2.0.1 → pyg90alarm-2.1.1}/src/pyg90alarm/entities/device_list.py +0 -0
  46. {pyg90alarm-2.0.1 → pyg90alarm-2.1.1}/src/pyg90alarm/entities/sensor_list.py +0 -0
  47. {pyg90alarm-2.0.1 → pyg90alarm-2.1.1}/src/pyg90alarm/exceptions.py +0 -0
  48. {pyg90alarm-2.0.1 → pyg90alarm-2.1.1}/src/pyg90alarm/local/__init__.py +0 -0
  49. {pyg90alarm-2.0.1 → pyg90alarm-2.1.1}/src/pyg90alarm/local/base_cmd.py +0 -0
  50. {pyg90alarm-2.0.1 → pyg90alarm-2.1.1}/src/pyg90alarm/local/discovery.py +0 -0
  51. {pyg90alarm-2.0.1 → pyg90alarm-2.1.1}/src/pyg90alarm/local/history.py +0 -0
  52. {pyg90alarm-2.0.1 → pyg90alarm-2.1.1}/src/pyg90alarm/local/host_info.py +0 -0
  53. {pyg90alarm-2.0.1 → pyg90alarm-2.1.1}/src/pyg90alarm/local/host_status.py +0 -0
  54. {pyg90alarm-2.0.1 → pyg90alarm-2.1.1}/src/pyg90alarm/local/notifications.py +0 -0
  55. {pyg90alarm-2.0.1 → pyg90alarm-2.1.1}/src/pyg90alarm/local/paginated_cmd.py +0 -0
  56. {pyg90alarm-2.0.1 → pyg90alarm-2.1.1}/src/pyg90alarm/local/paginated_result.py +0 -0
  57. {pyg90alarm-2.0.1 → pyg90alarm-2.1.1}/src/pyg90alarm/local/targeted_discovery.py +0 -0
  58. {pyg90alarm-2.0.1 → pyg90alarm-2.1.1}/src/pyg90alarm/local/user_data_crc.py +0 -0
  59. {pyg90alarm-2.0.1 → pyg90alarm-2.1.1}/src/pyg90alarm/notifications/__init__.py +0 -0
  60. {pyg90alarm-2.0.1 → pyg90alarm-2.1.1}/src/pyg90alarm/notifications/base.py +0 -0
  61. {pyg90alarm-2.0.1 → pyg90alarm-2.1.1}/src/pyg90alarm/notifications/protocol.py +0 -0
  62. {pyg90alarm-2.0.1 → pyg90alarm-2.1.1}/src/pyg90alarm/py.typed +0 -0
  63. {pyg90alarm-2.0.1 → pyg90alarm-2.1.1}/src/pyg90alarm.egg-info/dependency_links.txt +0 -0
  64. {pyg90alarm-2.0.1 → pyg90alarm-2.1.1}/src/pyg90alarm.egg-info/requires.txt +0 -0
  65. {pyg90alarm-2.0.1 → pyg90alarm-2.1.1}/src/pyg90alarm.egg-info/top_level.txt +0 -0
  66. {pyg90alarm-2.0.1 → pyg90alarm-2.1.1}/tests/__init__.py +0 -0
  67. {pyg90alarm-2.0.1 → pyg90alarm-2.1.1}/tests/conftest.py +0 -0
  68. {pyg90alarm-2.0.1 → pyg90alarm-2.1.1}/tests/device_mock.py +0 -0
  69. {pyg90alarm-2.0.1 → pyg90alarm-2.1.1}/tests/test_base_commands.py +0 -0
  70. {pyg90alarm-2.0.1 → pyg90alarm-2.1.1}/tests/test_cloud_notifications.py +0 -0
  71. {pyg90alarm-2.0.1 → pyg90alarm-2.1.1}/tests/test_discovery.py +0 -0
  72. {pyg90alarm-2.0.1 → pyg90alarm-2.1.1}/tests/test_history.py +0 -0
  73. {pyg90alarm-2.0.1 → pyg90alarm-2.1.1}/tests/test_local_notifications.py +0 -0
  74. {pyg90alarm-2.0.1 → pyg90alarm-2.1.1}/tests/test_paginated_commands.py +0 -0
  75. {pyg90alarm-2.0.1 → pyg90alarm-2.1.1}/tox.ini +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyg90alarm
3
- Version: 2.0.1
3
+ Version: 2.1.1
4
4
  Summary: G90 Alarm system protocol
5
5
  Home-page: https://github.com/hostcc/pyg90alarm
6
6
  Author: Ilia Sotnikov
@@ -96,8 +96,8 @@ Python, and it opens up a way for further integrations.
96
96
  Supported hardware
97
97
  ==================
98
98
 
99
- It mightn't possible to list every system supported by the package due to
100
- manufacturers name the products differently. Here is the list of hardware
99
+ It might not be possible to list every system supported by the package due to
100
+ manufacturers naming the products differently. Here is the list of hardware
101
101
  known to work with the package:
102
102
 
103
103
  * `PST G90B Plus <http://www.cameralarms.com/products/auto_dial_alarm_system/185.html>`_
@@ -170,7 +170,7 @@ set the IP address allocation up.
170
170
  alert to be enabled in the mobile application, otherwise it won't be
171
171
  recorded in the history.
172
172
 
173
- For the local notifications to be enabled the ```G90Alarm.use_local_notifications()``` needs to be called upon contstucting an instance of ``G90Alarm`` class, then ```G90Alarm.listen_notifications()`z to start processing those coming from the panel - the code fragment below demonstrate that though being incomplete since callbacks (e.g. ``G90Alarm.on_armdisarm()``) should be set for the actual processing of the notifications.
173
+ For the local notifications to be enabled the ``G90Alarm.use_local_notifications()`` needs to be called upon constructing an instance of ``G90Alarm`` class, then ``G90Alarm.listen_notifications()`` to start processing those coming from the panel - the code fragment below demonstrates that though being incomplete since callbacks (e.g. ``G90Alarm.on_armdisarm()``) should be set for the actual processing of the notifications.
174
174
 
175
175
  .. code:: python
176
176
 
@@ -189,10 +189,10 @@ Cloud notifications
189
189
  The cloud protocol is native to the panel and is used to interact with mobile application. The package can mimic the cloud server and interpret the messages the panel sends to the cloud, allowing to receive the notifications and alerts.
190
190
  While the protocol also allows to send commands to the panel, it is not implemented and local protocol is used for that - i.e. when cloud notifications are in use the local protocol still utilized for sending commands to the panel.
191
191
 
192
- The cloud protocol is TCP based and typically interacts with cloud service at known IP address and port (not customizeable at panel side). To process the cloud notifications all the traffic from panel towards the cloud (IP address ``47.88.7.61`` and TCP port ``5678`` as of writing) needs to be diverted to the node where the package is running - depending on your network equipment it could be port forwarding, DNAT or other means. It is unclear whether the panel utilizes DNS to resolve the cloud service IP address, hence the documentation only mentions IP-based traffic redirection.
192
+ The cloud protocol is TCP based and typically interacts with cloud service at known IP address and port (not customizable at panel side). To process the cloud notifications all the traffic from panel towards the cloud (IP address ``47.88.7.61`` and TCP port ``5678`` as of writing) needs to be diverted to the node where the package is running - depending on your network equipment it could be port forwarding, DNAT or other means. It is unclear whether the panel utilizes DNS to resolve the cloud service IP address, hence the documentation only mentions IP-based traffic redirection.
193
193
 
194
194
  Please see
195
- `cloud-protocol.rst <docs/cloud-protocol.rst>`_ for further details on the protocol.
195
+ `the section <docs/cloud-protocol.rst>`_ for further details on the protocol.
196
196
 
197
197
  The benefit of the cloud notifications is that the panel no longer required to have ``10.10.10.250`` IP address.
198
198
 
@@ -200,10 +200,10 @@ The package could act as:
200
200
 
201
201
  - Standalone cloud server with no Internet connectivity or cloud service
202
202
  required at all - good if you'd like to avoid having a vendor service involved. Please note the mobile application will show panel as offline in this mode
203
- - Chained cloud server, where in addition to intepreting the notifications it
204
- will also forward all packets received from the panel to the cloud server, and pass its responses back to the panel. This allows to have notificaitons processed by the package and the mobile application working as well.
203
+ - Chained cloud server, where in addition to interpreting the notifications it
204
+ will also forward all packets received from the panel to the cloud server, and pass its responses back to the panel. This allows to have notifications processed by the package and the mobile application working as well.
205
205
 
206
- .. note:: Sending packets upstream to the known IP address and port of the cloud server might result in those looped back (since traffic from panel to cloud service has to be redirected to the host where package runs), if your network equipment can't account for source address in redirection rules (i.e. limiting the port redirection to the panel's IP address). In that case you'll need another redirection, from the host where the package runs to the cloud service using an IP from your network. That way those two redirection rules will coexist correctly. To illustate:
206
+ .. note:: Sending packets upstream to the known IP address and port of the cloud server might result in those looped back (since traffic from panel to cloud service has to be redirected to the host where package runs), if your network equipment can't account for source address in redirection rules (i.e. limiting the port redirection to the panel's IP address). In that case you'll need another redirection, from the host where the package runs to the cloud service using an IP from your network. That way those two redirection rules will coexist correctly. To illustrate:
207
207
 
208
208
  Port forwarding rule 1:
209
209
 
@@ -213,6 +213,7 @@ The package could act as:
213
213
  - Redirect to host: host where package runs
214
214
  - Redirect to port: 5678 (or other port if you want to use it)
215
215
 
216
+
216
217
  Port forwarding rule 2 (optional):
217
218
 
218
219
  - Source: host where package runs
@@ -49,8 +49,8 @@ Python, and it opens up a way for further integrations.
49
49
  Supported hardware
50
50
  ==================
51
51
 
52
- It mightn't possible to list every system supported by the package due to
53
- manufacturers name the products differently. Here is the list of hardware
52
+ It might not be possible to list every system supported by the package due to
53
+ manufacturers naming the products differently. Here is the list of hardware
54
54
  known to work with the package:
55
55
 
56
56
  * `PST G90B Plus <http://www.cameralarms.com/products/auto_dial_alarm_system/185.html>`_
@@ -123,7 +123,7 @@ set the IP address allocation up.
123
123
  alert to be enabled in the mobile application, otherwise it won't be
124
124
  recorded in the history.
125
125
 
126
- For the local notifications to be enabled the ```G90Alarm.use_local_notifications()``` needs to be called upon contstucting an instance of ``G90Alarm`` class, then ```G90Alarm.listen_notifications()`z to start processing those coming from the panel - the code fragment below demonstrate that though being incomplete since callbacks (e.g. ``G90Alarm.on_armdisarm()``) should be set for the actual processing of the notifications.
126
+ For the local notifications to be enabled the ``G90Alarm.use_local_notifications()`` needs to be called upon constructing an instance of ``G90Alarm`` class, then ``G90Alarm.listen_notifications()`` to start processing those coming from the panel - the code fragment below demonstrates that though being incomplete since callbacks (e.g. ``G90Alarm.on_armdisarm()``) should be set for the actual processing of the notifications.
127
127
 
128
128
  .. code:: python
129
129
 
@@ -142,10 +142,10 @@ Cloud notifications
142
142
  The cloud protocol is native to the panel and is used to interact with mobile application. The package can mimic the cloud server and interpret the messages the panel sends to the cloud, allowing to receive the notifications and alerts.
143
143
  While the protocol also allows to send commands to the panel, it is not implemented and local protocol is used for that - i.e. when cloud notifications are in use the local protocol still utilized for sending commands to the panel.
144
144
 
145
- The cloud protocol is TCP based and typically interacts with cloud service at known IP address and port (not customizeable at panel side). To process the cloud notifications all the traffic from panel towards the cloud (IP address ``47.88.7.61`` and TCP port ``5678`` as of writing) needs to be diverted to the node where the package is running - depending on your network equipment it could be port forwarding, DNAT or other means. It is unclear whether the panel utilizes DNS to resolve the cloud service IP address, hence the documentation only mentions IP-based traffic redirection.
145
+ The cloud protocol is TCP based and typically interacts with cloud service at known IP address and port (not customizable at panel side). To process the cloud notifications all the traffic from panel towards the cloud (IP address ``47.88.7.61`` and TCP port ``5678`` as of writing) needs to be diverted to the node where the package is running - depending on your network equipment it could be port forwarding, DNAT or other means. It is unclear whether the panel utilizes DNS to resolve the cloud service IP address, hence the documentation only mentions IP-based traffic redirection.
146
146
 
147
147
  Please see
148
- `cloud-protocol.rst <docs/cloud-protocol.rst>`_ for further details on the protocol.
148
+ `the section <docs/cloud-protocol.rst>`_ for further details on the protocol.
149
149
 
150
150
  The benefit of the cloud notifications is that the panel no longer required to have ``10.10.10.250`` IP address.
151
151
 
@@ -153,10 +153,10 @@ The package could act as:
153
153
 
154
154
  - Standalone cloud server with no Internet connectivity or cloud service
155
155
  required at all - good if you'd like to avoid having a vendor service involved. Please note the mobile application will show panel as offline in this mode
156
- - Chained cloud server, where in addition to intepreting the notifications it
157
- will also forward all packets received from the panel to the cloud server, and pass its responses back to the panel. This allows to have notificaitons processed by the package and the mobile application working as well.
156
+ - Chained cloud server, where in addition to interpreting the notifications it
157
+ will also forward all packets received from the panel to the cloud server, and pass its responses back to the panel. This allows to have notifications processed by the package and the mobile application working as well.
158
158
 
159
- .. note:: Sending packets upstream to the known IP address and port of the cloud server might result in those looped back (since traffic from panel to cloud service has to be redirected to the host where package runs), if your network equipment can't account for source address in redirection rules (i.e. limiting the port redirection to the panel's IP address). In that case you'll need another redirection, from the host where the package runs to the cloud service using an IP from your network. That way those two redirection rules will coexist correctly. To illustate:
159
+ .. note:: Sending packets upstream to the known IP address and port of the cloud server might result in those looped back (since traffic from panel to cloud service has to be redirected to the host where package runs), if your network equipment can't account for source address in redirection rules (i.e. limiting the port redirection to the panel's IP address). In that case you'll need another redirection, from the host where the package runs to the cloud service using an IP from your network. That way those two redirection rules will coexist correctly. To illustrate:
160
160
 
161
161
  Port forwarding rule 1:
162
162
 
@@ -166,6 +166,7 @@ The package could act as:
166
166
  - Redirect to host: host where package runs
167
167
  - Redirect to port: 5678 (or other port if you want to use it)
168
168
 
169
+
169
170
  Port forwarding rule 2 (optional):
170
171
 
171
172
  - Source: host where package runs
@@ -28,11 +28,14 @@ from .local.paginated_result import G90PaginatedResult
28
28
  from .notifications.base import (
29
29
  G90DeviceAlert,
30
30
  )
31
- from .entities.sensor import G90Sensor, G90SensorTypes
31
+ from .entities.sensor import (
32
+ G90Sensor, G90SensorTypes, G90SensorAlertModes, G90SensorUserFlags
33
+ )
32
34
  from .entities.device import G90Device
33
35
  from .local.host_info import (
34
36
  G90HostInfo, G90HostInfoWifiStatus, G90HostInfoGsmStatus
35
37
  )
38
+ from .local.config import G90AlertConfigFlags
36
39
  from .local.host_status import G90HostStatus
37
40
  from .const import (
38
41
  G90MessageTypes,
@@ -48,8 +51,10 @@ from .exceptions import G90Error, G90TimeoutError
48
51
 
49
52
  __all__ = [
50
53
  'G90Alarm', 'G90BaseCommand', 'G90PaginatedResult', 'G90DeviceAlert',
51
- 'G90Sensor', 'G90SensorTypes', 'G90Device', 'G90HostInfo',
52
- 'G90HostInfoWifiStatus', 'G90HostInfoGsmStatus', 'G90HostStatus',
54
+ 'G90Sensor', 'G90SensorTypes', 'G90SensorAlertModes', 'G90SensorUserFlags',
55
+ 'G90AlertConfigFlags', 'G90Device',
56
+ 'G90HostInfo', 'G90HostInfoWifiStatus', 'G90HostInfoGsmStatus',
57
+ 'G90HostStatus',
53
58
  'G90MessageTypes', 'G90NotificationTypes', 'G90ArmDisarmTypes',
54
59
  'G90AlertTypes', 'G90AlertSources', 'G90AlertStates',
55
60
  'G90AlertStateChangeTypes', 'G90HistoryStates', 'G90Error',
@@ -201,7 +201,7 @@ class G90Alarm(G90NotificationProtocol):
201
201
  ] = None
202
202
  self._tamper_cb: Optional[TamperCallback] = None
203
203
  self._reset_occupancy_interval = reset_occupancy_interval
204
- self._alert_config: Optional[G90AlertConfigFlags] = None
204
+ self._alert_config = G90AlertConfig(self)
205
205
  self._sms_alert_when_armed = False
206
206
  self._alert_simulation_task: Optional[Task[Any]] = None
207
207
  self._alert_simulation_start_listener_back = False
@@ -385,50 +385,29 @@ class G90Alarm(G90NotificationProtocol):
385
385
  return G90HostStatus(*res)
386
386
 
387
387
  @property
388
- async def alert_config(self) -> G90AlertConfigFlags:
388
+ def alert_config(self) -> G90AlertConfig:
389
389
  """
390
- Property over new :meth:`.get_alert_config` method, retained for
391
- compatibility.
392
- """
393
- return await self.get_alert_config()
394
-
395
- async def get_alert_config(self) -> G90AlertConfigFlags:
396
- """
397
- Retrieves the alert configuration flags from the device. Please note
398
- the configuration is cached upon first call, so you need to
399
- re-instantiate the class to reflect any updates there.
390
+ Provides alert configuration object.
400
391
 
401
- :return: The alerts configured
392
+ :return: Alert configuration object
402
393
  """
403
- if not self._alert_config:
404
- self._alert_config = await self._alert_config_uncached()
405
394
  return self._alert_config
406
395
 
407
- async def _alert_config_uncached(self) -> G90AlertConfigFlags:
396
+ async def get_alert_config(self) -> G90AlertConfigFlags:
408
397
  """
409
- Retrieves the alert configuration flags directly from the device.
398
+ Provides alert configuration flags, retained for compatibility - using
399
+ `:attr:alert_config` and `:class:G90AlertConfig` is preferred.
410
400
 
411
401
  :return: The alerts configured
412
402
  """
413
- res = await self.command(G90Commands.GETNOTICEFLAG)
414
- return G90AlertConfig(*res).flags
403
+ return await self.alert_config.flags
415
404
 
416
405
  async def set_alert_config(self, flags: G90AlertConfigFlags) -> None:
417
406
  """
418
- Sets the alert configuration flags on the device.
407
+ Sets the alert configuration flags, retained for compatibility - using
408
+ `:attr:alert_config` and `:class:G90AlertConfig` is preferred.
419
409
  """
420
- # Use uncached method retrieving the alert configuration, to ensure the
421
- # actual value retrieved from the device
422
- alert_config = await self._alert_config_uncached()
423
- if alert_config != self._alert_config:
424
- _LOGGER.warning(
425
- 'Alert configuration changed externally,'
426
- ' overwriting (read "%s", will be set to "%s")',
427
- str(alert_config), str(flags)
428
- )
429
- await self.command(G90Commands.SETNOTICEFLAG, [flags.value])
430
- # Update the alert configuration stored
431
- self._alert_config = flags
410
+ await self.alert_config.set(flags)
432
411
 
433
412
  @property
434
413
  async def user_data_crc(self) -> G90UserDataCRC:
@@ -514,9 +493,9 @@ class G90Alarm(G90NotificationProtocol):
514
493
 
515
494
  # Determine if door close notifications are available for the given
516
495
  # sensor
517
- alert_config_flags = await self.alert_config
518
- door_close_alert_enabled = (
519
- G90AlertConfigFlags.DOOR_CLOSE in alert_config_flags)
496
+ door_close_alert_enabled = await self.alert_config.get_flag(
497
+ G90AlertConfigFlags.DOOR_CLOSE
498
+ )
520
499
  # The condition intentionally doesn't account for cord sensors of
521
500
  # subtype door, since those won't send door open/close alerts, only
522
501
  # notifications
@@ -532,7 +511,8 @@ class G90Alarm(G90NotificationProtocol):
532
511
  ' (alert config flags %s) or is a cord sensor,'
533
512
  ' closing event will be emulated upon'
534
513
  ' %s seconds',
535
- name, sensor.type, alert_config_flags,
514
+ name, sensor.type,
515
+ await self.alert_config.flags,
536
516
  self._reset_occupancy_interval)
537
517
  G90Callback.invoke_delayed(
538
518
  self._reset_occupancy_interval,
@@ -599,17 +579,12 @@ class G90Alarm(G90NotificationProtocol):
599
579
  :param state: Device state (armed, disarmed, armed home)
600
580
  """
601
581
  if self._sms_alert_when_armed:
602
- if state == G90ArmDisarmTypes.DISARM:
603
- # Disable SMS alerts from the device
604
- await self.set_alert_config(
605
- await self.alert_config & ~G90AlertConfigFlags.SMS_PUSH
606
- )
607
- if state in (G90ArmDisarmTypes.ARM_AWAY,
608
- G90ArmDisarmTypes.ARM_HOME):
609
- # Enable SMS alerts from the device
610
- await self.set_alert_config(
611
- await self.alert_config | G90AlertConfigFlags.SMS_PUSH
582
+ await self.alert_config.set_flag(
583
+ G90AlertConfigFlags.SMS_PUSH,
584
+ state in (
585
+ G90ArmDisarmTypes.ARM_AWAY, G90ArmDisarmTypes.ARM_HOME
612
586
  )
587
+ )
613
588
 
614
589
  # Reset the tampered and door open when arming flags on all sensors
615
590
  # having those set
@@ -50,7 +50,7 @@ class G90Device(G90Sensor):
50
50
  [self.index, 1, self.subindex])
51
51
 
52
52
  @property
53
- def supports_enable_disable(self) -> bool:
53
+ def supports_updates(self) -> bool:
54
54
  """
55
55
  Indicates if disabling/enabling the device (relay) is supported.
56
56
 
@@ -64,12 +64,12 @@ class G90Device(G90Sensor):
64
64
  # mostly.
65
65
  return False
66
66
 
67
- async def set_enabled(self, value: bool) -> None:
67
+ async def delete(self) -> None:
68
68
  """
69
- Changes the disabled/enabled state of the device (relay).
70
-
71
- :param value: Whether to enable or disable the device
69
+ Deletes the device (relay) from the G90 alarm panel.
72
70
  """
73
- _LOGGER.warning(
74
- 'Manipulating with enable/disable for device is unsupported'
75
- )
71
+ _LOGGER.debug("Deleting device: %s", self)
72
+ # Mark the device as unavailable
73
+ self.is_unavailable = True
74
+ # Delete the device from the alarm panel
75
+ await self.parent.command(G90Commands.DELDEVICE, [self.index])
@@ -52,7 +52,7 @@ class G90SensorCommonData: # pylint:disable=too-many-instance-attributes
52
52
  type_id: int
53
53
  subtype: int
54
54
  timeout: int
55
- user_flag_data: int
55
+ user_flags_data: int
56
56
  baudrate: int
57
57
  protocol_id: int
58
58
  reserved_data: int
@@ -117,6 +117,35 @@ class G90SensorUserFlags(IntFlag):
117
117
  )
118
118
 
119
119
 
120
+ class G90SensorAlertModes(IntEnum):
121
+ """
122
+ Dedicated alert modes for the sensors (subset of user flags).
123
+ """
124
+ ALERT_ALWAYS = 0
125
+ ALERT_WHEN_AWAY = 1
126
+ ALERT_WHEN_AWAY_AND_HOME = 2
127
+
128
+
129
+ # Mapping of relevant user flags to alert modes
130
+ ALERT_MODES_MAP_BY_FLAG = {
131
+ # No 'when away' or 'when away and home' flag set means 'alert always
132
+ G90SensorUserFlags.NONE:
133
+ G90SensorAlertModes.ALERT_ALWAYS,
134
+ G90SensorUserFlags.ALERT_WHEN_AWAY:
135
+ G90SensorAlertModes.ALERT_WHEN_AWAY,
136
+ G90SensorUserFlags.ALERT_WHEN_AWAY_AND_HOME:
137
+ G90SensorAlertModes.ALERT_WHEN_AWAY_AND_HOME,
138
+ }
139
+
140
+ # Reversed mapping of alert modes to corresponding user flags
141
+ ALERT_MODES_MAP_BY_VALUE = dict(
142
+ zip(
143
+ ALERT_MODES_MAP_BY_FLAG.values(),
144
+ ALERT_MODES_MAP_BY_FLAG.keys()
145
+ )
146
+ )
147
+
148
+
120
149
  class G90SensorProtocols(IntEnum):
121
150
  """
122
151
  Protocol types for the sensors.
@@ -363,12 +392,22 @@ class G90Sensor(G90BaseEntity): # pylint:disable=too-many-instance-attributes
363
392
 
364
393
  @property
365
394
  def user_flag(self) -> G90SensorUserFlags:
395
+ """
396
+ User flags for the sensor, retained for compatibility - please use
397
+ `:attr:user_flags` instead.
398
+
399
+ :return: User flags
400
+ """
401
+ return self.user_flags
402
+
403
+ @property
404
+ def user_flags(self) -> G90SensorUserFlags:
366
405
  """
367
406
  User flags for the sensor (disabled/enabled, arming type etc).
368
407
 
369
408
  :return: User flags
370
409
  """
371
- return G90SensorUserFlags(self._protocol_data.user_flag_data)
410
+ return G90SensorUserFlags(self._protocol_data.user_flags_data)
372
411
 
373
412
  @property
374
413
  def node_count(self) -> int:
@@ -417,6 +456,25 @@ class G90Sensor(G90BaseEntity): # pylint:disable=too-many-instance-attributes
417
456
  """
418
457
  return self._proto_idx
419
458
 
459
+ @property
460
+ def supports_updates(self) -> bool:
461
+ """
462
+ Indicates if the sensor supports updates.
463
+
464
+ :return: Support for updates
465
+ """
466
+ if not self._definition:
467
+ _LOGGER.warning(
468
+ 'Manipulating with user flags for sensor index=%s'
469
+ ' is unsupported - no sensor definition for'
470
+ ' type=%s, subtype=%s',
471
+ self.index,
472
+ self._protocol_data.type_id,
473
+ self._protocol_data.subtype
474
+ )
475
+ return False
476
+ return True
477
+
420
478
  @property
421
479
  def supports_enable_disable(self) -> bool:
422
480
  """
@@ -424,7 +482,7 @@ class G90Sensor(G90BaseEntity): # pylint:disable=too-many-instance-attributes
424
482
 
425
483
  :return: Support for enabling/disabling the sensor
426
484
  """
427
- return self._definition is not None
485
+ return self.supports_updates
428
486
 
429
487
  @property
430
488
  def protocol_data(self) -> G90SensorIncomingData:
@@ -518,16 +576,14 @@ class G90Sensor(G90BaseEntity): # pylint:disable=too-many-instance-attributes
518
576
  )
519
577
  self._door_open_when_arming = value
520
578
 
521
- @property
522
- def enabled(self) -> bool:
579
+ async def set_user_flag(self, value: G90SensorUserFlags) -> None:
523
580
  """
524
- Indicates if the sensor is enabled.
525
-
526
- :return: If sensor is enabled
581
+ Sets user flags of the sensor, retained for compatibility - please use
582
+ `:meth:set_user_flags` instead.
527
583
  """
528
- return self.user_flag & G90SensorUserFlags.ENABLED != 0
584
+ await self.set_user_flags(value)
529
585
 
530
- async def set_user_flag(self, value: G90SensorUserFlags) -> None:
586
+ async def set_user_flags(self, value: G90SensorUserFlags) -> None:
531
587
  """
532
588
  Sets user flags of the sensor.
533
589
 
@@ -535,26 +591,21 @@ class G90Sensor(G90BaseEntity): # pylint:disable=too-many-instance-attributes
535
591
  :attr:`.G90SensorUserFlags.USER_SETTABLE` will be ignored and
536
592
  preserved from existing sensor flags.
537
593
  """
594
+ if not self.supports_updates:
595
+ return
596
+
597
+ # Checking private attribute directly, since `mypy` doesn't recognize
598
+ # the check for sensor definition is done over
599
+ # `self.supports_updates` property
600
+ if not self._definition:
601
+ return
602
+
538
603
  if value & ~G90SensorUserFlags.USER_SETTABLE:
539
604
  _LOGGER.warning(
540
605
  'User flags for sensor index=%s contain non-user settable'
541
606
  ' flags, those will be ignored: %s',
542
607
  self.index, repr(value & ~G90SensorUserFlags.USER_SETTABLE)
543
608
  )
544
- # Checking private attribute directly, since `mypy` doesn't recognize
545
- # the check for sensor definition to be defined if done over
546
- # `self.supports_enable_disable` property
547
- if not self._definition:
548
- _LOGGER.warning(
549
- 'Manipulating with user flags for sensor index=%s'
550
- ' is unsupported - no sensor definition for'
551
- ' type=%s, subtype=%s',
552
- self.index,
553
- self._protocol_data.type_id,
554
- self._protocol_data.subtype
555
- )
556
-
557
- return
558
609
 
559
610
  # Refresh actual sensor data from the alarm panel before modifying it.
560
611
  # This implies the sensor is at the same position within sensor list
@@ -593,24 +644,32 @@ class G90Sensor(G90BaseEntity): # pylint:disable=too-many-instance-attributes
593
644
  )
594
645
  return
595
646
 
596
- prev_user_flag = self.user_flag
647
+ prev_user_flags = self.user_flags
597
648
 
598
649
  # Re-instantiate the protocol data with modified user flags
599
650
  _data = asdict(self._protocol_data)
600
- _data['user_flag_data'] = (
651
+ _data['user_flags_data'] = (
601
652
  # Preserve flags that are not user-settable
602
- self.user_flag & ~G90SensorUserFlags.USER_SETTABLE
653
+ self.user_flags & ~G90SensorUserFlags.USER_SETTABLE
603
654
  ) | (
604
655
  # Combine them with the new user-settable flags
605
656
  value & G90SensorUserFlags.USER_SETTABLE
606
657
  )
607
658
  self._protocol_data = self._protocol_incoming_data_kls(**_data)
608
659
 
660
+ if self.user_flags == prev_user_flags:
661
+ _LOGGER.debug(
662
+ 'Sensor index=%s: user flags %s have not changed,'
663
+ ' skipping update',
664
+ self._protocol_data.index, repr(prev_user_flags)
665
+ )
666
+ return
667
+
609
668
  _LOGGER.debug(
610
- 'Sensor index=%s: previous user_flag %s, resulting user_flag %s',
669
+ 'Sensor index=%s: previous user flags %s, resulting flags %s',
611
670
  self._protocol_data.index,
612
- repr(prev_user_flag),
613
- repr(self.user_flag)
671
+ repr(prev_user_flags),
672
+ repr(self.user_flags)
614
673
  )
615
674
 
616
675
  # Generate protocol data from write operation, deriving values either
@@ -623,7 +682,7 @@ class G90Sensor(G90BaseEntity): # pylint:disable=too-many-instance-attributes
623
682
  type_id=self._protocol_data.type_id,
624
683
  subtype=self._protocol_data.subtype,
625
684
  timeout=self._protocol_data.timeout,
626
- user_flag_data=self._protocol_data.user_flag_data,
685
+ user_flags_data=self._protocol_data.user_flags_data,
627
686
  baudrate=self._protocol_data.baudrate,
628
687
  protocol_id=self._protocol_data.protocol_id,
629
688
  reserved_data=self._definition.reserved_data,
@@ -637,20 +696,110 @@ class G90Sensor(G90BaseEntity): # pylint:disable=too-many-instance-attributes
637
696
  G90Commands.SETSINGLESENSOR, list(astuple(outgoing_data))
638
697
  )
639
698
 
640
- async def set_enabled(self, value: bool) -> None:
699
+ def get_flag(self, flag: G90SensorUserFlags) -> bool:
641
700
  """
642
- Sets the sensor enabled/disabled.
701
+ Gets the user flag for the sensor.
702
+
703
+ :param flag: User flag to get
704
+ :return: User flag value
643
705
  """
706
+ return flag in self.user_flag
644
707
 
645
- # Modify the value of the user flag setting enabled/disabled one
646
- # appropriately.
647
- user_flag = self.user_flag
648
- if value:
649
- user_flag |= G90SensorUserFlags.ENABLED
650
- else:
651
- user_flag &= ~G90SensorUserFlags.ENABLED
708
+ async def set_flag(
709
+ self, flag: G90SensorUserFlags, value: bool
710
+ ) -> None:
711
+ """
712
+ Sets the user flag for the sensor.
713
+
714
+ :param flag: User flag to set
715
+ :param value: New value for the user flag
716
+ """
717
+ # Skip updating the flag if it has the desired value
718
+ if self.get_flag(flag) == value:
719
+ _LOGGER.debug(
720
+ 'Sensor index=%s: user flag %s has not changed,'
721
+ ' skipping update',
722
+ self._protocol_data.index, repr(flag)
723
+ )
724
+ return
725
+
726
+ # Invert corresponding user flag and set it
727
+ user_flag = self.user_flag ^ flag
652
728
  await self.set_user_flag(user_flag)
653
729
 
730
+ @property
731
+ def enabled(self) -> bool:
732
+ """
733
+ Indicates if the sensor is enabled, using `:meth:get_user_flag` instead
734
+ is preferred.
735
+
736
+ :return: If sensor is enabled
737
+ """
738
+ return self.get_flag(G90SensorUserFlags.ENABLED)
739
+
740
+ async def set_enabled(self, value: bool) -> None:
741
+ """
742
+ Sets the sensor enabled/disabled, using `:meth:set_user_flag` instead
743
+ is preferred.
744
+
745
+ :param value: New the sensor should be enabled
746
+ """
747
+ await self.set_flag(G90SensorUserFlags.ENABLED, value)
748
+
749
+ @property
750
+ def alert_mode(self) -> G90SensorAlertModes:
751
+ """
752
+ Alert mode for the sensor.
753
+
754
+ :return: Alert mode
755
+ """
756
+ # Filter out irrelevant flags
757
+ mode = self.user_flag & (
758
+ G90SensorUserFlags.ALERT_WHEN_AWAY
759
+ | G90SensorUserFlags.ALERT_WHEN_AWAY_AND_HOME
760
+ )
761
+ # Map the relevant user flags to alert mode
762
+ result = ALERT_MODES_MAP_BY_FLAG.get(mode, None)
763
+
764
+ if result is None:
765
+ raise ValueError(
766
+ f"Unknown alert mode for sensor {self.name}: {mode}"
767
+ f" (user flag: {self.user_flag})"
768
+ )
769
+
770
+ return result
771
+
772
+ async def set_alert_mode(self, value: G90SensorAlertModes) -> None:
773
+ """
774
+ Sets the sensor alert mode.
775
+ """
776
+ # Skip update if the value is already set to the requested one
777
+ if self.alert_mode == value:
778
+ _LOGGER.debug(
779
+ 'Sensor index=%s: alert mode %s has not changed,'
780
+ ' skipping update',
781
+ self._protocol_data.index, repr(value)
782
+ )
783
+ return
784
+
785
+ # Map the alert mode to user flag value
786
+ result = ALERT_MODES_MAP_BY_VALUE.get(value, None)
787
+
788
+ if result is None:
789
+ raise ValueError(
790
+ f"Attempting to set alert mode for sensor {self.name} to"
791
+ f" unknown value '{value}'"
792
+ )
793
+
794
+ # Add the mapped value over the user flags, filtering out previous
795
+ # value of the alert mode
796
+ user_flags = self.user_flag & ~(
797
+ G90SensorUserFlags.ALERT_WHEN_AWAY
798
+ | G90SensorUserFlags.ALERT_WHEN_AWAY_AND_HOME
799
+ ) | result
800
+ # Set the updated user flags
801
+ await self.set_user_flags(user_flags)
802
+
654
803
  @property
655
804
  def extra_data(self) -> Any:
656
805
  """
@@ -674,6 +823,19 @@ class G90Sensor(G90BaseEntity): # pylint:disable=too-many-instance-attributes
674
823
  def is_unavailable(self, value: bool) -> None:
675
824
  self._unavailable = value
676
825
 
826
+ async def delete(self) -> None:
827
+ """
828
+ Deletes the sensor from the alarm panel.
829
+ """
830
+ _LOGGER.debug("Deleting sensor: %s", self)
831
+
832
+ # Mark the sensor as unavailable
833
+ self.is_unavailable = True
834
+ # Delete the sensor from the alarm panel
835
+ await self.parent.command(
836
+ G90Commands.DELSENSOR, [self.index]
837
+ )
838
+
677
839
  def _asdict(self) -> Dict[str, Any]:
678
840
  """
679
841
  Returns dictionary representation of the sensor.
@@ -693,8 +855,15 @@ class G90Sensor(G90BaseEntity): # pylint:disable=too-many-instance-attributes
693
855
  'user_flag': self.user_flag,
694
856
  'reserved': self.reserved,
695
857
  'extra_data': self.extra_data,
696
- 'enabled': self.enabled,
697
- 'supports_enable_disable': self.supports_enable_disable,
858
+ 'enabled': self.get_flag(G90SensorUserFlags.ENABLED),
859
+ 'detect_door': self.get_flag(G90SensorUserFlags.DETECT_DOOR),
860
+ 'door_chime': self.get_flag(G90SensorUserFlags.DOOR_CHIME),
861
+ 'independent_zone': self.get_flag(
862
+ G90SensorUserFlags.INDEPENDENT_ZONE
863
+ ),
864
+ 'arm_delay': self.get_flag(G90SensorUserFlags.ARM_DELAY),
865
+ 'alert_mode': self.alert_mode,
866
+ 'supports_updates': self.supports_updates,
698
867
  'is_wireless': self.is_wireless,
699
868
  'is_low_battery': self.is_low_battery,
700
869
  'is_tampered': self.is_tampered,