pyg90alarm 2.3.0__py3-none-any.whl → 2.4.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.
@@ -0,0 +1,190 @@
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
+ Represents various configuration aspects of the alarm panel.
23
+ """
24
+ from __future__ import annotations
25
+ from typing import TYPE_CHECKING, Optional
26
+ import logging
27
+ from dataclasses import dataclass
28
+ from enum import IntFlag
29
+
30
+ from pyg90alarm.exceptions import G90Error
31
+ from ..const import G90Commands
32
+ if TYPE_CHECKING:
33
+ from ..alarm import G90Alarm
34
+
35
+
36
+ class G90AlertConfigFlags(IntFlag):
37
+ """
38
+ Alert configuration flags, used bitwise
39
+ """
40
+ AC_POWER_FAILURE = 1
41
+ AC_POWER_RECOVER = 2
42
+ ARM_DISARM = 4
43
+ HOST_LOW_VOLTAGE = 8
44
+ SENSOR_LOW_VOLTAGE = 16
45
+ WIFI_AVAILABLE = 32
46
+ WIFI_UNAVAILABLE = 64
47
+ DOOR_OPEN = 128
48
+ DOOR_CLOSE = 256
49
+ SMS_PUSH = 512
50
+ UNKNOWN1 = 2048
51
+ UNKNOWN2 = 8192
52
+
53
+
54
+ _LOGGER = logging.getLogger(__name__)
55
+
56
+
57
+ @dataclass
58
+ class G90AlertConfigData:
59
+ """
60
+ Represents alert configuration data as received from the alarm panel.
61
+ """
62
+ flags_data: int
63
+
64
+ @property
65
+ def flags(self) -> G90AlertConfigFlags:
66
+ """
67
+ :return: The alert configuration flags
68
+ """
69
+ return G90AlertConfigFlags(self.flags_data)
70
+
71
+ @flags.setter
72
+ def flags(self, value: G90AlertConfigFlags) -> None:
73
+ """
74
+ :param value: The alert configuration flags
75
+ """
76
+ self.flags_data = value.value
77
+
78
+
79
+ class G90AlertConfig:
80
+ """
81
+ Represents alert configuration as received from the alarm panel.
82
+ """
83
+ def __init__(self, parent: G90Alarm) -> None:
84
+ self.parent = parent
85
+ self._cached_data: Optional[G90AlertConfigData] = None
86
+
87
+ async def _get(self) -> G90AlertConfigData:
88
+ """
89
+ Retrieves the alert configuration flags directly from the device.
90
+
91
+ :return: The alerts configured
92
+ """
93
+ _LOGGER.debug('Retrieving alert configuration from the device')
94
+ res = await self.parent.command(G90Commands.GETNOTICEFLAG)
95
+ data = G90AlertConfigData(*res)
96
+ _LOGGER.debug(
97
+ 'Alert configuration: %s, flags: %s', data,
98
+ repr(data.flags)
99
+ )
100
+
101
+ # Cache the retrieved data for `flags_with_fallback` property
102
+ self._cached_data = data
103
+
104
+ return data
105
+
106
+ async def set(self, flags: G90AlertConfigFlags) -> None:
107
+ """
108
+ .. deprecated:: 2.3.0
109
+
110
+ This method is deprecated and will always raise a RuntimeError.
111
+ Please use :meth:`set_flag` to set individual flags.
112
+ """
113
+ raise RuntimeError(
114
+ 'The set() method is deprecated. Please use set_flag() to set'
115
+ ' individual flags instead.'
116
+ )
117
+
118
+ async def _set(self, flags: G90AlertConfigFlags) -> None:
119
+ """
120
+ Sets the alert configuration flags on the device.
121
+ """
122
+ _LOGGER.debug('Setting alert configuration to %s', repr(flags))
123
+ await self.parent.command(G90Commands.SETNOTICEFLAG, [flags.value])
124
+
125
+ async def get_flag(self, flag: G90AlertConfigFlags) -> bool:
126
+ """
127
+ :param flag: The flag to check
128
+ """
129
+ return flag in await self.flags
130
+
131
+ async def set_flag(self, flag: G90AlertConfigFlags, value: bool) -> None:
132
+ """
133
+ Sets the given flag to the desired value.
134
+
135
+ Uses read-modify-write approach.
136
+
137
+ :param flag: The flag to set
138
+ :param value: The value to set
139
+ """
140
+ # Retrieve current flags
141
+ current_flags = await self.flags
142
+ # Skip updating the flag if it has the desired value
143
+ if (flag in current_flags) == value:
144
+ _LOGGER.debug(
145
+ 'Flag %s already set to %s, skipping update',
146
+ repr(flag), value
147
+ )
148
+ return
149
+
150
+ # Set or reset corresponding user flag depending on desired value
151
+ if value:
152
+ current_flags |= flag
153
+ else:
154
+ current_flags &= ~flag
155
+
156
+ # Set the updated flags
157
+ await self._set(current_flags)
158
+
159
+ @property
160
+ async def flags(self) -> G90AlertConfigFlags:
161
+ """
162
+ :return: Symbolic names for corresponding flag bits
163
+ """
164
+ return (await self._get()).flags
165
+
166
+ @property
167
+ async def flags_with_fallback(self) -> Optional[G90AlertConfigFlags]:
168
+ """
169
+ :return: Symbolic names for corresponding flag bits, falling back to
170
+ cached data if device communication fails
171
+ """
172
+ result = None
173
+
174
+ try:
175
+ result = (await self._get()).flags
176
+ except G90Error as exc:
177
+ _LOGGER.debug(
178
+ 'Retrieving alert config flags resulted in error %s',
179
+ repr(exc)
180
+ )
181
+ if self._cached_data is not None:
182
+ _LOGGER.debug(
183
+ 'Falling back to cached alert configuration flags: %s',
184
+ repr(self._cached_data.flags)
185
+ )
186
+ result = self._cached_data.flags
187
+ else:
188
+ _LOGGER.debug('No cached alert configuration flags available')
189
+
190
+ return result
@@ -17,141 +17,16 @@
17
17
  # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
18
  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
19
  # SOFTWARE.
20
-
21
20
  """
22
- Represents various configuration aspects of the alarm panel.
21
+ Compatibility module for the alert configuration, which should be imported
22
+ from `local.alert_config` instead.
23
23
  """
24
- from __future__ import annotations
25
- from typing import TYPE_CHECKING
26
- import logging
27
- from dataclasses import dataclass
28
- from enum import IntFlag
29
- from ..const import G90Commands
30
- if TYPE_CHECKING:
31
- from ..alarm import G90Alarm
32
-
33
-
34
- class G90AlertConfigFlags(IntFlag):
35
- """
36
- Alert configuration flags, used bitwise
37
- """
38
- AC_POWER_FAILURE = 1
39
- AC_POWER_RECOVER = 2
40
- ARM_DISARM = 4
41
- HOST_LOW_VOLTAGE = 8
42
- SENSOR_LOW_VOLTAGE = 16
43
- WIFI_AVAILABLE = 32
44
- WIFI_UNAVAILABLE = 64
45
- DOOR_OPEN = 128
46
- DOOR_CLOSE = 256
47
- SMS_PUSH = 512
48
- UNKNOWN1 = 2048
49
- UNKNOWN2 = 8192
50
-
51
-
52
- _LOGGER = logging.getLogger(__name__)
53
-
54
-
55
- @dataclass
56
- class G90AlertConfigData:
57
- """
58
- Represents alert configuration data as received from the alarm panel.
59
- """
60
- flags_data: int
61
-
62
- @property
63
- def flags(self) -> G90AlertConfigFlags:
64
- """
65
- :return: The alert configuration flags
66
- """
67
- return G90AlertConfigFlags(self.flags_data)
68
-
69
- @flags.setter
70
- def flags(self, value: G90AlertConfigFlags) -> None:
71
- """
72
- :param value: The alert configuration flags
73
- """
74
- self.flags_data = value.value
75
-
76
-
77
- class G90AlertConfig:
78
- """
79
- Represents alert configuration as received from the alarm panel.
80
- """
81
- def __init__(self, parent: G90Alarm) -> None:
82
- self.parent = parent
83
-
84
- async def _get(self) -> G90AlertConfigData:
85
- """
86
- Retrieves the alert configuration flags directly from the device.
87
-
88
- :return: The alerts configured
89
- """
90
- _LOGGER.debug('Retrieving alert configuration from the device')
91
- res = await self.parent.command(G90Commands.GETNOTICEFLAG)
92
- data = G90AlertConfigData(*res)
93
- _LOGGER.debug(
94
- 'Alert configuration: %s, flags: %s', data,
95
- repr(data.flags)
96
- )
97
- return data
98
-
99
- async def set(self, flags: G90AlertConfigFlags) -> None:
100
- """
101
- .. deprecated:: 2.3.0
102
-
103
- This method is deprecated and will always raise a RuntimeError.
104
- Please use :meth:`set_flag` to set individual flags.
105
- """
106
- raise RuntimeError(
107
- 'The set() method is deprecated. Please use set_flag() to set'
108
- ' individual flags instead.'
109
- )
110
-
111
- async def _set(self, flags: G90AlertConfigFlags) -> None:
112
- """
113
- Sets the alert configuration flags on the device.
114
- """
115
- _LOGGER.debug('Setting alert configuration to %s', repr(flags))
116
- await self.parent.command(G90Commands.SETNOTICEFLAG, [flags.value])
117
-
118
- async def get_flag(self, flag: G90AlertConfigFlags) -> bool:
119
- """
120
- :param flag: The flag to check
121
- """
122
- return flag in await self.flags
123
-
124
- async def set_flag(self, flag: G90AlertConfigFlags, value: bool) -> None:
125
- """
126
- Sets the given flag to the desired value.
127
-
128
- Uses read-modify-write approach.
129
-
130
- :param flag: The flag to set
131
- :param value: The value to set
132
- """
133
- # Retrieve current flags
134
- current_flags = await self.flags
135
- # Skip updating the flag if it has the desired value
136
- if (flag in current_flags) == value:
137
- _LOGGER.debug(
138
- 'Flag %s already set to %s, skipping update',
139
- repr(flag), value
140
- )
141
- return
142
-
143
- # Set or reset corresponding user flag depending on desired value
144
- if value:
145
- current_flags |= flag
146
- else:
147
- current_flags &= ~flag
148
-
149
- # Set the updated flags
150
- await self._set(current_flags)
151
-
152
- @property
153
- async def flags(self) -> G90AlertConfigFlags:
154
- """
155
- :return: Symbolic names for corresponding flag bits
156
- """
157
- return (await self._get()).flags
24
+ from .alert_config import (
25
+ G90AlertConfig, G90AlertConfigData, G90AlertConfigFlags
26
+ )
27
+
28
+ __all__ = [
29
+ 'G90AlertConfig',
30
+ 'G90AlertConfigData',
31
+ 'G90AlertConfigFlags',
32
+ ]
@@ -0,0 +1,131 @@
1
+
2
+ # Copyright (c) 2026 Ilia Sotnikov
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ # of this software and associated documentation files (the "Software"), to deal
6
+ # in the Software without restriction, including without limitation the rights
7
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ # copies of the Software, and to permit persons to whom the Software is
9
+ # furnished to do so, subject to the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be included in
12
+ # all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ # SOFTWARE.
21
+ """
22
+ Base class for loading/saving dataclasses to a device.
23
+ """
24
+ from __future__ import annotations
25
+ from typing import TYPE_CHECKING, Type, TypeVar, Optional, ClassVar, Any, Dict
26
+ import logging
27
+ from dataclasses import dataclass, astuple, asdict
28
+ from ..const import G90Commands
29
+ if TYPE_CHECKING:
30
+ from ..alarm import G90Alarm
31
+
32
+
33
+ _LOGGER = logging.getLogger(__name__)
34
+ S = TypeVar('S', bound='DataclassLoadSave')
35
+
36
+
37
+ @dataclass
38
+ class DataclassLoadSave:
39
+ """
40
+ Base class for loading/saving dataclasses to a device.
41
+
42
+ There are multiple ways to implement the functionality:
43
+ - Encapsulate the dataclass inside another class that handles
44
+ loading/saving and exposes dataclass fields as properties. The latter
45
+ part gets complex as properties need to be asynchronous, as well as
46
+ added dynamically at runtime to improve maintainability.
47
+ - Inherit from this class, which provides `load` and `save` methods on top
48
+ of standard dataclasses. This is believed to be more concise and easier
49
+ to understand.
50
+
51
+ Implementing classes must define `LOAD_COMMAND` and `SAVE_COMMAND` class
52
+ variables to specify which commands to use for loading and saving data.
53
+
54
+ Example usage:
55
+
56
+ @dataclass
57
+ class G90ExampleConfig(DataclassLoadSave):
58
+ LOAD_COMMAND = G90Commands.GETEXAMPLECONFIG
59
+ SAVE_COMMAND = G90Commands.SETEXAMPLECONFIG
60
+ field1: int
61
+ field2: str
62
+
63
+ # Loading data
64
+ config = await G90ExampleConfig.load(G90_alarm_instance)
65
+ print(config.field1, config.field2)
66
+
67
+ # Modifying and saving data
68
+ config.field1 = 42
69
+ await config.save()
70
+ """
71
+ LOAD_COMMAND: ClassVar[Optional[G90Commands]] = None
72
+ SAVE_COMMAND: ClassVar[Optional[G90Commands]] = None
73
+
74
+ def __post_init__(self) -> None:
75
+ """
76
+ Post-initialization processing.
77
+ """
78
+ # Instance variable to hold reference to parent G90Alarm instance,
79
+ # declared here to avoid being part of dataclass fields
80
+ self._parent: Optional[G90Alarm] = None
81
+
82
+ async def save(self) -> None:
83
+ """
84
+ Save the current data to the device.
85
+ """
86
+ assert self.SAVE_COMMAND is not None, '`SAVE_COMMAND` must be defined'
87
+ assert self._parent is not None, 'Please call `load()` first'
88
+
89
+ _LOGGER.debug('Setting data to the device: %s', str(self))
90
+ await self._parent.command(
91
+ self.SAVE_COMMAND,
92
+ list(astuple(self))
93
+ )
94
+
95
+ @classmethod
96
+ async def load(cls: Type[S], parent: G90Alarm) -> S:
97
+ """
98
+ Create an instance with values loaded from the device.
99
+
100
+ :return: An instance of the dataclass loaded from the device.
101
+ """
102
+ assert cls.LOAD_COMMAND is not None, '`LOAD_COMMAND` must be defined'
103
+ assert parent is not None, '`parent` must be provided'
104
+
105
+ data = await parent.command(cls.LOAD_COMMAND)
106
+ obj = cls(*data)
107
+ _LOGGER.debug('Loaded data: %s', str(obj))
108
+
109
+ obj._parent = parent
110
+
111
+ return obj
112
+
113
+ def _asdict(self) -> Dict[str, Any]:
114
+ """
115
+ Returns the dataclass fields as a dictionary.
116
+
117
+ :return: A dictionary representation.
118
+ """
119
+ return asdict(self)
120
+
121
+ def __str__(self) -> str:
122
+ """
123
+ Textual representation of the entry.
124
+
125
+ `str()` is used instead of `repr()` since dataclass provides `repr()`
126
+ by default, and it would be impractical to require each ancestor to
127
+ disable that.
128
+
129
+ :return: A textual representation.
130
+ """
131
+ return super().__repr__() + f'({str(self._asdict())})'
@@ -33,7 +33,9 @@ from ..const import (
33
33
  G90AlertStateChangeTypes,
34
34
  G90HistoryStates,
35
35
  G90RemoteButtonStates,
36
+ G90RFIDKeypadStates,
36
37
  )
38
+ from ..event_mapping import map_alert_state
37
39
  from ..notifications.base import G90DeviceAlert
38
40
 
39
41
  _LOGGER = logging.getLogger(__name__)
@@ -54,6 +56,8 @@ states_mapping_alerts = {
54
56
  G90HistoryStates.LOW_BATTERY,
55
57
  G90AlertStates.ALARM:
56
58
  G90HistoryStates.ALARM,
59
+ G90AlertStates.MOTION_DETECTED:
60
+ G90HistoryStates.MOTION_DETECTED,
57
61
  }
58
62
 
59
63
  states_mapping_state_changes = {
@@ -86,6 +90,27 @@ states_mapping_remote_buttons = {
86
90
  G90HistoryStates.REMOTE_BUTTON_SOS,
87
91
  }
88
92
 
93
+ states_mapping_rfid = {
94
+ G90RFIDKeypadStates.ARM_AWAY:
95
+ G90HistoryStates.RFID_KEY_ARM_AWAY,
96
+ G90RFIDKeypadStates.ARM_HOME:
97
+ G90HistoryStates.RFID_KEY_ARM_HOME,
98
+ G90RFIDKeypadStates.DISARM:
99
+ G90HistoryStates.RFID_KEY_DISARM,
100
+ G90RFIDKeypadStates.LOW_BATTERY:
101
+ G90HistoryStates.LOW_BATTERY,
102
+ G90RFIDKeypadStates.CARD_0:
103
+ G90HistoryStates.RFID_CARD_0,
104
+ G90RFIDKeypadStates.CARD_1:
105
+ G90HistoryStates.RFID_CARD_1,
106
+ G90RFIDKeypadStates.CARD_2:
107
+ G90HistoryStates.RFID_CARD_2,
108
+ G90RFIDKeypadStates.CARD_3:
109
+ G90HistoryStates.RFID_CARD_3,
110
+ G90RFIDKeypadStates.CARD_4:
111
+ G90HistoryStates.RFID_CARD_4,
112
+ }
113
+
89
114
 
90
115
  @dataclass
91
116
  class ProtocolData:
@@ -140,21 +165,28 @@ class G90History:
140
165
  """
141
166
  State for the history entry.
142
167
  """
168
+ # pylint: disable=too-many-return-statements
143
169
  # No meaningful state for SOS alerts initiated by the panel itself
144
170
  # (host)
145
171
  if self.type == G90AlertTypes.HOST_SOS:
146
172
  return None
147
173
 
148
174
  try:
149
- # State of the remote indicate which button has been pressed
150
- if (
151
- self.type in [
152
- G90AlertTypes.SENSOR_ACTIVITY, G90AlertTypes.ALARM
153
- ] and self.source == G90AlertSources.REMOTE
154
- ):
155
- return states_mapping_remote_buttons[
156
- G90RemoteButtonStates(self._protocol_data.state)
157
- ]
175
+ # Remote button pressed or RFID keypad event occurred
176
+ if self.type in [
177
+ G90AlertTypes.SENSOR_ACTIVITY, G90AlertTypes.ALARM
178
+ ]:
179
+ # State of the remote indicate which button has been pressed
180
+ if self.source == G90AlertSources.REMOTE:
181
+ return states_mapping_remote_buttons[
182
+ G90RemoteButtonStates(self._protocol_data.state)
183
+ ]
184
+
185
+ # State of the RFID keypad indicate which action has occurred
186
+ if self.source == G90AlertSources.RFID:
187
+ return states_mapping_rfid[
188
+ G90RFIDKeypadStates(self._protocol_data.state)
189
+ ]
158
190
 
159
191
  # Door open/close or alert types, mapped against `G90AlertStates`
160
192
  # using `state` incoming field
@@ -162,8 +194,14 @@ class G90History:
162
194
  G90AlertTypes.SENSOR_ACTIVITY, G90AlertTypes.ALARM
163
195
  ]:
164
196
  return G90HistoryStates(
197
+ # Map to history state via consolidated alert state
165
198
  states_mapping_alerts[
166
- G90AlertStates(self._protocol_data.state)
199
+ # Map to consolidated alert state first
200
+ map_alert_state(
201
+ # Defaults to sensor source if none available
202
+ self.source or G90AlertSources.SENSOR,
203
+ self._protocol_data.state
204
+ )
167
205
  ]
168
206
  )
169
207
  except (ValueError, KeyError):
@@ -228,8 +266,12 @@ class G90History:
228
266
  ID of the sensor related to the history entry, might be empty if none
229
267
  associated.
230
268
  """
231
- # Sensor ID will only be available if entry source is a sensor
232
- if self.source == G90AlertSources.SENSOR:
269
+ # Sensor ID will only be available if entry source is an infrared, RFID
270
+ # keypad or other sensor
271
+ if self.source in [
272
+ G90AlertSources.SENSOR, G90AlertSources.RFID,
273
+ G90AlertSources.INFRARED
274
+ ]:
233
275
  return self._protocol_data.event_id
234
276
 
235
277
  return None
@@ -267,6 +309,6 @@ class G90History:
267
309
 
268
310
  def __repr__(self) -> str:
269
311
  """
270
- Textural representation of the history entry.
312
+ Textual representation of the history entry.
271
313
  """
272
314
  return super().__repr__() + f'({repr(self._asdict())})'