pyg90alarm 1.19.0__py3-none-any.whl → 2.0.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.
Files changed (34) hide show
  1. pyg90alarm/__init__.py +5 -5
  2. pyg90alarm/alarm.py +159 -114
  3. pyg90alarm/cloud/__init__.py +31 -0
  4. pyg90alarm/cloud/const.py +56 -0
  5. pyg90alarm/cloud/messages.py +593 -0
  6. pyg90alarm/cloud/notifications.py +409 -0
  7. pyg90alarm/cloud/protocol.py +518 -0
  8. pyg90alarm/const.py +5 -0
  9. pyg90alarm/entities/base_entity.py +83 -0
  10. pyg90alarm/entities/base_list.py +165 -0
  11. pyg90alarm/entities/device_list.py +58 -0
  12. pyg90alarm/entities/sensor.py +63 -3
  13. pyg90alarm/entities/sensor_list.py +50 -0
  14. pyg90alarm/local/__init__.py +0 -0
  15. pyg90alarm/{base_cmd.py → local/base_cmd.py} +3 -6
  16. pyg90alarm/{discovery.py → local/discovery.py} +1 -1
  17. pyg90alarm/{history.py → local/history.py} +4 -2
  18. pyg90alarm/{host_status.py → local/host_status.py} +1 -1
  19. pyg90alarm/local/notifications.py +116 -0
  20. pyg90alarm/{paginated_cmd.py → local/paginated_cmd.py} +2 -2
  21. pyg90alarm/{paginated_result.py → local/paginated_result.py} +1 -1
  22. pyg90alarm/{targeted_discovery.py → local/targeted_discovery.py} +2 -2
  23. pyg90alarm/notifications/__init__.py +0 -0
  24. pyg90alarm/{device_notifications.py → notifications/base.py} +115 -173
  25. pyg90alarm/notifications/protocol.py +116 -0
  26. {pyg90alarm-1.19.0.dist-info → pyg90alarm-2.0.0.dist-info}/METADATA +112 -18
  27. pyg90alarm-2.0.0.dist-info/RECORD +40 -0
  28. {pyg90alarm-1.19.0.dist-info → pyg90alarm-2.0.0.dist-info}/WHEEL +1 -1
  29. pyg90alarm-1.19.0.dist-info/RECORD +0 -27
  30. /pyg90alarm/{config.py → local/config.py} +0 -0
  31. /pyg90alarm/{host_info.py → local/host_info.py} +0 -0
  32. /pyg90alarm/{user_data_crc.py → local/user_data_crc.py} +0 -0
  33. {pyg90alarm-1.19.0.dist-info → pyg90alarm-2.0.0.dist-info/licenses}/LICENSE +0 -0
  34. {pyg90alarm-1.19.0.dist-info → pyg90alarm-2.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,165 @@
1
+ # Copyright (c) 2025 Ilia Sotnikov
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ # SOFTWARE.
20
+
21
+ """
22
+ Base entity list.
23
+ """
24
+ from abc import ABC, abstractmethod
25
+ from typing import (
26
+ List, AsyncGenerator, Optional, TypeVar, Generic, cast, TYPE_CHECKING,
27
+ )
28
+ import asyncio
29
+ import logging
30
+
31
+ from .base_entity import G90BaseEntity
32
+ if TYPE_CHECKING:
33
+ from ..alarm import G90Alarm
34
+ else:
35
+ # Alias G90Alarm to object avoid circular imports
36
+ # (`G90Alarm` -> `G90SensorList` -> `G90BaseList` -> `G90Alarm`)
37
+ G90Alarm = object
38
+
39
+ T = TypeVar('T', bound=G90BaseEntity)
40
+ _LOGGER = logging.getLogger(__name__)
41
+
42
+
43
+ class G90BaseList(Generic[T], ABC):
44
+ """
45
+ Base entity list class.
46
+
47
+ :param parent: Parent alarm panel instance.
48
+ """
49
+ def __init__(self, parent: G90Alarm) -> None:
50
+ self._entities: List[T] = []
51
+ self._lock = asyncio.Lock()
52
+ self._parent = parent
53
+
54
+ @abstractmethod
55
+ async def _fetch(self) -> AsyncGenerator[T, None]:
56
+ """
57
+ Fetch the list of entities from the panel.
58
+
59
+ :return: Async generator of entities
60
+ """
61
+ yield cast(T, None)
62
+
63
+ @property
64
+ async def entities(self) -> List[T]:
65
+ """
66
+ Return the list of entities.
67
+
68
+ :meth:`update` is called if the list is empty.
69
+
70
+ :return: List of entities
71
+ """
72
+ # Please see below for the explanation of the lock usage
73
+ async with self._lock:
74
+ entities = self._entities
75
+
76
+ if not entities:
77
+ return await self.update()
78
+
79
+ return entities
80
+
81
+ async def update(self) -> List[T]:
82
+ """
83
+ Update the list of entities from the panel.
84
+
85
+ :return: List of entities
86
+ """
87
+ # Use lock around the operation, to ensure no duplicated entries in the
88
+ # resulting list or redundant exchanges with panel are made when the
89
+ # method is called concurrently
90
+ async with self._lock:
91
+ entities = self._fetch()
92
+
93
+ non_existing_entities = self._entities.copy()
94
+ async for entity in entities:
95
+ try:
96
+ existing_entity_idx = self._entities.index(entity)
97
+ except ValueError:
98
+ existing_entity_idx = None
99
+
100
+ if existing_entity_idx is not None:
101
+ existing_entity = self._entities[existing_entity_idx]
102
+ # Update the existing entity with the new data
103
+ _LOGGER.debug(
104
+ "Updating existing entity '%s' from protocol"
105
+ " data '%s'", existing_entity, entity
106
+ )
107
+
108
+ self._entities[existing_entity_idx].update(entity)
109
+ non_existing_entities.remove(entity)
110
+ else:
111
+ # Add the new entity to the list
112
+ _LOGGER.debug('Adding new entity: %s', entity)
113
+ self._entities.append(entity)
114
+
115
+ # Mark the entities that are no longer in the list
116
+ for unavailable_entity in non_existing_entities:
117
+ _LOGGER.debug(
118
+ 'Marking entity as unavailable: %s', unavailable_entity
119
+ )
120
+ unavailable_entity.is_unavailable = True
121
+
122
+ _LOGGER.debug(
123
+ 'Total number of entities: %s, unavailable: %s',
124
+ len(self._entities), len(non_existing_entities)
125
+ )
126
+
127
+ return self._entities
128
+
129
+ async def find(
130
+ self, idx: int, name: str, exclude_unavailable: bool
131
+ ) -> Optional[T]:
132
+ """
133
+ Finds entity by index and name.
134
+
135
+ :param idx: Entity index
136
+ :param name: Entity name
137
+ :param exclude_unavailable: Exclude unavailable entities
138
+ :return: Entity instance
139
+ """
140
+ entities = await self.entities
141
+
142
+ found = None
143
+ # Fast lookup by direct index
144
+ if idx < len(entities) and entities[idx].name == name:
145
+ entity = entities[idx]
146
+ _LOGGER.debug('Found entity via fast lookup: %s', entity)
147
+ found = entity
148
+
149
+ # Fast lookup failed, perform slow one over the whole entities list
150
+ if not found:
151
+ for entity in entities:
152
+ if entity.index == idx and entity.name == name:
153
+ _LOGGER.debug('Found entity: %s', entity)
154
+ found = entity
155
+
156
+ if found:
157
+ if not exclude_unavailable or not found.is_unavailable:
158
+ return found
159
+
160
+ _LOGGER.debug(
161
+ 'Entity is found but unavailable, will result in none returned'
162
+ )
163
+
164
+ _LOGGER.error('Entity not found: idx=%s, name=%s', idx, name)
165
+ return None
@@ -0,0 +1,58 @@
1
+ # Copyright (c) 2025 Ilia Sotnikov
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ # SOFTWARE.
20
+ """
21
+ Device list.
22
+ """
23
+ from typing import AsyncGenerator
24
+ from .device import G90Device
25
+ from .base_list import G90BaseList
26
+ from ..const import G90Commands
27
+
28
+
29
+ class G90DeviceList(G90BaseList[G90Device]):
30
+ """
31
+ Device list class.
32
+ """
33
+ async def _fetch(self) -> AsyncGenerator[G90Device, None]:
34
+ """
35
+ Fetch the list of devices from the panel.
36
+
37
+ :yields: G90Device: Device entity.
38
+ """
39
+ devices = self._parent.paginated_result(
40
+ G90Commands.GETDEVICELIST
41
+ )
42
+
43
+ async for device in devices:
44
+ obj = G90Device(
45
+ *device.data, parent=self._parent, subindex=0,
46
+ proto_idx=device.proto_idx
47
+ )
48
+
49
+ yield obj
50
+
51
+ # Multi-node devices (first node has already been handled
52
+ # above)
53
+ for node in range(1, obj.node_count):
54
+ obj = G90Device(
55
+ *device.data, parent=self._parent,
56
+ subindex=node, proto_idx=device.proto_idx
57
+ )
58
+ yield obj
@@ -31,6 +31,7 @@ from typing import (
31
31
  from enum import IntEnum, IntFlag
32
32
  from ..definitions.sensors import SENSOR_DEFINITIONS, SensorDefinition
33
33
  from ..const import G90Commands
34
+ from .base_entity import G90BaseEntity
34
35
  if TYPE_CHECKING:
35
36
  from ..alarm import (
36
37
  G90Alarm, SensorStateCallback, SensorLowBatteryCallback,
@@ -171,7 +172,7 @@ _LOGGER = logging.getLogger(__name__)
171
172
 
172
173
 
173
174
  # pylint: disable=too-many-public-methods
174
- class G90Sensor: # pylint:disable=too-many-instance-attributes
175
+ class G90Sensor(G90BaseEntity): # pylint:disable=too-many-instance-attributes
175
176
  """
176
177
  Interacts with sensor on G90 alarm panel.
177
178
 
@@ -208,6 +209,7 @@ class G90Sensor: # pylint:disable=too-many-instance-attributes
208
209
  self._door_open_when_arming = False
209
210
  self._proto_idx = proto_idx
210
211
  self._extra_data: Any = None
212
+ self._unavailable = False
211
213
 
212
214
  self._definition: Optional[SensorDefinition] = None
213
215
  # Get sensor definition corresponds to the sensor type/subtype if any
@@ -219,6 +221,15 @@ class G90Sensor: # pylint:disable=too-many-instance-attributes
219
221
  self._definition = s_def
220
222
  break
221
223
 
224
+ def update(self, obj: G90Sensor) -> None:
225
+ """
226
+ Updates sensor from another instance.
227
+
228
+ :param obj: Sensor instance to update from
229
+ """
230
+ self._protocol_data = obj.protocol_data
231
+ self._proto_idx = obj.proto_idx
232
+
222
233
  @property
223
234
  def name(self) -> str:
224
235
  """
@@ -396,6 +407,16 @@ class G90Sensor: # pylint:disable=too-many-instance-attributes
396
407
  """
397
408
  return self._subindex
398
409
 
410
+ @property
411
+ def proto_idx(self) -> int:
412
+ """
413
+ Index of the sensor within list of sensors as retrieved from the alarm
414
+ panel.
415
+
416
+ :return: Index of sensor in list of sensors.
417
+ """
418
+ return self._proto_idx
419
+
399
420
  @property
400
421
  def supports_enable_disable(self) -> bool:
401
422
  """
@@ -405,6 +426,15 @@ class G90Sensor: # pylint:disable=too-many-instance-attributes
405
426
  """
406
427
  return self._definition is not None
407
428
 
429
+ @property
430
+ def protocol_data(self) -> G90SensorIncomingData:
431
+ """
432
+ Protocol data of the sensor.
433
+
434
+ :return: Protocol data
435
+ """
436
+ return self._protocol_data
437
+
408
438
  @property
409
439
  def is_wireless(self) -> bool:
410
440
  """
@@ -532,11 +562,11 @@ class G90Sensor: # pylint:disable=too-many-instance-attributes
532
562
  # when instantiated.
533
563
  _LOGGER.debug(
534
564
  'Refreshing sensor at index=%s, position in protocol list=%s',
535
- self.index, self._proto_idx
565
+ self.index, self.proto_idx
536
566
  )
537
567
  sensors_result = self.parent.paginated_result(
538
568
  G90Commands.GETSENSORLIST,
539
- start=self._proto_idx, end=self._proto_idx
569
+ start=self.proto_idx, end=self.proto_idx
540
570
  )
541
571
  sensors = [x.data async for x in sensors_result]
542
572
 
@@ -633,6 +663,17 @@ class G90Sensor: # pylint:disable=too-many-instance-attributes
633
663
  def extra_data(self, val: Any) -> None:
634
664
  self._extra_data = val
635
665
 
666
+ @property
667
+ def is_unavailable(self) -> bool:
668
+ """
669
+ Indicates if the sensor is unavailable (e.g. has been removed).
670
+ """
671
+ return self._unavailable
672
+
673
+ @is_unavailable.setter
674
+ def is_unavailable(self, value: bool) -> None:
675
+ self._unavailable = value
676
+
636
677
  def _asdict(self) -> Dict[str, Any]:
637
678
  """
638
679
  Returns dictionary representation of the sensor.
@@ -644,6 +685,7 @@ class G90Sensor: # pylint:disable=too-many-instance-attributes
644
685
  'type': self.type,
645
686
  'subtype': self.subtype,
646
687
  'index': self.index,
688
+ 'protocol_index': self.proto_idx,
647
689
  'subindex': self.subindex,
648
690
  'node_count': self.node_count,
649
691
  'protocol': self.protocol,
@@ -657,6 +699,7 @@ class G90Sensor: # pylint:disable=too-many-instance-attributes
657
699
  'is_low_battery': self.is_low_battery,
658
700
  'is_tampered': self.is_tampered,
659
701
  'is_door_open_when_arming': self.is_door_open_when_arming,
702
+ 'is_unavailable': self.is_unavailable,
660
703
  }
661
704
 
662
705
  def __repr__(self) -> str:
@@ -666,3 +709,20 @@ class G90Sensor: # pylint:disable=too-many-instance-attributes
666
709
  :return: String representation
667
710
  """
668
711
  return super().__repr__() + f'({repr(self._asdict())})'
712
+
713
+ def __eq__(self, value: object) -> bool:
714
+ """
715
+ Compares the sensor with another object.
716
+
717
+ :param value: Object to compare with
718
+ :return: If the sensor is equal to the object
719
+ """
720
+ if not isinstance(value, G90Sensor):
721
+ return False
722
+
723
+ return (
724
+ self.type == value.type
725
+ and self.subtype == value.subtype
726
+ and self.index == value.index
727
+ and self.name == value.name
728
+ )
@@ -0,0 +1,50 @@
1
+ # Copyright (c) 2025 Ilia Sotnikov
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ # SOFTWARE.
20
+ """
21
+ Sensor list.
22
+ """
23
+ from typing import (
24
+ AsyncGenerator
25
+ )
26
+
27
+ from .sensor import G90Sensor
28
+ from .base_list import G90BaseList
29
+ from ..const import G90Commands
30
+
31
+
32
+ class G90SensorList(G90BaseList[G90Sensor]):
33
+ """
34
+ Sensor list class.
35
+ """
36
+ async def _fetch(self) -> AsyncGenerator[G90Sensor, None]:
37
+ """
38
+ Fetch the list of sensors from the panel.
39
+
40
+ :yields: G90Sensor: Sensor entity.
41
+ """
42
+ entities = self._parent.paginated_result(
43
+ G90Commands.GETSENSORLIST
44
+ )
45
+
46
+ async for entity in entities:
47
+ yield G90Sensor(
48
+ *entity.data, parent=self._parent, subindex=0,
49
+ proto_idx=entity.proto_idx
50
+ )
File without changes
@@ -30,8 +30,8 @@ from asyncio.protocols import DatagramProtocol
30
30
  from asyncio.transports import DatagramTransport, BaseTransport
31
31
  from typing import Optional, Tuple, List, Any
32
32
  from dataclasses import dataclass
33
- from .exceptions import (G90Error, G90TimeoutError)
34
- from .const import G90Commands
33
+ from ..exceptions import (G90Error, G90TimeoutError)
34
+ from ..const import G90Commands
35
35
 
36
36
 
37
37
  _LOGGER = logging.getLogger(__name__)
@@ -121,10 +121,7 @@ class G90BaseCommand(DatagramProtocol):
121
121
  """
122
122
  Creates UDP connection to the alarm panel.
123
123
  """
124
- try:
125
- loop = asyncio.get_running_loop()
126
- except AttributeError:
127
- loop = asyncio.get_event_loop()
124
+ loop = asyncio.get_running_loop()
128
125
 
129
126
  _LOGGER.debug('Creating UDP endpoint for %s:%s',
130
127
  self.host, self.port)
@@ -29,7 +29,7 @@ import logging
29
29
 
30
30
  from .base_cmd import G90BaseCommand
31
31
  from .host_info import G90HostInfo
32
- from .const import G90Commands
32
+ from ..const import G90Commands
33
33
 
34
34
  _LOGGER = logging.getLogger(__name__)
35
35
 
@@ -26,7 +26,7 @@ import logging
26
26
  from typing import Any, Optional, Dict
27
27
  from dataclasses import dataclass
28
28
  from datetime import datetime, timezone
29
- from .const import (
29
+ from ..const import (
30
30
  G90AlertTypes,
31
31
  G90AlertSources,
32
32
  G90AlertStates,
@@ -34,7 +34,7 @@ from .const import (
34
34
  G90HistoryStates,
35
35
  G90RemoteButtonStates,
36
36
  )
37
- from .device_notifications import G90DeviceAlert
37
+ from ..notifications.base import G90DeviceAlert
38
38
 
39
39
  _LOGGER = logging.getLogger(__name__)
40
40
 
@@ -52,6 +52,8 @@ states_mapping_alerts = {
52
52
  G90HistoryStates.TAMPER,
53
53
  G90AlertStates.LOW_BATTERY:
54
54
  G90HistoryStates.LOW_BATTERY,
55
+ G90AlertStates.ALARM:
56
+ G90HistoryStates.ALARM,
55
57
  }
56
58
 
57
59
  states_mapping_state_changes = {
@@ -24,7 +24,7 @@ Protocol entity for G90 alarm panel status.
24
24
  from __future__ import annotations
25
25
  from typing import Any, Dict
26
26
  from dataclasses import dataclass, asdict
27
- from .const import G90ArmDisarmTypes
27
+ from ..const import G90ArmDisarmTypes
28
28
 
29
29
 
30
30
  @dataclass
@@ -0,0 +1,116 @@
1
+ # Copyright (c) 2021 Ilia Sotnikov
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ # SOFTWARE.
20
+
21
+ """
22
+ Implements support for notifications/alerts sent by G90 alarm panel.
23
+ """
24
+ import logging
25
+ from typing import (
26
+ Optional, Tuple, Callable
27
+ )
28
+ import asyncio
29
+ from asyncio.transports import BaseTransport
30
+ from asyncio.protocols import DatagramProtocol
31
+
32
+ from ..notifications.base import G90NotificationsBase
33
+ from ..notifications.protocol import G90NotificationProtocol
34
+
35
+ _LOGGER = logging.getLogger(__name__)
36
+
37
+
38
+ class G90LocalNotifications(G90NotificationsBase, DatagramProtocol):
39
+ """
40
+ Implements support for notifications/alerts sent by alarm panel.
41
+
42
+ There is a basic check to ensure only notifications/alerts from the correct
43
+ device are processed - the check uses the host and port of the device, and
44
+ the device ID (GUID) that is set by the ancestor class that implements the
45
+ commands (e.g. :class:`G90Alarm`). The latter to work correctly needs a
46
+ command to be performed first, one that fetches device GUID and then stores
47
+ it using :attr:`.device_id` (e.g. :meth:`G90Alarm.get_host_info`).
48
+
49
+ :param protocol_factory: A callable that returns a new instance of the
50
+ :class:`G90NotificationProtocol` class.
51
+ :param port: The port on which the device is listening for notifications.
52
+ :param host: The host on which the device is listening for notifications.
53
+ :param local_port: The port on which the local host is listening for
54
+ notifications.
55
+ :param local_host: The host on which the local host is listening for
56
+ notifications.
57
+ """
58
+ def __init__( # pylint:disable=too-many-arguments
59
+ self, protocol_factory: Callable[[], G90NotificationProtocol],
60
+ port: int, host: str, local_port: int, local_host: str,
61
+ ):
62
+ super().__init__(protocol_factory)
63
+
64
+ self._host = host
65
+ self._port = port
66
+ self._notifications_local_host = local_host
67
+ self._notifications_local_port = local_port
68
+
69
+ # Implementation of datagram protocol,
70
+ # https://docs.python.org/3/library/asyncio-protocol.html#datagram-protocols
71
+ def connection_made(self, transport: BaseTransport) -> None:
72
+ """
73
+ Invoked when connection from the device is made.
74
+ """
75
+
76
+ def connection_lost(self, exc: Optional[Exception]) -> None:
77
+ """
78
+ Same but when the connection is lost.
79
+ """
80
+
81
+ def datagram_received(
82
+ self, data: bytes, addr: Tuple[str, int]
83
+ ) -> None:
84
+ """
85
+ Invoked when datagram is received from the device.
86
+ """
87
+ if self._host and self._host != addr[0]:
88
+ _LOGGER.error(
89
+ "Received notification/alert from wrong host '%s',"
90
+ " expected from '%s'",
91
+ addr[0], self._host
92
+ )
93
+ return
94
+
95
+ self.set_last_device_packet_time()
96
+
97
+ _LOGGER.debug('Received device message from %s:%s: %s',
98
+ addr[0], addr[1], data)
99
+
100
+ self.handle(data)
101
+
102
+ async def listen(self) -> None:
103
+ """
104
+ Listens for notifications/alerts from the device.
105
+ """
106
+ loop = asyncio.get_running_loop()
107
+
108
+ _LOGGER.debug('Creating UDP endpoint for %s:%s',
109
+ self._notifications_local_host,
110
+ self._notifications_local_port)
111
+ (self._transport,
112
+ _protocol) = await loop.create_datagram_endpoint(
113
+ lambda: self,
114
+ local_addr=(
115
+ self._notifications_local_host, self._notifications_local_port
116
+ ))
@@ -26,8 +26,8 @@ import logging
26
26
  from typing import Any, cast
27
27
  from dataclasses import dataclass
28
28
  from .base_cmd import G90BaseCommand, G90BaseCommandData
29
- from .exceptions import G90Error
30
- from .const import G90Commands
29
+ from ..exceptions import G90Error
30
+ from ..const import G90Commands
31
31
 
32
32
  _LOGGER = logging.getLogger(__name__)
33
33
 
@@ -27,7 +27,7 @@ import logging
27
27
  from typing import Any, Optional, AsyncGenerator, Iterable, cast
28
28
  from dataclasses import dataclass
29
29
  from .paginated_cmd import G90PaginatedCommand
30
- from .const import (
30
+ from ..const import (
31
31
  G90Commands,
32
32
  CMD_PAGE_SIZE,
33
33
  )
@@ -28,8 +28,8 @@ from dataclasses import dataclass, asdict
28
28
  import asyncio
29
29
  from asyncio.transports import BaseTransport
30
30
  from .base_cmd import G90BaseCommand
31
- from .const import G90Commands
32
- from .exceptions import G90Error
31
+ from ..const import G90Commands
32
+ from ..exceptions import G90Error
33
33
 
34
34
  _LOGGER = logging.getLogger(__name__)
35
35
 
File without changes