pyg90alarm 1.15.1__py3-none-any.whl → 1.16.0__py3-none-any.whl

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.
pyg90alarm/alarm.py CHANGED
@@ -60,7 +60,8 @@ from .const import (
60
60
  G90Commands, REMOTE_PORT,
61
61
  REMOTE_TARGETED_DISCOVERY_PORT,
62
62
  LOCAL_TARGETED_DISCOVERY_PORT,
63
- NOTIFICATIONS_PORT,
63
+ LOCAL_NOTIFICATIONS_HOST,
64
+ LOCAL_NOTIFICATIONS_PORT,
64
65
  G90ArmDisarmTypes,
65
66
  )
66
67
  from .base_cmd import (G90BaseCommand, G90BaseCommandData)
@@ -140,11 +141,14 @@ class G90Alarm(G90DeviceNotifications):
140
141
  # pylint: disable=too-many-instance-attributes,too-many-arguments
141
142
  def __init__(self, host: str, port: int = REMOTE_PORT,
142
143
  reset_occupancy_interval: float = 3.0,
143
- notifications_host: str = '0.0.0.0',
144
- notifications_port: int = NOTIFICATIONS_PORT):
145
- super().__init__(host=notifications_host, port=notifications_port)
146
- self._host = host
147
- self._port = port
144
+ notifications_local_host: str = LOCAL_NOTIFICATIONS_HOST,
145
+ notifications_local_port: int = LOCAL_NOTIFICATIONS_PORT):
146
+ super().__init__(
147
+ local_host=notifications_local_host,
148
+ local_port=notifications_local_port
149
+ )
150
+ self._host: str = host
151
+ self._port: int = port
148
152
  self._sensors: List[G90Sensor] = []
149
153
  self._devices: List[G90Device] = []
150
154
  self._notifications: Optional[G90DeviceNotifications] = None
@@ -336,7 +340,9 @@ class G90Alarm(G90DeviceNotifications):
336
340
  :return: Device information
337
341
  """
338
342
  res = await self.command(G90Commands.GETHOSTINFO)
339
- return G90HostInfo(*res)
343
+ info = G90HostInfo(*res)
344
+ self.device_id = info.host_guid
345
+ return info
340
346
 
341
347
  @property
342
348
  async def host_status(self) -> G90HostStatus:
@@ -813,7 +819,7 @@ class G90Alarm(G90DeviceNotifications):
813
819
  # code as alert, as if it came from the device and its
814
820
  # notifications port
815
821
  self._handle_alert(
816
- (self._host, self._notifications_port),
822
+ (self._host, self._notifications_local_port),
817
823
  item.as_device_alert()
818
824
  )
819
825
 
pyg90alarm/base_cmd.py CHANGED
@@ -154,7 +154,12 @@ class G90BaseCommand(DatagramProtocol):
154
154
  Parses the response from the alarm panel.
155
155
  """
156
156
  _LOGGER.debug('To be decoded from wire format %s', data)
157
- self._parse(data.decode('utf-8', errors='ignore'))
157
+ try:
158
+ self._parse(data.decode('utf-8'))
159
+ except UnicodeDecodeError as exc:
160
+ raise G90Error(
161
+ 'Unable to decode response from UTF-8'
162
+ ) from exc
158
163
  return self._resp.data or []
159
164
 
160
165
  def _parse(self, data: str) -> None:
pyg90alarm/const.py CHANGED
@@ -28,7 +28,8 @@ from typing import Optional
28
28
  REMOTE_PORT = 12368
29
29
  REMOTE_TARGETED_DISCOVERY_PORT = 12900
30
30
  LOCAL_TARGETED_DISCOVERY_PORT = 12901
31
- NOTIFICATIONS_PORT = 12901
31
+ LOCAL_NOTIFICATIONS_HOST = '0.0.0.0'
32
+ LOCAL_NOTIFICATIONS_PORT = 12901
32
33
 
33
34
  CMD_PAGE_SIZE = 10
34
35
 
@@ -41,7 +41,6 @@ from .const import (
41
41
  G90AlertStates,
42
42
  )
43
43
 
44
-
45
44
  _LOGGER = logging.getLogger(__name__)
46
45
 
47
46
 
@@ -107,12 +106,29 @@ class G90DeviceAlert: # pylint: disable=too-many-instance-attributes
107
106
  class G90DeviceNotifications(DatagramProtocol):
108
107
  """
109
108
  Implements support for notifications/alerts sent by alarm panel.
109
+
110
+ There is a basic check to ensure only notifications/alerts from the correct
111
+ device are processed - the check uses the host and port of the device, and
112
+ the device ID (GUID) that is set by the ancestor class that implements the
113
+ commands (e.g. :class:`G90Alarm`). The latter to work correctly needs a
114
+ command to be performed first, one that fetches device GUID and then stores
115
+ it using :attr:`.device_id` (e.g. :meth:`G90Alarm.get_host_info`).
110
116
  """
111
- def __init__(self, port: int, host: str):
117
+ def __init__(self, local_port: int, local_host: str):
112
118
  # pylint: disable=too-many-arguments
113
119
  self._notification_transport: Optional[BaseTransport] = None
114
- self._notifications_host = host
115
- self._notifications_port = port
120
+ self._notifications_local_host = local_host
121
+ self._notifications_local_port = local_port
122
+ # Host/port of the device is configured to communicating via commands.
123
+ # Inteded to validate if notifications/alert are received from the
124
+ # correct device.
125
+ self._host: Optional[str] = None
126
+ self._port: Optional[int] = None
127
+ # Same but for device ID (GUID) - the notifications logic uses it to
128
+ # perform validation, but doesn't set it from messages received (it
129
+ # will diminish the purpose of the validation, should be done by an
130
+ # ancestor class).
131
+ self._device_id: Optional[str] = None
116
132
 
117
133
  def _handle_notification(
118
134
  self, addr: Tuple[str, int], notification: G90Notification
@@ -145,6 +161,15 @@ class G90DeviceNotifications(DatagramProtocol):
145
161
  def _handle_alert(
146
162
  self, addr: Tuple[str, int], alert: G90DeviceAlert
147
163
  ) -> None:
164
+ # Stop processing when alert is received from the device with different
165
+ # GUID
166
+ if self.device_id and alert.device_id != self.device_id:
167
+ _LOGGER.error(
168
+ "Received alert from wrong device: expected '%s', got '%s'",
169
+ self.device_id, alert.device_id
170
+ )
171
+ return
172
+
148
173
  if alert.type == G90AlertTypes.DOOR_OPEN_CLOSE:
149
174
  if alert.state in (
150
175
  G90AlertStates.DOOR_OPEN, G90AlertStates.DOOR_CLOSE
@@ -219,11 +244,23 @@ class G90DeviceNotifications(DatagramProtocol):
219
244
  def datagram_received( # pylint:disable=R0911
220
245
  self, data: bytes, addr: Tuple[str, int]
221
246
  ) -> None:
222
-
223
247
  """
224
- Invoked from datagram is received from the device.
248
+ Invoked when datagram is received from the device.
225
249
  """
226
- s_data = data.decode('utf-8')
250
+ if self._host and self._host != addr[0]:
251
+ _LOGGER.error(
252
+ "Received notification/alert from wrong host '%s',"
253
+ " expected from '%s'",
254
+ addr[0], self._host
255
+ )
256
+ return
257
+
258
+ try:
259
+ s_data = data.decode('utf-8')
260
+ except UnicodeDecodeError:
261
+ _LOGGER.error('Unable to decode device message from UTF-8')
262
+ return
263
+
227
264
  if not s_data.endswith('\0'):
228
265
  _LOGGER.error('Missing end marker in data')
229
266
  return
@@ -304,13 +341,13 @@ class G90DeviceNotifications(DatagramProtocol):
304
341
  loop = asyncio.get_event_loop()
305
342
 
306
343
  _LOGGER.debug('Creating UDP endpoint for %s:%s',
307
- self._notifications_host,
308
- self._notifications_port)
344
+ self._notifications_local_host,
345
+ self._notifications_local_port)
309
346
  (self._notification_transport,
310
347
  _protocol) = await loop.create_datagram_endpoint(
311
348
  lambda: self,
312
349
  local_addr=(
313
- self._notifications_host, self._notifications_port
350
+ self._notifications_local_host, self._notifications_local_port
314
351
  ))
315
352
 
316
353
  @property
@@ -328,3 +365,17 @@ class G90DeviceNotifications(DatagramProtocol):
328
365
  _LOGGER.debug('No longer listening for device notifications')
329
366
  self._notification_transport.close()
330
367
  self._notification_transport = None
368
+
369
+ @property
370
+ def device_id(self) -> Optional[str]:
371
+ """
372
+ The ID (GUID) of the panel being communicated with thru commands.
373
+
374
+ Available when any panel command receives it from the device
375
+ (:meth:`G90Alarm.get_host_info` currently).
376
+ """
377
+ return self._device_id
378
+
379
+ @device_id.setter
380
+ def device_id(self, device_id: str) -> None:
381
+ self._device_id = device_id
@@ -103,7 +103,12 @@ class G90TargetedDiscovery(G90BaseCommand):
103
103
  """
104
104
  try:
105
105
  _LOGGER.debug('Received from %s:%s: %s', addr[0], addr[1], data)
106
- decoded = data.decode('utf-8', errors='ignore')
106
+ try:
107
+ decoded = data.decode('utf-8')
108
+ except UnicodeDecodeError as exc:
109
+ raise G90Error(
110
+ 'Unable to decode discovery response from UTF-8'
111
+ ) from exc
107
112
  if not decoded.endswith('\0'):
108
113
  raise G90Error('Invalid discovery response')
109
114
  host_info = G90TargetedDiscoveryInfo(*decoded[:-1].split(','))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pyg90alarm
3
- Version: 1.15.1
3
+ Version: 1.16.0
4
4
  Summary: G90 Alarm system protocol
5
5
  Home-page: https://github.com/hostcc/pyg90alarm
6
6
  Author: Ilia Sotnikov
@@ -20,6 +20,7 @@ Classifier: Programming Language :: Python :: 3.9
20
20
  Classifier: Programming Language :: Python :: 3.10
21
21
  Classifier: Programming Language :: Python :: 3.11
22
22
  Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
23
24
  Classifier: Programming Language :: Python :: 3 :: Only
24
25
  Requires-Python: >=3.8, <4
25
26
  Description-Content-Type: text/x-rst
@@ -1,10 +1,10 @@
1
1
  pyg90alarm/__init__.py,sha256=5AITRm5jZSzuQaL7PS8fZZMZb4-IuGRhSqyAdfTt0Cs,2236
2
- pyg90alarm/alarm.py,sha256=9SqJHGK6wawFUZSaRo6-GkLBz_SxONNgCwGeD38MsSU,32211
3
- pyg90alarm/base_cmd.py,sha256=wu2v_RKpcPHxUW4HBDLWcUFMzPsGooY4o2Rc0LgcanU,9754
2
+ pyg90alarm/alarm.py,sha256=VssyGxYXb-r-fb8TdN2NfLaIPr1Zq8ogdGUV521bjsQ,32414
3
+ pyg90alarm/base_cmd.py,sha256=Bz7yoZ0RpkcjWARya664DKAPo3goD6BeaKtuW-hA804,9902
4
4
  pyg90alarm/callback.py,sha256=3JsD_JChmZD24OyjaCP-PxxuBDBX7myGYhkM4RN7bk4,3742
5
5
  pyg90alarm/config.py,sha256=2YtIgdT7clQXmYvkdn_fhIdS05CY8E1Yc90R8_tAmRI,1961
6
- pyg90alarm/const.py,sha256=4eJyEZpUE5XZkNL1sePKnuPloIvcjKIAJJ4g2e1cXVA,6054
7
- pyg90alarm/device_notifications.py,sha256=I5K8IsyYTe0XDJT8XiQoC8a2xY1XLjaGx4QFrEsaixg,11135
6
+ pyg90alarm/const.py,sha256=1nzx2rueRdAdv1J5nsfS-c_9a4tcM4_z5sWwCNG7tiQ,6097
7
+ pyg90alarm/device_notifications.py,sha256=QFs4FiaPkSKoQ_geU3Pngj9iPMHMHwi9Mjpzs8asDrM,13382
8
8
  pyg90alarm/discovery.py,sha256=fwyBHDCKGej06OwhpbVCHYTRU9WWkeYysAFgv3FiwqI,3575
9
9
  pyg90alarm/exceptions.py,sha256=eiOcRe7D18EIPyPFDNU9DdFgbnkwPmkiLl8lGPOhBNw,1475
10
10
  pyg90alarm/history.py,sha256=GnCVRsQNV2x9g6KngBBvvIUnTAIyXKX2nVKF_aFA0oM,7160
@@ -13,15 +13,15 @@ pyg90alarm/host_status.py,sha256=4XhuilBzB8XsXkpeWj3PAVpmDPcTnBBOYunO21Flabo,186
13
13
  pyg90alarm/paginated_cmd.py,sha256=vJ8slMS7aNLpkAxnIe25EHstusYy1bYTl1j306ps-MQ,4439
14
14
  pyg90alarm/paginated_result.py,sha256=Zs_yB9UW6VlHRBPIzOwHy8ZJ0FqCUPMB-rmfPG2BdnU,5447
15
15
  pyg90alarm/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
- pyg90alarm/targeted_discovery.py,sha256=zD-oW8u75H39sBanKX_Cjwz3_z_n__TC20doSctmy-g,5336
16
+ pyg90alarm/targeted_discovery.py,sha256=h0SijMpPQ12iBZtfh4xsaP_HIbiQRNBnXsd0-K9rjHA,5514
17
17
  pyg90alarm/user_data_crc.py,sha256=JQBOPY3RlOgVtvR55R-rM8OuKjYW-BPXQ0W4pi6CEH0,1689
18
18
  pyg90alarm/definitions/__init__.py,sha256=s0NZnkW_gMH718DJbgez28z9WA231CyszUf1O_ojUiI,68
19
19
  pyg90alarm/definitions/sensors.py,sha256=rKOu21ZpI44xk6aMh_vBjniFqnsNTc1CKwAvnv4PYLQ,19904
20
20
  pyg90alarm/entities/__init__.py,sha256=hHb6AOiC4Tz--rOWiiICMdLaZDs1Tf_xpWk_HeS_gO4,66
21
21
  pyg90alarm/entities/device.py,sha256=f_LHvKCAqTEebZ4mrRh3CpPUI7o-OvpvOfyTRCbftJs,2818
22
22
  pyg90alarm/entities/sensor.py,sha256=4r8ouAYTZB8ih8I4ncWdQOaifYsRxaC-ukY9jvnrRvk,16139
23
- pyg90alarm-1.15.1.dist-info/LICENSE,sha256=f884inRbeNv-O-hbwz62Ro_1J8xiHRTnJ2cCx6A0WvU,1070
24
- pyg90alarm-1.15.1.dist-info/METADATA,sha256=csITqi1utrvJOAqm0PtFGz9UIeDZzTX7JBWJRBjjIzY,7663
25
- pyg90alarm-1.15.1.dist-info/WHEEL,sha256=UvcQYKBHoFqaQd6LKyqHw9fxEolWLQnlzP0h_LgJAfI,91
26
- pyg90alarm-1.15.1.dist-info/top_level.txt,sha256=czHiGxYMyTk5QEDTDb0EpPiKqUMRa8zI4zx58Ii409M,11
27
- pyg90alarm-1.15.1.dist-info/RECORD,,
23
+ pyg90alarm-1.16.0.dist-info/LICENSE,sha256=f884inRbeNv-O-hbwz62Ro_1J8xiHRTnJ2cCx6A0WvU,1070
24
+ pyg90alarm-1.16.0.dist-info/METADATA,sha256=G_6wt8C5-9K8P8up1jqsBCW-RAq4EkP1tP9y6n-rdUc,7714
25
+ pyg90alarm-1.16.0.dist-info/WHEEL,sha256=OVMc5UfuAQiSplgO0_WdW7vXVGAt9Hdd6qtN4HotdyA,91
26
+ pyg90alarm-1.16.0.dist-info/top_level.txt,sha256=czHiGxYMyTk5QEDTDb0EpPiKqUMRa8zI4zx58Ii409M,11
27
+ pyg90alarm-1.16.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (74.0.0)
2
+ Generator: setuptools (75.2.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5