pyg90alarm 1.20.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 (29) hide show
  1. pyg90alarm/__init__.py +5 -5
  2. pyg90alarm/alarm.py +130 -34
  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/local/__init__.py +0 -0
  10. pyg90alarm/{base_cmd.py → local/base_cmd.py} +3 -6
  11. pyg90alarm/{discovery.py → local/discovery.py} +1 -1
  12. pyg90alarm/{history.py → local/history.py} +4 -2
  13. pyg90alarm/{host_status.py → local/host_status.py} +1 -1
  14. pyg90alarm/local/notifications.py +116 -0
  15. pyg90alarm/{paginated_cmd.py → local/paginated_cmd.py} +2 -2
  16. pyg90alarm/{paginated_result.py → local/paginated_result.py} +1 -1
  17. pyg90alarm/{targeted_discovery.py → local/targeted_discovery.py} +2 -2
  18. pyg90alarm/notifications/__init__.py +0 -0
  19. pyg90alarm/{device_notifications.py → notifications/base.py} +115 -173
  20. pyg90alarm/notifications/protocol.py +116 -0
  21. {pyg90alarm-1.20.0.dist-info → pyg90alarm-2.0.0.dist-info}/METADATA +101 -18
  22. pyg90alarm-2.0.0.dist-info/RECORD +40 -0
  23. {pyg90alarm-1.20.0.dist-info → pyg90alarm-2.0.0.dist-info}/WHEEL +1 -1
  24. pyg90alarm-1.20.0.dist-info/RECORD +0 -31
  25. /pyg90alarm/{config.py → local/config.py} +0 -0
  26. /pyg90alarm/{host_info.py → local/host_info.py} +0 -0
  27. /pyg90alarm/{user_data_crc.py → local/user_data_crc.py} +0 -0
  28. {pyg90alarm-1.20.0.dist-info → pyg90alarm-2.0.0.dist-info/licenses}/LICENSE +0 -0
  29. {pyg90alarm-1.20.0.dist-info → pyg90alarm-2.0.0.dist-info}/top_level.txt +0 -0
pyg90alarm/__init__.py CHANGED
@@ -23,17 +23,17 @@ Python package to control G90-based alarm systems.
23
23
  """
24
24
 
25
25
  from .alarm import G90Alarm
26
- from .base_cmd import G90BaseCommand
27
- from .paginated_result import G90PaginatedResult
28
- from .device_notifications import (
26
+ from .local.base_cmd import G90BaseCommand
27
+ from .local.paginated_result import G90PaginatedResult
28
+ from .notifications.base import (
29
29
  G90DeviceAlert,
30
30
  )
31
31
  from .entities.sensor import G90Sensor, G90SensorTypes
32
32
  from .entities.device import G90Device
33
- from .host_info import (
33
+ from .local.host_info import (
34
34
  G90HostInfo, G90HostInfoWifiStatus, G90HostInfoGsmStatus
35
35
  )
36
- from .host_status import G90HostStatus
36
+ from .local.host_status import G90HostStatus
37
37
  from .const import (
38
38
  G90MessageTypes,
39
39
  G90NotificationTypes,
pyg90alarm/alarm.py CHANGED
@@ -22,7 +22,8 @@
22
22
  """
23
23
  Provides interface to G90 alarm panel.
24
24
 
25
- .. note:: Only protocol 1.2 is supported!
25
+ .. note:: Both local protocol (referred to as 1.2) and cloud one
26
+ (mentioned as 1.1) are supported.
26
27
 
27
28
  The next example queries the device with IP address `10.10.10.250` for the
28
29
  information - the product name, protocol version, HW versions and such.
@@ -52,6 +53,7 @@ G90HostInfo(host_guid='<...>',
52
53
  from __future__ import annotations
53
54
  import asyncio
54
55
  from asyncio import Task
56
+ from datetime import datetime
55
57
  import logging
56
58
  from typing import (
57
59
  TYPE_CHECKING, Any, List, Optional, AsyncGenerator,
@@ -63,29 +65,36 @@ from .const import (
63
65
  LOCAL_TARGETED_DISCOVERY_PORT,
64
66
  LOCAL_NOTIFICATIONS_HOST,
65
67
  LOCAL_NOTIFICATIONS_PORT,
68
+ CLOUD_NOTIFICATIONS_HOST,
69
+ CLOUD_NOTIFICATIONS_PORT,
70
+ REMOTE_CLOUD_HOST,
71
+ REMOTE_CLOUD_PORT,
66
72
  G90ArmDisarmTypes,
67
73
  G90RemoteButtonStates,
68
74
  )
69
- from .base_cmd import (G90BaseCommand, G90BaseCommandData)
70
- from .paginated_result import G90PaginatedResult, G90PaginatedResponse
75
+ from .local.base_cmd import (G90BaseCommand, G90BaseCommandData)
76
+ from .local.paginated_result import G90PaginatedResult, G90PaginatedResponse
71
77
  from .entities.sensor import (G90Sensor, G90SensorTypes)
72
78
  from .entities.sensor_list import G90SensorList
73
79
  from .entities.device import G90Device
74
80
  from .entities.device_list import G90DeviceList
75
- from .device_notifications import (
76
- G90DeviceNotifications,
81
+ from .notifications.protocol import (
82
+ G90NotificationProtocol
77
83
  )
78
- from .discovery import G90Discovery, G90DiscoveredDevice
79
- from .targeted_discovery import (
84
+ from .notifications.base import G90NotificationsBase
85
+ from .local.notifications import G90LocalNotifications
86
+ from .local.discovery import G90Discovery, G90DiscoveredDevice
87
+ from .local.targeted_discovery import (
80
88
  G90TargetedDiscovery, G90DiscoveredDeviceTargeted,
81
89
  )
82
- from .host_info import G90HostInfo
83
- from .host_status import G90HostStatus
84
- from .config import (G90AlertConfig, G90AlertConfigFlags)
85
- from .history import G90History
86
- from .user_data_crc import G90UserDataCRC
90
+ from .local.host_info import G90HostInfo
91
+ from .local.host_status import G90HostStatus
92
+ from .local.config import (G90AlertConfig, G90AlertConfigFlags)
93
+ from .local.history import G90History
94
+ from .local.user_data_crc import G90UserDataCRC
87
95
  from .callback import G90Callback
88
96
  from .exceptions import G90Error, G90TimeoutError
97
+ from .cloud.notifications import G90CloudNotifications
89
98
 
90
99
  _LOGGER = logging.getLogger(__name__)
91
100
 
@@ -153,7 +162,7 @@ if TYPE_CHECKING:
153
162
 
154
163
 
155
164
  # pylint: disable=too-many-public-methods
156
- class G90Alarm(G90DeviceNotifications):
165
+ class G90Alarm(G90NotificationProtocol):
157
166
 
158
167
  """
159
168
  Allows to interact with G90 alarm panel.
@@ -169,19 +178,15 @@ class G90Alarm(G90DeviceNotifications):
169
178
  simulated to go into inactive state.
170
179
  """
171
180
  # pylint: disable=too-many-instance-attributes,too-many-arguments
172
- def __init__(self, host: str, port: int = REMOTE_PORT,
173
- reset_occupancy_interval: float = 3.0,
174
- notifications_local_host: str = LOCAL_NOTIFICATIONS_HOST,
175
- notifications_local_port: int = LOCAL_NOTIFICATIONS_PORT):
176
- super().__init__(
177
- local_host=notifications_local_host,
178
- local_port=notifications_local_port
179
- )
181
+ def __init__(
182
+ self, host: str, port: int = REMOTE_PORT,
183
+ reset_occupancy_interval: float = 3.0
184
+ ) -> None:
180
185
  self._host: str = host
181
186
  self._port: int = port
187
+ self._notifications: Optional[G90NotificationsBase] = None
182
188
  self._sensors = G90SensorList(self)
183
189
  self._devices = G90DeviceList(self)
184
- self._notifications: Optional[G90DeviceNotifications] = None
185
190
  self._sensor_cb: Optional[SensorCallback] = None
186
191
  self._armdisarm_cb: Optional[ArmDisarmCallback] = None
187
192
  self._door_open_close_cb: Optional[DoorOpenCloseCallback] = None
@@ -201,6 +206,24 @@ class G90Alarm(G90DeviceNotifications):
201
206
  self._alert_simulation_task: Optional[Task[Any]] = None
202
207
  self._alert_simulation_start_listener_back = False
203
208
 
209
+ @property
210
+ def host(self) -> str:
211
+ """
212
+ Returns the hostname or IP address of the alarm panel.
213
+
214
+ This is the address used for communication with the device.
215
+ """
216
+ return self._host
217
+
218
+ @property
219
+ def port(self) -> int:
220
+ """
221
+ Returns the UDP port number used to communicate with the alarm panel.
222
+
223
+ By default, this is set to the standard G90 protocol port.
224
+ """
225
+ return self._port
226
+
204
227
  async def command(
205
228
  self, code: G90Commands, data: Optional[G90BaseCommandData] = None
206
229
  ) -> G90BaseCommandData:
@@ -338,7 +361,8 @@ class G90Alarm(G90DeviceNotifications):
338
361
  """
339
362
  res = await self.command(G90Commands.GETHOSTINFO)
340
363
  info = G90HostInfo(*res)
341
- self.device_id = info.host_guid
364
+ if self._notifications:
365
+ self._notifications.device_id = info.host_guid
342
366
  return info
343
367
 
344
368
  @property
@@ -493,6 +517,9 @@ class G90Alarm(G90DeviceNotifications):
493
517
  alert_config_flags = await self.alert_config
494
518
  door_close_alert_enabled = (
495
519
  G90AlertConfigFlags.DOOR_CLOSE in alert_config_flags)
520
+ # The condition intentionally doesn't account for cord sensors of
521
+ # subtype door, since those won't send door open/close alerts, only
522
+ # notifications
496
523
  sensor_is_door = sensor.type == G90SensorTypes.DOOR
497
524
 
498
525
  # Alarm panel could emit door close alerts (if enabled) for sensors
@@ -502,7 +529,7 @@ class G90Alarm(G90DeviceNotifications):
502
529
  if not door_close_alert_enabled or not sensor_is_door:
503
530
  _LOGGER.debug("Sensor '%s' is not a door (type %s),"
504
531
  ' or door close alert is disabled'
505
- ' (alert config flags %s),'
532
+ ' (alert config flags %s) or is a cord sensor,'
506
533
  ' closing event will be emulated upon'
507
534
  ' %s seconds',
508
535
  name, sensor.type, alert_config_flags,
@@ -831,17 +858,19 @@ class G90Alarm(G90DeviceNotifications):
831
858
  def tamper_callback(self, value: TamperCallback) -> None:
832
859
  self._tamper_cb = value
833
860
 
834
- async def listen_device_notifications(self) -> None:
861
+ async def listen_notifications(self) -> None:
835
862
  """
836
863
  Starts internal listener for device notifications/alerts.
837
864
  """
838
- await self.listen()
865
+ if self._notifications:
866
+ await self._notifications.listen()
839
867
 
840
- def close_device_notifications(self) -> None:
868
+ async def close_notifications(self) -> None:
841
869
  """
842
870
  Closes the listener for device notifications/alerts.
843
871
  """
844
- self.close()
872
+ if self._notifications:
873
+ await self._notifications.close()
845
874
 
846
875
  async def arm_away(self) -> None:
847
876
  """
@@ -896,9 +925,12 @@ class G90Alarm(G90DeviceNotifications):
896
925
  each polling cycle
897
926
  """
898
927
  # Remember if device notifications listener has been started already
899
- self._alert_simulation_start_listener_back = self.listener_started
928
+ self._alert_simulation_start_listener_back = (
929
+ self._notifications is not None
930
+ and self._notifications.listener_started
931
+ )
900
932
  # And then stop it
901
- self.close()
933
+ await self.close_notifications()
902
934
 
903
935
  # Start the task
904
936
  self._alert_simulation_task = asyncio.create_task(
@@ -920,8 +952,11 @@ class G90Alarm(G90DeviceNotifications):
920
952
 
921
953
  # Start device notifications listener back if it was running when
922
954
  # simulated alerts have been enabled
923
- if self._alert_simulation_start_listener_back:
924
- await self.listen()
955
+ if (
956
+ self._notifications
957
+ and self._alert_simulation_start_listener_back
958
+ ):
959
+ await self._notifications.listen()
925
960
 
926
961
  async def _simulate_alerts_from_history(
927
962
  self, interval: float, history_depth: int
@@ -935,6 +970,10 @@ class G90Alarm(G90DeviceNotifications):
935
970
 
936
971
  See :meth:`.start_simulating_alerts_from_history` for the parameters.
937
972
  """
973
+ dummy_notifications = G90NotificationsBase(
974
+ protocol_factory=lambda: self
975
+ )
976
+
938
977
  last_history_ts = None
939
978
 
940
979
  _LOGGER.debug(
@@ -978,8 +1017,7 @@ class G90Alarm(G90DeviceNotifications):
978
1017
  # Send the history entry down the device notification
979
1018
  # code as alert, as if it came from the device and its
980
1019
  # notifications port
981
- self._handle_alert(
982
- (self._host, self._notifications_local_port),
1020
+ dummy_notifications.handle_alert(
983
1021
  item.as_device_alert(),
984
1022
  # Skip verifying device GUID, since history entry
985
1023
  # don't have it
@@ -1005,3 +1043,61 @@ class G90Alarm(G90DeviceNotifications):
1005
1043
 
1006
1044
  # Sleep to next iteration
1007
1045
  await asyncio.sleep(interval)
1046
+
1047
+ async def use_local_notifications(
1048
+ self, notifications_local_host: str = LOCAL_NOTIFICATIONS_HOST,
1049
+ notifications_local_port: int = LOCAL_NOTIFICATIONS_PORT
1050
+ ) -> None:
1051
+ """
1052
+ Switches to use local notifications for device alerts.
1053
+ """
1054
+ await self.close_notifications()
1055
+
1056
+ self._notifications = G90LocalNotifications(
1057
+ protocol_factory=lambda: self,
1058
+ host=self._host,
1059
+ port=self._port,
1060
+ local_host=notifications_local_host,
1061
+ local_port=notifications_local_port
1062
+ )
1063
+
1064
+ async def use_cloud_notifications(
1065
+ self, cloud_local_host: str = CLOUD_NOTIFICATIONS_HOST,
1066
+ cloud_local_port: int = CLOUD_NOTIFICATIONS_PORT,
1067
+ upstream_host: str = REMOTE_CLOUD_HOST,
1068
+ upstream_port: int = REMOTE_CLOUD_PORT,
1069
+ keep_single_connection: bool = True
1070
+ ) -> None:
1071
+ """
1072
+ Switches to use cloud notifications for device alerts.
1073
+ """
1074
+ await self.close_notifications()
1075
+
1076
+ self._notifications = G90CloudNotifications(
1077
+ protocol_factory=lambda: self,
1078
+ upstream_host=upstream_host,
1079
+ upstream_port=upstream_port,
1080
+ local_host=cloud_local_host,
1081
+ local_port=cloud_local_port,
1082
+ keep_single_connection=keep_single_connection
1083
+ )
1084
+
1085
+ @property
1086
+ def last_device_packet_time(self) -> Optional[datetime]:
1087
+ """
1088
+ Returns the time of the last packet received from the device.
1089
+ """
1090
+ if not self._notifications:
1091
+ return None
1092
+
1093
+ return self._notifications.last_device_packet_time
1094
+
1095
+ @property
1096
+ def last_upstream_packet_time(self) -> Optional[datetime]:
1097
+ """
1098
+ Returns the time of the last packet received from the upstream server.
1099
+ """
1100
+ if not self._notifications:
1101
+ return None
1102
+
1103
+ return self._notifications.last_upstream_packet_time
@@ -0,0 +1,31 @@
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
+ Cloud communication implementation for G90 alarm systems.
23
+
24
+ This module provides the necessary components to interact with G90 alarm
25
+ systems through their cloud protocol (referred to as version 1.1).
26
+ """
27
+ from .notifications import G90CloudNotifications # noqa: F401
28
+
29
+ __all__ = [
30
+ 'G90CloudNotifications',
31
+ ]
@@ -0,0 +1,56 @@
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
+ Constants used in the G90 cloud protocol implementation.
23
+
24
+ This module defines the main enumerations for direction and command types
25
+ used in the cloud protocol communication with G90 alarm systems.
26
+ """
27
+ from enum import IntEnum
28
+
29
+
30
+ class G90CloudDirection(IntEnum):
31
+ """
32
+ Defines the direction of G90 cloud protocol messages.
33
+
34
+ These values indicate whether messages are flowing from the cloud to the
35
+ device, from the device to the cloud, or are part of discovery processes.
36
+ """
37
+ UNSPECIFIED = 0
38
+ CLOUD = 32 # 0x20
39
+ DEVICE = 16 # 0x10
40
+ DEVICE_DISCOVERY = 48 # 0x30
41
+ CLOUD_DISCOVERY = 208 # 0xD0
42
+
43
+
44
+ class G90CloudCommand(IntEnum):
45
+ """
46
+ Defines the command types used in G90 cloud protocol messages.
47
+
48
+ These values identify the purpose of each cloud protocol message,
49
+ such as hello messages, notifications, commands, and status updates.
50
+ """
51
+ HELLO = 0x01
52
+ HELLO_ACK = 0x41
53
+ NOTIFICATION = 0x22
54
+ STATUS_CHANGE = 0x21
55
+ HELLO_INFO = 0x63
56
+ COMMAND = 0x29