pyg90alarm 2.1.0__py3-none-any.whl → 2.2.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.
@@ -29,9 +29,17 @@ from typing import (
29
29
  )
30
30
 
31
31
  from enum import IntEnum, IntFlag
32
- from ..definitions.sensors import SENSOR_DEFINITIONS, SensorDefinition
32
+ from ..definitions.base import (
33
+ G90PeripheralDefinition,
34
+ G90PeripheralProtocols, G90PeripheralTypes,
35
+ )
36
+ from ..definitions.sensors import (
37
+ G90SensorDefinitions,
38
+ )
33
39
  from ..const import G90Commands
34
40
  from .base_entity import G90BaseEntity
41
+ from ..callback import G90CallbackList
42
+ from ..exceptions import G90PeripheralDefinitionNotFound
35
43
  if TYPE_CHECKING:
36
44
  from ..alarm import (
37
45
  G90Alarm, SensorStateCallback, SensorLowBatteryCallback,
@@ -146,57 +154,6 @@ ALERT_MODES_MAP_BY_VALUE = dict(
146
154
  )
147
155
 
148
156
 
149
- class G90SensorProtocols(IntEnum):
150
- """
151
- Protocol types for the sensors.
152
- """
153
- RF_1527 = 0
154
- RF_2262 = 1
155
- RF_PRIVATE = 2
156
- RF_SLIDER = 3
157
- CORD = 5
158
- WIFI = 4
159
- USB = 6
160
-
161
-
162
- class G90SensorTypes(IntEnum):
163
- """
164
- Sensor types.
165
- """
166
- DOOR = 1
167
- GLASS = 2
168
- GAS = 3
169
- SMOKE = 4
170
- SOS = 5
171
- VIB = 6
172
- WATER = 7
173
- INFRARED = 8
174
- IN_BEAM = 9
175
- REMOTE = 10
176
- RFID = 11
177
- DOORBELL = 12
178
- BUTTONID = 13
179
- WATCH = 14
180
- FINGER_LOCK = 15
181
- SUBHOST = 16
182
- REMOTE_2_4G = 17
183
- CORD_SENSOR = 126
184
- SOCKET = 128
185
- SIREN = 129
186
- CURTAIN = 130
187
- SLIDINGWIN = 131
188
- AIRCON = 136
189
- TV = 137
190
- NIGHTLIGHT = 138
191
- SOCKET_2_4G = 140
192
- SIREN_2_4G = 141
193
- SWITCH_2_4G = 142
194
- TOUCH_SWITCH_2_4G = 143
195
- CURTAIN_2_4G = 144
196
- CORD_DEV = 254
197
- UNKNOWN = 255
198
-
199
-
200
157
  _LOGGER = logging.getLogger(__name__)
201
158
 
202
159
 
@@ -227,28 +184,45 @@ class G90Sensor(G90BaseEntity): # pylint:disable=too-many-instance-attributes
227
184
  self._parent = parent
228
185
  self._subindex = subindex
229
186
  self._occupancy = False
230
- self._state_callback: Optional[SensorStateCallback] = None
231
- self._low_battery_callback: Optional[SensorLowBatteryCallback] = None
187
+ self._state_callback: G90CallbackList[SensorStateCallback] = (
188
+ G90CallbackList()
189
+ )
190
+ self._low_battery_callback: G90CallbackList[
191
+ SensorLowBatteryCallback
192
+ ] = G90CallbackList()
232
193
  self._low_battery = False
233
194
  self._tampered = False
234
- self._door_open_when_arming_callback: Optional[
195
+ self._door_open_when_arming_callback: G90CallbackList[
235
196
  SensorDoorOpenWhenArmingCallback
236
- ] = None
237
- self._tamper_callback: Optional[SensorTamperCallback] = None
197
+ ] = G90CallbackList()
198
+ self._tamper_callback: G90CallbackList[SensorTamperCallback] = (
199
+ G90CallbackList()
200
+ )
238
201
  self._door_open_when_arming = False
239
202
  self._proto_idx = proto_idx
240
203
  self._extra_data: Any = None
241
204
  self._unavailable = False
205
+ self._definition: Optional[G90PeripheralDefinition] = None
242
206
 
243
- self._definition: Optional[SensorDefinition] = None
244
- # Get sensor definition corresponds to the sensor type/subtype if any
245
- for s_def in SENSOR_DEFINITIONS:
246
- if (
247
- s_def.type == self._protocol_data.type_id
248
- and s_def.subtype == self._protocol_data.subtype # noqa:W503
249
- ):
250
- self._definition = s_def
251
- break
207
+ @property
208
+ def definition(self) -> Optional[G90PeripheralDefinition]:
209
+ """
210
+ Returns the definition for the sensor.
211
+
212
+ :return: Sensor definition
213
+ """
214
+ if not self._definition:
215
+ # No definition has been cached, try to find it by type, subtype
216
+ # and protocol
217
+ try:
218
+ self._definition = (
219
+ G90SensorDefinitions.get_by_id(
220
+ self.type, self.subtype, self.protocol
221
+ )
222
+ )
223
+ except G90PeripheralDefinitionNotFound:
224
+ return None
225
+ return self._definition
252
226
 
253
227
  def update(self, obj: G90Sensor) -> None:
254
228
  """
@@ -272,41 +246,49 @@ class G90Sensor(G90BaseEntity): # pylint:disable=too-many-instance-attributes
272
246
  return f'{self._protocol_data.parent_name}#{self._subindex + 1}'
273
247
 
274
248
  @property
275
- def state_callback(self) -> Optional[SensorStateCallback]:
249
+ def state_callback(self) -> G90CallbackList[SensorStateCallback]:
276
250
  """
277
251
  Callback that is invoked when the sensor changes its state.
278
252
 
279
253
  :return: Sensor state callback
254
+
255
+ .. seealso:: :attr:`G90Alarm.sensor_callback` for compatiblity notes
280
256
  """
281
257
  return self._state_callback
282
258
 
283
259
  @state_callback.setter
284
260
  def state_callback(self, value: SensorStateCallback) -> None:
285
- self._state_callback = value
261
+ self._state_callback.add(value)
286
262
 
287
263
  @property
288
- def low_battery_callback(self) -> Optional[SensorLowBatteryCallback]:
264
+ def low_battery_callback(
265
+ self
266
+ ) -> G90CallbackList[SensorLowBatteryCallback]:
289
267
  """
290
268
  Callback that is invoked when the sensor reports on low battery
291
269
  condition.
292
270
 
293
271
  :return: Sensor's low battery callback
272
+
273
+ .. seealso:: :attr:`G90Alarm.sensor_callback` for compatiblity notes
294
274
  """
295
275
  return self._low_battery_callback
296
276
 
297
277
  @low_battery_callback.setter
298
278
  def low_battery_callback(self, value: SensorLowBatteryCallback) -> None:
299
- self._low_battery_callback = value
279
+ self._low_battery_callback.add(value)
300
280
 
301
281
  @property
302
282
  def door_open_when_arming_callback(
303
283
  self
304
- ) -> Optional[SensorDoorOpenWhenArmingCallback]:
284
+ ) -> G90CallbackList[SensorDoorOpenWhenArmingCallback]:
305
285
  """
306
286
  Callback that is invoked when the sensor reports on open door
307
287
  condition when arming.
308
288
 
309
289
  :return: Sensor's door open when arming callback
290
+
291
+ .. seealso:: :attr:`G90Alarm.sensor_callback` for compatiblity notes
310
292
  """
311
293
  return self._door_open_when_arming_callback
312
294
 
@@ -314,20 +296,22 @@ class G90Sensor(G90BaseEntity): # pylint:disable=too-many-instance-attributes
314
296
  def door_open_when_arming_callback(
315
297
  self, value: SensorDoorOpenWhenArmingCallback
316
298
  ) -> None:
317
- self._door_open_when_arming_callback = value
299
+ self._door_open_when_arming_callback.add(value)
318
300
 
319
301
  @property
320
- def tamper_callback(self) -> Optional[SensorTamperCallback]:
302
+ def tamper_callback(self) -> G90CallbackList[SensorTamperCallback]:
321
303
  """
322
304
  Callback that is invoked when the sensor reports being tampered.
323
305
 
324
306
  :return: Sensor's tamper callback
307
+
308
+ .. seealso:: :attr:`G90Alarm.sensor_callback` for compatiblity notes
325
309
  """
326
310
  return self._tamper_callback
327
311
 
328
312
  @tamper_callback.setter
329
313
  def tamper_callback(self, value: SensorTamperCallback) -> None:
330
- self._tamper_callback = value
314
+ self._tamper_callback.add(value)
331
315
 
332
316
  @property
333
317
  def occupancy(self) -> bool:
@@ -355,22 +339,22 @@ class G90Sensor(G90BaseEntity): # pylint:disable=too-many-instance-attributes
355
339
  self._occupancy = value
356
340
 
357
341
  @property
358
- def protocol(self) -> G90SensorProtocols:
342
+ def protocol(self) -> G90PeripheralProtocols:
359
343
  """
360
344
  Protocol type of the sensor.
361
345
 
362
346
  :return: Protocol type
363
347
  """
364
- return G90SensorProtocols(self._protocol_data.protocol_id)
348
+ return G90PeripheralProtocols(self._protocol_data.protocol_id)
365
349
 
366
350
  @property
367
- def type(self) -> G90SensorTypes:
351
+ def type(self) -> G90PeripheralTypes:
368
352
  """
369
353
  Type of the sensor.
370
354
 
371
355
  :return: Sensor type
372
356
  """
373
- return G90SensorTypes(self._protocol_data.type_id)
357
+ return G90PeripheralTypes(self._protocol_data.type_id)
374
358
 
375
359
  @property
376
360
  def subtype(self) -> int:
@@ -456,6 +440,18 @@ class G90Sensor(G90BaseEntity): # pylint:disable=too-many-instance-attributes
456
440
  """
457
441
  return self._proto_idx
458
442
 
443
+ @property
444
+ def type_name(self) -> Optional[str]:
445
+ """
446
+ Type of the sensor.
447
+
448
+ :return: Type
449
+ """
450
+ if not self.definition:
451
+ return None
452
+
453
+ return self.definition.name
454
+
459
455
  @property
460
456
  def supports_updates(self) -> bool:
461
457
  """
@@ -463,14 +459,12 @@ class G90Sensor(G90BaseEntity): # pylint:disable=too-many-instance-attributes
463
459
 
464
460
  :return: Support for updates
465
461
  """
466
- if not self._definition:
462
+ if not self.definition:
467
463
  _LOGGER.warning(
468
464
  'Manipulating with user flags for sensor index=%s'
469
465
  ' is unsupported - no sensor definition for'
470
- ' type=%s, subtype=%s',
471
- self.index,
472
- self._protocol_data.type_id,
473
- self._protocol_data.subtype
466
+ ' type=%s, subtype=%s, protocol=%s',
467
+ self.index, self.type, self.subtype, self.protocol
474
468
  )
475
469
  return False
476
470
  return True
@@ -498,7 +492,7 @@ class G90Sensor(G90BaseEntity): # pylint:disable=too-many-instance-attributes
498
492
  """
499
493
  Indicates if the sensor is wireless.
500
494
  """
501
- return self.protocol not in (G90SensorProtocols.CORD,)
495
+ return self.protocol not in (G90PeripheralProtocols.CORD,)
502
496
 
503
497
  @property
504
498
  def is_low_battery(self) -> bool:
@@ -597,7 +591,7 @@ class G90Sensor(G90BaseEntity): # pylint:disable=too-many-instance-attributes
597
591
  # Checking private attribute directly, since `mypy` doesn't recognize
598
592
  # the check for sensor definition is done over
599
593
  # `self.supports_updates` property
600
- if not self._definition:
594
+ if not self.definition:
601
595
  return
602
596
 
603
597
  if value & ~G90SensorUserFlags.USER_SETTABLE:
@@ -685,11 +679,11 @@ class G90Sensor(G90BaseEntity): # pylint:disable=too-many-instance-attributes
685
679
  user_flags_data=self._protocol_data.user_flags_data,
686
680
  baudrate=self._protocol_data.baudrate,
687
681
  protocol_id=self._protocol_data.protocol_id,
688
- reserved_data=self._definition.reserved_data,
682
+ reserved_data=self.definition.reserved_data,
689
683
  node_count=self._protocol_data.node_count,
690
- rx=self._definition.rx,
691
- tx=self._definition.tx,
692
- private_data=self._definition.private_data,
684
+ rx=self.definition.rx,
685
+ tx=self.definition.tx,
686
+ private_data=self.definition.private_data,
693
687
  )
694
688
  # Modify the sensor
695
689
  await self._parent.command(
@@ -20,19 +20,32 @@
20
20
  """
21
21
  Sensor list.
22
22
  """
23
- from typing import (
24
- AsyncGenerator
25
- )
23
+ from __future__ import annotations
24
+ from typing import AsyncGenerator, Optional, TYPE_CHECKING
25
+ import logging
26
+ import asyncio
26
27
 
27
28
  from .sensor import G90Sensor
28
29
  from .base_list import G90BaseList
29
30
  from ..const import G90Commands
31
+ from ..exceptions import G90EntityRegistrationError, G90Error
32
+ from ..definitions.sensors import G90SensorDefinitions
33
+ from ..entities.sensor import G90SensorUserFlags
34
+
35
+ if TYPE_CHECKING:
36
+ from ..alarm import G90Alarm
37
+
38
+ _LOGGER = logging.getLogger(__name__)
30
39
 
31
40
 
32
41
  class G90SensorList(G90BaseList[G90Sensor]):
33
42
  """
34
43
  Sensor list class.
35
44
  """
45
+ def __init__(self, parent: G90Alarm) -> None:
46
+ super().__init__(parent)
47
+ self._sensor_change_future: Optional[asyncio.Future[G90Sensor]] = None
48
+
36
49
  async def _fetch(self) -> AsyncGenerator[G90Sensor, None]:
37
50
  """
38
51
  Fetch the list of sensors from the panel.
@@ -48,3 +61,123 @@ class G90SensorList(G90BaseList[G90Sensor]):
48
61
  *entity.data, parent=self._parent, subindex=0,
49
62
  proto_idx=entity.proto_idx
50
63
  )
64
+
65
+ async def sensor_change_callback(
66
+ self, idx: int, name: str, added: bool
67
+ ) -> None:
68
+ """
69
+ Sensor change callback.
70
+
71
+ Should be invoked from corresponding panel's notification handler to
72
+ finish the registration process.
73
+
74
+ :param idx: Sensor index.
75
+ :param name: Sensor name.
76
+ :param added: True if the sensor was added, False if removed.
77
+ """
78
+ _LOGGER.debug(
79
+ "Sensor change callback: name='%s', index=%d, added=%s",
80
+ name, idx, added
81
+ )
82
+ # The method depends on the future to be created before it is called
83
+ if not self._sensor_change_future:
84
+ raise G90EntityRegistrationError(
85
+ "Sensor change callback called without a future"
86
+ )
87
+
88
+ # Update the sensor list to get the latest data, unfortunately there is
89
+ # no panel command to get just a single sensor
90
+ try:
91
+ await self.update()
92
+ except G90Error as err:
93
+ _LOGGER.debug(
94
+ "Failed to update the sensor list: %s", err
95
+ )
96
+ # Indicate the error to the caller
97
+ self._sensor_change_future.set_exception(err)
98
+ return
99
+
100
+ # Attempt to find the sensor just changed by its index
101
+ if not (found := await self.find_by_idx(
102
+ idx, exclude_unavailable=False
103
+ )):
104
+ msg = (
105
+ f"Failed to find the added sensor '{name}'"
106
+ f' at index {idx}'
107
+ )
108
+ _LOGGER.debug(msg)
109
+ # Indicate the error to the caller
110
+ self._sensor_change_future.set_exception(
111
+ G90EntityRegistrationError(msg)
112
+ )
113
+ return
114
+
115
+ # Provide the found sensor as the future result so that the caller
116
+ # can use it
117
+ self._sensor_change_future.set_result(found)
118
+
119
+ async def register(
120
+ self, definition_name: str,
121
+ room_id: int, timeout: float, name: Optional[str] = None,
122
+ ) -> G90Sensor:
123
+ """
124
+ Registers sensor to the panel.
125
+
126
+ :param definition_name: Sensor definition name.
127
+ :param room_id: Room ID to assign the sensor to.
128
+ :param timeout: Timeout for the registration process.
129
+ :param name: Optional name for the sensor, if not provided, the
130
+ definition name will be used.
131
+ :raises G90EntityRegistrationError: If the registration fails.
132
+ :return: G90Sensor: The registered sensor entity.
133
+ """
134
+ sensor_definition = G90SensorDefinitions.get_by_name(definition_name)
135
+ dev_name = name or sensor_definition.name
136
+
137
+ # Future is needed to coordinate the registration process with panel
138
+ # notifications (see `sensor_change_callback()` method)
139
+ self._sensor_change_future = asyncio.get_running_loop().create_future()
140
+
141
+ # Register the sensor with the panel
142
+ await self._parent.command(
143
+ G90Commands.ADDSENSOR, [
144
+ dev_name,
145
+ # Registering sensor requires to provide a free index from
146
+ # panel point of view
147
+ await self.find_free_idx(),
148
+ room_id,
149
+ sensor_definition.type,
150
+ sensor_definition.subtype,
151
+ sensor_definition.timeout,
152
+ # Newly registered sensors are enabled by default and set to
153
+ # alarm in away and home modes
154
+ G90SensorUserFlags.ENABLED
155
+ | G90SensorUserFlags.ALERT_WHEN_AWAY_AND_HOME,
156
+ sensor_definition.baudrate,
157
+ sensor_definition.protocol,
158
+ sensor_definition.reserved_data,
159
+ sensor_definition.node_count,
160
+ sensor_definition.rx,
161
+ sensor_definition.tx,
162
+ sensor_definition.private_data
163
+ ]
164
+ )
165
+
166
+ # Waiting for the registration to finish, where panel will send
167
+ # corresponding notification processed by `sensor_change_callback()`
168
+ # method, which manipulates the future created above.
169
+ done, _ = await asyncio.wait(
170
+ [self._sensor_change_future], timeout=timeout
171
+ )
172
+
173
+ if self._sensor_change_future not in done:
174
+ msg = f"Failed to learn the device '{dev_name}', timed out"
175
+ _LOGGER.debug(msg)
176
+ raise G90EntityRegistrationError(msg)
177
+
178
+ # Propagate any exception that might have occurred in
179
+ # `sensor_change_callback()` method during the registration process
180
+ self._sensor_change_future.exception()
181
+
182
+ # Return the registered sensor entity
183
+ return self._sensor_change_future.result()
pyg90alarm/exceptions.py CHANGED
@@ -37,3 +37,27 @@ class G90TimeoutError(asyncio.TimeoutError): # pylint:disable=R0903
37
37
  Raised when particular package class to report an operation (typically
38
38
  device command) has timed out.
39
39
  """
40
+
41
+
42
+ class G90CommandFailure(G90Error):
43
+ """
44
+ Raised when a command to the alarm panel reports failure.
45
+ """
46
+
47
+
48
+ class G90CommandError(G90Error):
49
+ """
50
+ Raised when a command to the alarm panel reports an error.
51
+ """
52
+
53
+
54
+ class G90EntityRegistrationError(G90Error):
55
+ """
56
+ Raised when registering an entity to the alarm panel fails.
57
+ """
58
+
59
+
60
+ class G90PeripheralDefinitionNotFound(G90Error):
61
+ """
62
+ Raised when a peripheral definition is not found.
63
+ """
@@ -30,7 +30,9 @@ 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)
33
+ from ..exceptions import (
34
+ G90Error, G90TimeoutError, G90CommandFailure, G90CommandError
35
+ )
34
36
  from ..const import G90Commands
35
37
 
36
38
 
@@ -168,19 +170,39 @@ class G90BaseCommand(DatagramProtocol):
168
170
  if not data.endswith('IEND\0'):
169
171
  raise G90Error('Missing end marker in data')
170
172
  payload = data[6:-5]
171
- _LOGGER.debug("Decoded from wire: JSON string '%s'", payload)
173
+ _LOGGER.debug("Decoded from wire: string '%s'", payload)
174
+
175
+ if not payload:
176
+ return
177
+
178
+ # Panel may report the last command has failed
179
+ if payload == 'fail':
180
+ raise G90CommandFailure(
181
+ f"Command {self._code.name}"
182
+ f" (code={self._code.value}) failed"
183
+ )
184
+
185
+ # Also, panel may report an error supplying specific reason, e.g.
186
+ # command and its arguments that have failed
187
+ if payload.startswith('error'):
188
+ error = payload[5:]
189
+ raise G90CommandError(
190
+ f"Command {self._code.name}"
191
+ f" (code={self._code.value}) failed"
192
+ f" with error: '{error}'")
172
193
 
173
194
  resp = None
174
- if payload:
175
- try:
176
- resp = json.loads(payload)
177
- except json.JSONDecodeError as exc:
178
- raise G90Error('Unable to parse response as JSON:'
179
- f" '{payload}'") from exc
180
-
181
- if not isinstance(resp, list):
182
- raise G90Error('Mailformed response,'
183
- f" 'list' expected: '{payload}'")
195
+ try:
196
+ resp = json.loads(payload, strict=False)
197
+ except json.JSONDecodeError as exc:
198
+ raise G90Error(
199
+ f"Unable to parse response as JSON: '{payload}'"
200
+ ) from exc
201
+
202
+ if not isinstance(resp, list):
203
+ raise G90Error(
204
+ f"Malformed response, 'list' expected: '{payload}'"
205
+ )
184
206
 
185
207
  if resp is not None:
186
208
  self._resp = G90Header(*resp)
@@ -42,12 +42,12 @@ class G90LocalNotifications(G90NotificationsBase, DatagramProtocol):
42
42
  There is a basic check to ensure only notifications/alerts from the correct
43
43
  device are processed - the check uses the host and port of the device, and
44
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
45
+ commands (e.g. :class:`.G90Alarm`). The latter to work correctly needs a
46
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`).
47
+ it using :attr:`.device_id` (e.g. :meth:`.G90Alarm.get_host_info`).
48
48
 
49
49
  :param protocol_factory: A callable that returns a new instance of the
50
- :class:`G90NotificationProtocol` class.
50
+ :class:`.G90NotificationProtocol` class.
51
51
  :param port: The port on which the device is listening for notifications.
52
52
  :param host: The host on which the device is listening for notifications.
53
53
  :param local_port: The port on which the local host is listening for
@@ -89,6 +89,18 @@ class G90ArmDisarmInfo:
89
89
  state: int
90
90
 
91
91
 
92
+ @dataclass
93
+ class G90SensorChangeInfo:
94
+ """
95
+ Represents the sensor added notification received from the device.
96
+
97
+ :meta private:
98
+ """
99
+ idx: int
100
+ name: str
101
+ action: int # 1 - if added, 2 - for update (?)
102
+
103
+
92
104
  @dataclass
93
105
  class G90DeviceAlert: # pylint: disable=too-many-instance-attributes
94
106
  """
@@ -112,9 +124,9 @@ class G90NotificationsBase:
112
124
  There is a basic check to ensure only notifications/alerts from the correct
113
125
  device are processed - the check uses the host and port of the device, and
114
126
  the device ID (GUID) that is set by the ancestor class that implements the
115
- commands (e.g. :class:`G90Alarm`). The latter to work correctly needs a
127
+ commands (e.g. :class:`.G90Alarm`). The latter to work correctly needs a
116
128
  command to be performed first, one that fetches device GUID and then stores
117
- it using :attr:`.device_id` (e.g. :meth:`G90Alarm.get_host_info`).
129
+ it using :attr:`.device_id` (e.g. :meth:`.G90Alarm.get_host_info`).
118
130
  """
119
131
  def __init__(
120
132
  self, protocol_factory: Callable[[], G90NotificationProtocol],
@@ -173,6 +185,21 @@ class G90NotificationsBase:
173
185
  )
174
186
  return
175
187
 
188
+ # Sensor has been added or removed
189
+ if notification.kind == G90NotificationTypes.SENSOR_CHANGE:
190
+ g90_sensor_info = G90SensorChangeInfo(*notification.data)
191
+ sensor_added = g90_sensor_info.action == 1
192
+ _LOGGER.debug(
193
+ 'Sensor change notification, added=%s: %s',
194
+ sensor_added, g90_sensor_info
195
+ )
196
+ G90Callback.invoke(
197
+ self._protocol.on_sensor_change,
198
+ g90_sensor_info.idx, g90_sensor_info.name,
199
+ sensor_added
200
+ )
201
+ return
202
+
176
203
  _LOGGER.warning('Unknown notification received:'
177
204
  ' kind %s, data %s',
178
205
  notification.kind, notification.data)
@@ -114,3 +114,14 @@ class G90NotificationProtocol:
114
114
  :param zone_name: Name of the sensor that reports SOS.
115
115
  :param is_host_sos: Indicates if the SOS is host-initiated.
116
116
  """
117
+
118
+ async def on_sensor_change(
119
+ self, sensor_idx: int, sensor_name: str, added: bool
120
+ ) -> None:
121
+ """
122
+ Invoked when a sensor is added or changed on the panel.
123
+
124
+ :param sensor_idx: Index of the sensor.
125
+ :param sensor_name: Name of the sensor.
126
+ :param added: True if the sensor was added.
127
+ """
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyg90alarm
3
- Version: 2.1.0
3
+ Version: 2.2.0
4
4
  Summary: G90 Alarm system protocol
5
5
  Home-page: https://github.com/hostcc/pyg90alarm
6
6
  Author: Ilia Sotnikov