pyg90alarm 1.15.1__py3-none-any.whl → 1.16.1__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
 
@@ -165,7 +166,10 @@ class G90NotificationTypes(IntEnum):
165
166
  Defines types of notifications sent by the alarm panel.
166
167
  """
167
168
  ARM_DISARM = 1
169
+ SENSOR_ADDED = 4
168
170
  SENSOR_ACTIVITY = 5
171
+ DOOR_OPEN_WHEN_ARMING = 6
172
+ FIRMWARE_UPDATING = 8
169
173
 
170
174
 
171
175
  class G90ArmDisarmTypes(IntEnum):
@@ -194,7 +198,10 @@ class G90AlertSources(IntEnum):
194
198
  """
195
199
  DEVICE = 0
196
200
  SENSOR = 1
201
+ REMOTE = 10
202
+ RFID = 11
197
203
  DOORBELL = 12
204
+ FINGERPRINT = 15
198
205
 
199
206
 
200
207
  class G90AlertStates(IntEnum):
@@ -203,6 +210,7 @@ class G90AlertStates(IntEnum):
203
210
  """
204
211
  DOOR_CLOSE = 0
205
212
  DOOR_OPEN = 1
213
+ SOS = 2
206
214
  TAMPER = 3
207
215
  LOW_BATTERY = 4
208
216
 
@@ -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
pyg90alarm/history.py CHANGED
@@ -21,6 +21,8 @@
21
21
  """
22
22
  History protocol entity.
23
23
  """
24
+ import logging
25
+
24
26
  from typing import Any, Optional, Dict
25
27
  from dataclasses import dataclass
26
28
  from datetime import datetime, timezone
@@ -33,12 +35,13 @@ from .const import (
33
35
  )
34
36
  from .device_notifications import G90DeviceAlert
35
37
 
38
+ _LOGGER = logging.getLogger(__name__)
39
+
36
40
 
37
41
  # The state of the incoming history entries are mixed of `G90AlertStates` and
38
- # `G90AlertStateChangeTypes`, depending on entry type - the mapping
39
- # consilidates them into unified `G90HistoryStates`. The latter enum can't be
40
- # just an union of former two, since those have conflicting values
41
- states_mapping = {
42
+ # `G90AlertStateChangeTypes`, depending on entry type - hence two separate
43
+ # dictionaries, since enums used for keys have conflicting values
44
+ states_mapping_alerts = {
42
45
  G90AlertStates.DOOR_CLOSE:
43
46
  G90HistoryStates.DOOR_CLOSE,
44
47
  G90AlertStates.DOOR_OPEN:
@@ -47,6 +50,9 @@ states_mapping = {
47
50
  G90HistoryStates.TAMPER,
48
51
  G90AlertStates.LOW_BATTERY:
49
52
  G90HistoryStates.LOW_BATTERY,
53
+ }
54
+
55
+ states_mapping_state_changes = {
50
56
  G90AlertStateChangeTypes.AC_POWER_FAILURE:
51
57
  G90HistoryStates.AC_POWER_FAILURE,
52
58
  G90AlertStateChangeTypes.AC_POWER_RECOVER:
@@ -87,6 +93,7 @@ class G90History:
87
93
  Represents a history entry from the alarm panel.
88
94
  """
89
95
  def __init__(self, *args: Any, **kwargs: Any):
96
+ self._raw_data = args
90
97
  self._protocol_data = ProtocolData(*args, **kwargs)
91
98
 
92
99
  @property
@@ -99,55 +106,79 @@ class G90History:
99
106
  )
100
107
 
101
108
  @property
102
- def type(self) -> G90AlertTypes:
109
+ def type(self) -> Optional[G90AlertTypes]:
103
110
  """
104
111
  Type of the history entry.
105
112
  """
106
- return G90AlertTypes(self._protocol_data.type)
113
+ try:
114
+ return G90AlertTypes(self._protocol_data.type)
115
+ except ValueError:
116
+ _LOGGER.warning(
117
+ "Can't interpret '%s' as alert type (decoded protocol"
118
+ " data '%s', raw data '%s')",
119
+ self._protocol_data.type, self._protocol_data, self._raw_data
120
+ )
121
+ return None
107
122
 
108
123
  @property
109
- def state(self) -> G90HistoryStates:
124
+ def state(self) -> Optional[G90HistoryStates]:
110
125
  """
111
126
  State for the history entry.
112
127
  """
113
- # Door open/close type, mapped against `G90AlertStates` using `state`
114
- # incoming field
115
- if self.type == G90AlertTypes.DOOR_OPEN_CLOSE:
116
- return G90HistoryStates(
117
- states_mapping[G90AlertStates(self._protocol_data.state)]
128
+ try:
129
+ # Door open/close or alert types, mapped against `G90AlertStates`
130
+ # using `state` incoming field
131
+ if self.type in [
132
+ G90AlertTypes.DOOR_OPEN_CLOSE, G90AlertTypes.ALARM
133
+ ]:
134
+ return G90HistoryStates(
135
+ states_mapping_alerts[
136
+ G90AlertStates(self._protocol_data.state)
137
+ ]
138
+ )
139
+ except ValueError:
140
+ _LOGGER.warning(
141
+ "Can't interpret '%s' as alert state (decoded protocol"
142
+ " data '%s', raw data '%s')",
143
+ self._protocol_data.state, self._protocol_data, self._raw_data
118
144
  )
145
+ return None
119
146
 
120
- # Device state change, mapped against `G90AlertStateChangeTypes` using
121
- # `event_id` incoming field
122
- if self.type == G90AlertTypes.STATE_CHANGE:
147
+ try:
148
+ # Other types are mapped against `G90AlertStateChangeTypes`
123
149
  return G90HistoryStates(
124
- states_mapping[
150
+ states_mapping_state_changes[
125
151
  G90AlertStateChangeTypes(self._protocol_data.event_id)
126
152
  ]
127
153
  )
128
-
129
- # Alarm gets mapped to its counterpart in `G90HistoryStates`
130
- if self.type == G90AlertTypes.ALARM:
131
- return G90HistoryStates.ALARM
132
-
133
- # Other types are mapped against `G90AlertStateChangeTypes`
134
- return G90HistoryStates(
135
- states_mapping[
136
- G90AlertStateChangeTypes(self._protocol_data.event_id)
137
- ]
138
- )
154
+ except ValueError:
155
+ _LOGGER.warning(
156
+ "Can't interpret '%s' as state change (decoded protocol"
157
+ " data '%s', raw data '%s')",
158
+ self._protocol_data.event_id, self._protocol_data,
159
+ self._raw_data
160
+ )
161
+ return None
139
162
 
140
163
  @property
141
- def source(self) -> G90AlertSources:
164
+ def source(self) -> Optional[G90AlertSources]:
142
165
  """
143
166
  Source of the history entry.
144
167
  """
145
- # Device state changes or open/close events are mapped against
146
- # `G90AlertSources` using `source` incoming field
147
- if self.type in [
148
- G90AlertTypes.STATE_CHANGE, G90AlertTypes.DOOR_OPEN_CLOSE
149
- ]:
150
- return G90AlertSources(self._protocol_data.source)
168
+ try:
169
+ # Device state changes or open/close events are mapped against
170
+ # `G90AlertSources` using `source` incoming field
171
+ if self.type in [
172
+ G90AlertTypes.STATE_CHANGE, G90AlertTypes.DOOR_OPEN_CLOSE
173
+ ]:
174
+ return G90AlertSources(self._protocol_data.source)
175
+ except ValueError:
176
+ _LOGGER.warning(
177
+ "Can't interpret '%s' as alert source (decoded protocol"
178
+ " data '%s', raw data '%s')",
179
+ self._protocol_data.source, self._protocol_data, self._raw_data
180
+ )
181
+ return None
151
182
 
152
183
  # Alarm will have `SENSOR` as the source, since that is likely what
153
184
  # triggered it
@@ -182,6 +213,7 @@ class G90History:
182
213
  Returns the history entry represented as device alert structure,
183
214
  suitable for :meth:`G90DeviceNotifications._handle_alert`.
184
215
  """
216
+
185
217
  return G90DeviceAlert(
186
218
  type=self._protocol_data.type,
187
219
  event_id=self._protocol_data.event_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.1
4
4
  Summary: G90 Alarm system protocol
5
5
  Home-page: https://github.com/hostcc/pyg90alarm
6
6
  Author: Ilia Sotnikov
@@ -20,18 +20,19 @@ 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
26
27
  License-File: LICENSE
27
28
  Provides-Extra: dev
28
29
  Requires-Dist: check-manifest; extra == "dev"
29
- Provides-Extra: docs
30
- Requires-Dist: sphinx; extra == "docs"
31
- Requires-Dist: sphinx-rtd-theme; extra == "docs"
32
30
  Provides-Extra: test
33
31
  Requires-Dist: coverage; extra == "test"
34
32
  Requires-Dist: asynctest; extra == "test"
33
+ Provides-Extra: docs
34
+ Requires-Dist: sphinx; extra == "docs"
35
+ Requires-Dist: sphinx-rtd-theme; extra == "docs"
35
36
 
36
37
  .. image:: https://github.com/hostcc/pyg90alarm/actions/workflows/main.yml/badge.svg?branch=master
37
38
  :target: https://github.com/hostcc/pyg90alarm/tree/master
@@ -1,27 +1,27 @@
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=uq4-vQvbhCkddQKO8C38tQzkjHm-l-TacxNlLdhQZyg,6237
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
- pyg90alarm/history.py,sha256=GnCVRsQNV2x9g6KngBBvvIUnTAIyXKX2nVKF_aFA0oM,7160
10
+ pyg90alarm/history.py,sha256=M70BCl7ykX3FFPY1htZofreYWUCviePRB2CIKVgL-ZI,8188
11
11
  pyg90alarm/host_info.py,sha256=4lFIaFEpYd3EvgNrDJmKijTrzX9i29nFISLLlXGnkmE,2759
12
12
  pyg90alarm/host_status.py,sha256=4XhuilBzB8XsXkpeWj3PAVpmDPcTnBBOYunO21Flabo,1862
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.1.dist-info/LICENSE,sha256=f884inRbeNv-O-hbwz62Ro_1J8xiHRTnJ2cCx6A0WvU,1070
24
+ pyg90alarm-1.16.1.dist-info/METADATA,sha256=iOhBwSXDDvBDAGAWHJvwygQBm2GiWUQ5sfzNzwx4x_g,7714
25
+ pyg90alarm-1.16.1.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
26
+ pyg90alarm-1.16.1.dist-info/top_level.txt,sha256=czHiGxYMyTk5QEDTDb0EpPiKqUMRa8zI4zx58Ii409M,11
27
+ pyg90alarm-1.16.1.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (74.0.0)
2
+ Generator: setuptools (75.6.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5