qolsys-controller 0.0.34__tar.gz → 0.0.35__tar.gz

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.

Potentially problematic release.


This version of qolsys-controller might be problematic. Click here for more details.

Files changed (77) hide show
  1. {qolsys_controller-0.0.34 → qolsys_controller-0.0.35}/PKG-INFO +5 -4
  2. {qolsys_controller-0.0.34 → qolsys_controller-0.0.35}/README.md +4 -3
  3. {qolsys_controller-0.0.34 → qolsys_controller-0.0.35}/pyproject.toml +1 -1
  4. {qolsys_controller-0.0.34 → qolsys_controller-0.0.35}/qolsys_controller/enum.py +1 -0
  5. {qolsys_controller-0.0.34 → qolsys_controller-0.0.35}/qolsys_controller/panel.py +10 -2
  6. {qolsys_controller-0.0.34 → qolsys_controller-0.0.35}/qolsys_controller/plugin_remote.py +44 -61
  7. {qolsys_controller-0.0.34 → qolsys_controller-0.0.35}/qolsys_controller/settings.py +18 -1
  8. {qolsys_controller-0.0.34 → qolsys_controller-0.0.35}/qolsys_controller/state.py +8 -4
  9. {qolsys_controller-0.0.34 → qolsys_controller-0.0.35}/qolsys_controller/task_manager.py +8 -3
  10. {qolsys_controller-0.0.34 → qolsys_controller-0.0.35}/qolsys_controller/zone.py +29 -1
  11. {qolsys_controller-0.0.34 → qolsys_controller-0.0.35}/.github/workflows/build.yml +0 -0
  12. {qolsys_controller-0.0.34 → qolsys_controller-0.0.35}/.github/workflows/publish.yml +0 -0
  13. {qolsys_controller-0.0.34 → qolsys_controller-0.0.35}/.gitignore +0 -0
  14. {qolsys_controller-0.0.34 → qolsys_controller-0.0.35}/Info_mqtt.md +0 -0
  15. {qolsys_controller-0.0.34 → qolsys_controller-0.0.35}/LICENSE +0 -0
  16. {qolsys_controller-0.0.34 → qolsys_controller-0.0.35}/bin/qolsys.py +0 -0
  17. {qolsys_controller-0.0.34 → qolsys_controller-0.0.35}/example.py +0 -0
  18. {qolsys_controller-0.0.34 → qolsys_controller-0.0.35}/info_pairing.md +0 -0
  19. {qolsys_controller-0.0.34 → qolsys_controller-0.0.35}/info_qolsys.md +0 -0
  20. {qolsys_controller-0.0.34 → qolsys_controller-0.0.35}/qolsys_controller/__init__.py +0 -0
  21. {qolsys_controller-0.0.34 → qolsys_controller-0.0.35}/qolsys_controller/controller.py +0 -0
  22. {qolsys_controller-0.0.34 → qolsys_controller-0.0.35}/qolsys_controller/database/db.py +0 -0
  23. {qolsys_controller-0.0.34 → qolsys_controller-0.0.35}/qolsys_controller/database/table.py +0 -0
  24. {qolsys_controller-0.0.34 → qolsys_controller-0.0.35}/qolsys_controller/database/table_alarmedsensor.py +0 -0
  25. {qolsys_controller-0.0.34 → qolsys_controller-0.0.35}/qolsys_controller/database/table_automation.py +0 -0
  26. {qolsys_controller-0.0.34 → qolsys_controller-0.0.35}/qolsys_controller/database/table_country_locale.py +0 -0
  27. {qolsys_controller-0.0.34 → qolsys_controller-0.0.35}/qolsys_controller/database/table_dashboard_msgs.py +0 -0
  28. {qolsys_controller-0.0.34 → qolsys_controller-0.0.35}/qolsys_controller/database/table_dimmerlight.py +0 -0
  29. {qolsys_controller-0.0.34 → qolsys_controller-0.0.35}/qolsys_controller/database/table_doorlock.py +0 -0
  30. {qolsys_controller-0.0.34 → qolsys_controller-0.0.35}/qolsys_controller/database/table_eu_event.py +0 -0
  31. {qolsys_controller-0.0.34 → qolsys_controller-0.0.35}/qolsys_controller/database/table_heat_map.py +0 -0
  32. {qolsys_controller-0.0.34 → qolsys_controller-0.0.35}/qolsys_controller/database/table_history.py +0 -0
  33. {qolsys_controller-0.0.34 → qolsys_controller-0.0.35}/qolsys_controller/database/table_iqremotesettings.py +0 -0
  34. {qolsys_controller-0.0.34 → qolsys_controller-0.0.35}/qolsys_controller/database/table_iqrouter_network_config.py +0 -0
  35. {qolsys_controller-0.0.34 → qolsys_controller-0.0.35}/qolsys_controller/database/table_iqrouter_user_device.py +0 -0
  36. {qolsys_controller-0.0.34 → qolsys_controller-0.0.35}/qolsys_controller/database/table_master_slave.py +0 -0
  37. {qolsys_controller-0.0.34 → qolsys_controller-0.0.35}/qolsys_controller/database/table_nest_device.py +0 -0
  38. {qolsys_controller-0.0.34 → qolsys_controller-0.0.35}/qolsys_controller/database/table_output_rules.py +0 -0
  39. {qolsys_controller-0.0.34 → qolsys_controller-0.0.35}/qolsys_controller/database/table_partition.py +0 -0
  40. {qolsys_controller-0.0.34 → qolsys_controller-0.0.35}/qolsys_controller/database/table_pgm_outputs.py +0 -0
  41. {qolsys_controller-0.0.34 → qolsys_controller-0.0.35}/qolsys_controller/database/table_powerg_device.py +0 -0
  42. {qolsys_controller-0.0.34 → qolsys_controller-0.0.35}/qolsys_controller/database/table_qolsyssettings.py +0 -0
  43. {qolsys_controller-0.0.34 → qolsys_controller-0.0.35}/qolsys_controller/database/table_scene.py +0 -0
  44. {qolsys_controller-0.0.34 → qolsys_controller-0.0.35}/qolsys_controller/database/table_sensor.py +0 -0
  45. {qolsys_controller-0.0.34 → qolsys_controller-0.0.35}/qolsys_controller/database/table_shades.py +0 -0
  46. {qolsys_controller-0.0.34 → qolsys_controller-0.0.35}/qolsys_controller/database/table_smartsocket.py +0 -0
  47. {qolsys_controller-0.0.34 → qolsys_controller-0.0.35}/qolsys_controller/database/table_state.py +0 -0
  48. {qolsys_controller-0.0.34 → qolsys_controller-0.0.35}/qolsys_controller/database/table_tcc.py +0 -0
  49. {qolsys_controller-0.0.34 → qolsys_controller-0.0.35}/qolsys_controller/database/table_thermostat.py +0 -0
  50. {qolsys_controller-0.0.34 → qolsys_controller-0.0.35}/qolsys_controller/database/table_trouble_conditions.py +0 -0
  51. {qolsys_controller-0.0.34 → qolsys_controller-0.0.35}/qolsys_controller/database/table_user.py +0 -0
  52. {qolsys_controller-0.0.34 → qolsys_controller-0.0.35}/qolsys_controller/database/table_virtual_device.py +0 -0
  53. {qolsys_controller-0.0.34 → qolsys_controller-0.0.35}/qolsys_controller/database/table_weather.py +0 -0
  54. {qolsys_controller-0.0.34 → qolsys_controller-0.0.35}/qolsys_controller/database/table_zigbee_device.py +0 -0
  55. {qolsys_controller-0.0.34 → qolsys_controller-0.0.35}/qolsys_controller/database/table_zwave_association_group.py +0 -0
  56. {qolsys_controller-0.0.34 → qolsys_controller-0.0.35}/qolsys_controller/database/table_zwave_history.py +0 -0
  57. {qolsys_controller-0.0.34 → qolsys_controller-0.0.35}/qolsys_controller/database/table_zwave_node.py +0 -0
  58. {qolsys_controller-0.0.34 → qolsys_controller-0.0.35}/qolsys_controller/database/table_zwave_other.py +0 -0
  59. {qolsys_controller-0.0.34 → qolsys_controller-0.0.35}/qolsys_controller/enum_zwave.py +0 -0
  60. {qolsys_controller-0.0.34 → qolsys_controller-0.0.35}/qolsys_controller/errors.py +0 -0
  61. {qolsys_controller-0.0.34 → qolsys_controller-0.0.35}/qolsys_controller/mdns.py +0 -0
  62. {qolsys_controller-0.0.34 → qolsys_controller-0.0.35}/qolsys_controller/mqtt_command_queue.py +0 -0
  63. {qolsys_controller-0.0.34 → qolsys_controller-0.0.35}/qolsys_controller/observable.py +0 -0
  64. {qolsys_controller-0.0.34 → qolsys_controller-0.0.35}/qolsys_controller/partition.py +0 -0
  65. {qolsys_controller-0.0.34 → qolsys_controller-0.0.35}/qolsys_controller/pki.py +0 -0
  66. {qolsys_controller-0.0.34 → qolsys_controller-0.0.35}/qolsys_controller/plugin.py +0 -0
  67. {qolsys_controller-0.0.34 → qolsys_controller-0.0.35}/qolsys_controller/plugin_c4.py +0 -0
  68. {qolsys_controller-0.0.34 → qolsys_controller-0.0.35}/qolsys_controller/scene.py +0 -0
  69. {qolsys_controller-0.0.34 → qolsys_controller-0.0.35}/qolsys_controller/utils_mqtt.py +0 -0
  70. {qolsys_controller-0.0.34 → qolsys_controller-0.0.35}/qolsys_controller/zwave_device.py +0 -0
  71. {qolsys_controller-0.0.34 → qolsys_controller-0.0.35}/qolsys_controller/zwave_dimmer.py +0 -0
  72. {qolsys_controller-0.0.34 → qolsys_controller-0.0.35}/qolsys_controller/zwave_garagedoor.py +0 -0
  73. {qolsys_controller-0.0.34 → qolsys_controller-0.0.35}/qolsys_controller/zwave_generic.py +0 -0
  74. {qolsys_controller-0.0.34 → qolsys_controller-0.0.35}/qolsys_controller/zwave_lock.py +0 -0
  75. {qolsys_controller-0.0.34 → qolsys_controller-0.0.35}/qolsys_controller/zwave_outlet.py +0 -0
  76. {qolsys_controller-0.0.34 → qolsys_controller-0.0.35}/qolsys_controller/zwave_thermostat.py +0 -0
  77. {qolsys_controller-0.0.34 → qolsys_controller-0.0.35}/requirements.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: qolsys-controller
3
- Version: 0.0.34
3
+ Version: 0.0.35
4
4
  Summary: A Python module that emulates a virtual IQ Remote device, enabling full local control of a Qolsys IQ Panel
5
5
  Project-URL: Homepage, https://github.com/EHylands/QolsysController
6
6
  Project-URL: Issues, https://github.com/EHylands/QolsysController/issues
@@ -33,10 +33,11 @@ A Python module that emulates a virtual IQ Remote device, enabling full **local
33
33
  | **Panel** | Diagnostics sensors | ✅ |
34
34
  |---|---|---|
35
35
  | **Partitions** | Arming status | ✅ |
36
- | | Set Exit sounds | ✅ |
37
- | | Home Instant arming | ✅ |
38
- | | Home Silent Disarm | ✅ |
39
36
  | | Alarm state and type | ✅ |
37
+ | | Set Exit sound | ✅ |
38
+ | | Set Entry Delay | ✅ |
39
+ | | Arm-Stay Instant arming | ✅ |
40
+ | | Arm-Stay Silent Disarm | ✅ |
40
41
  | | Disarm pictures | 🛠️ WIP |
41
42
  |---|---|---|
42
43
  | **Zones** | Sensor Status | ✅ |
@@ -17,10 +17,11 @@ A Python module that emulates a virtual IQ Remote device, enabling full **local
17
17
  | **Panel** | Diagnostics sensors | ✅ |
18
18
  |---|---|---|
19
19
  | **Partitions** | Arming status | ✅ |
20
- | | Set Exit sounds | ✅ |
21
- | | Home Instant arming | ✅ |
22
- | | Home Silent Disarm | ✅ |
23
20
  | | Alarm state and type | ✅ |
21
+ | | Set Exit sound | ✅ |
22
+ | | Set Entry Delay | ✅ |
23
+ | | Arm-Stay Instant arming | ✅ |
24
+ | | Arm-Stay Silent Disarm | ✅ |
24
25
  | | Disarm pictures | 🛠️ WIP |
25
26
  |---|---|---|
26
27
  | **Zones** | Sensor Status | ✅ |
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "qolsys-controller"
3
- version = "0.0.34"
3
+ version = "0.0.35"
4
4
  authors = [
5
5
  { name="Eric Hylands", email="" },
6
6
  ]
@@ -26,6 +26,7 @@ class PartitionAlarmType(StrEnum):
26
26
  SILENT_AUXILIARY_EMERGENCY = "Silent Auxiliary Emergency"
27
27
  SILENT_POLICE_EMERGENCY = "Silent Police Emergency"
28
28
  GLASS_BREAK_AWAY_ONLY = "glassbreakawayonly"
29
+ GLASS_BREAK = "glassbreak"
29
30
 
30
31
  class ZoneStatus(StrEnum):
31
32
  ALARMED = "Alarmed"
@@ -353,7 +353,7 @@ class QolsysPanel(QolsysObservable):
353
353
  def parse_iq2meid_message(self, data: dict) -> bool: # noqa: C901, PLR0912, PLR0915
354
354
 
355
355
  eventName = data.get("eventName")
356
- dbOperation = data.get("dbOperation")
356
+ dbOperation = data.get("dbOperation","")
357
357
  uri = data.get("uri")
358
358
 
359
359
  match eventName:
@@ -361,6 +361,9 @@ class QolsysPanel(QolsysObservable):
361
361
  case "stopScreenCapture":
362
362
  pass
363
363
 
364
+ case "primaryDisconnect":
365
+ LOGGER.info("Main Panel Disconnect")
366
+
364
367
  case "dbChanged":
365
368
 
366
369
  match dbOperation:
@@ -508,6 +511,11 @@ class QolsysPanel(QolsysObservable):
508
511
  if scene is not None and isinstance(node, QolsysScene):
509
512
  scene.update(content_values)
510
513
 
514
+ # Update Trouble Conditions
515
+ case self.db.table_trouble_conditions:
516
+ self.db.table_trouble_conditions.update(selection,selection_argument,content_values)
517
+ # No action needed
518
+
511
519
 
512
520
  case _:
513
521
  LOGGER.debug("iq2meid updating unknow uri:%s", uri)
@@ -799,7 +807,7 @@ class QolsysPanel(QolsysObservable):
799
807
 
800
808
  # Create sensors array
801
809
  for zone_info in zones_list:
802
- zones.append(QolsysZone(zone_info))
810
+ zones.append(QolsysZone(zone_info,self._settings))
803
811
 
804
812
  return zones
805
813
 
@@ -1,11 +1,13 @@
1
1
  import asyncio
2
2
  import base64
3
+ import contextlib
3
4
  import datetime
4
5
  import json
5
6
  import logging
6
7
  import random
7
8
  import ssl
8
9
  import uuid
10
+ from pathlib import Path
9
11
 
10
12
  import aiomqtt
11
13
 
@@ -146,7 +148,7 @@ class QolsysPluginRemote(QolsysPlugin):
146
148
  return True
147
149
 
148
150
  async def start_operation(self) -> None:
149
- await self._task_manager.run(self.mqtt_connect_task(reconnect=True), self._mqtt_task_connect_label)
151
+ await self._task_manager.run(self.mqtt_connect_task(reconnect=True, run_forever=True), self._mqtt_task_connect_label)
150
152
 
151
153
  async def stop_operation(self) -> None:
152
154
  LOGGER.debug("Stopping Plugin Operation")
@@ -166,7 +168,7 @@ class QolsysPluginRemote(QolsysPlugin):
166
168
  self.connected = False
167
169
  self.connected_observer.notify()
168
170
 
169
- async def mqtt_connect_task(self, reconnect: bool) -> None:
171
+ async def mqtt_connect_task(self, reconnect: bool, run_forever: bool) -> None:
170
172
  # Configure TLS parameters for MQTT connection
171
173
  tls_params = aiomqtt.TLSParameters(
172
174
  ca_certs=self._pki.qolsys_cer_file_path,
@@ -179,6 +181,9 @@ class QolsysPluginRemote(QolsysPlugin):
179
181
 
180
182
  LOGGER.debug("MQTT: Connecting ...")
181
183
 
184
+ self._task_manager.cancel(self._mqtt_task_listen_label)
185
+ self._task_manager.cancel(self._mqtt_task_ping_label)
186
+
182
187
  while True:
183
188
  try:
184
189
  self.aiomqtt = aiomqtt.Client(
@@ -188,7 +193,7 @@ class QolsysPluginRemote(QolsysPlugin):
188
193
  tls_insecure=True,
189
194
  clean_session=True,
190
195
  timeout=self.settings.mqtt_timeout,
191
- identifier="QolsysController",
196
+ identifier= self.settings.mqtt_remote_client_id,
192
197
  )
193
198
 
194
199
  await self.aiomqtt.__aenter__()
@@ -207,14 +212,11 @@ class QolsysPluginRemote(QolsysPlugin):
207
212
  # Only log all traffic for debug purposes
208
213
  if self.log_mqtt_mesages:
209
214
  # Subscribe to MQTT commands send to panel by other devices
210
- #await self.aiomqtt.subscribe("mastermeid", qos=self.settings.mqtt_qos)
215
+ await self.aiomqtt.subscribe("mastermeid", qos=self.settings.mqtt_qos)
211
216
 
212
217
  # Subscribe to all topics
213
- await self.aiomqtt.subscribe("#", qos=self.settings.mqtt_qos)
218
+ # await self.aiomqtt.subscribe("#", qos=self.settings.mqtt_qos)
214
219
 
215
- # Start mqtt_listent_task and mqtt_ping_task
216
- self._task_manager.cancel(self._mqtt_task_listen_label)
217
- self._task_manager.cancel(self._mqtt_task_ping_label)
218
220
  self._task_manager.run(self.mqtt_listen_task(), self._mqtt_task_listen_label)
219
221
  self._task_manager.run(self.mqtt_ping_task(), self._mqtt_task_ping_label)
220
222
 
@@ -223,7 +225,6 @@ class QolsysPluginRemote(QolsysPlugin):
223
225
  self.panel.product_type = response_connect.get("primary_product_type", "")
224
226
 
225
227
  await self.command_pingevent()
226
- await self.command_timesync()
227
228
  await self.command_pair_status_request()
228
229
 
229
230
  response_database = await self.command_sync_database()
@@ -234,17 +235,24 @@ class QolsysPluginRemote(QolsysPlugin):
234
235
 
235
236
  self.connected = True
236
237
  self.connected_observer.notify()
238
+
239
+ if not run_forever:
240
+ self.connected = False
241
+ self.connected_observer.notify()
242
+ self._task_manager.cancel(self._mqtt_task_listen_label)
243
+ self._task_manager.cancel(self._mqtt_task_ping_label)
244
+ await self.aiomqtt.__aexit__(None,None,None)
245
+
237
246
  break
238
247
 
239
248
  except aiomqtt.MqttError as err:
240
249
  # Receive pannel network error
241
250
  self.connected = False
242
251
  self.connected_observer.notify()
243
- await self.aiomqtt.__aexit__(None, None, None)
244
252
  self.aiomqtt = None
245
253
 
246
254
  if reconnect:
247
- LOGGER.debug("MQTT Error - %s: Reconnecting in %s seconds ...", err, self.settings.mqtt_timeout)
255
+ LOGGER.debug("MQTT Error - %s: Connect - Reconnecting in %s seconds ...", err, self.settings.mqtt_timeout)
248
256
  await asyncio.sleep(self.settings.mqtt_timeout)
249
257
  else:
250
258
  raise QolsysMqttError from err
@@ -255,14 +263,14 @@ class QolsysPluginRemote(QolsysPlugin):
255
263
  # Pannels need to be re-paired
256
264
  self.connected = False
257
265
  self.connected_observer.notify()
258
- await self.aiomqtt.__aexit__(None, None, None)
259
266
  self.aiomqtt = None
260
267
  raise QolsysSslError from err
261
268
 
262
269
  async def mqtt_ping_task(self) -> None:
263
270
  while True:
264
271
  if self.aiomqtt is not None and self.connected:
265
- await self.command_pingevent()
272
+ with contextlib.suppress(aiomqtt.MqttError):
273
+ await self.command_pingevent()
266
274
 
267
275
  await asyncio.sleep(self.settings.mqtt_ping)
268
276
 
@@ -293,25 +301,13 @@ class QolsysPluginRemote(QolsysPlugin):
293
301
  decoded_payload = base64.b64decode(zwave.get("ZWAVE_PAYLOAD","")).hex()
294
302
  LOGGER.debug("Z-Wave Response: Node(%s) - Status(%s) - Payload(%s)",zwave.get("NODE_ID",""),zwave.get("ZWAVE_COMMAND_STATUS",""),decoded_payload)
295
303
 
296
-
297
304
  except aiomqtt.MqttError as err:
298
305
  self.connected = False
299
306
  self.connected_observer.notify()
300
307
 
301
- LOGGER.debug("%s: Reconnecting in %s seconds ...", err, self.settings.mqtt_timeout)
302
- self._task_manager.cancel(self.mqtt_ping_task)
308
+ LOGGER.debug("%s: Listen - Reconnecting in %s seconds ...", err, self.settings.mqtt_timeout)
303
309
  await asyncio.sleep(self.settings.mqtt_timeout)
304
- self._task_manager.run(self.mqtt_connect_task(reconnect=True), self._mqtt_task_connect_label)
305
-
306
- except ssl.SSLError as err:
307
- # SSL error is and authentication error with invalid certificates en pki
308
- # We cannot recover from this error automaticly
309
- # Pannels need to be re-paired
310
- self.connected = False
311
- self.connected_observer.notify()
312
- await self.aiomqtt.__aexit__(None, None, None)
313
- self.aiomqtt = None
314
- raise QolsysSslError from err
310
+ self._task_manager.run(self.mqtt_connect_task(reconnect=True, run_forever=True), self._mqtt_task_connect_label)
315
311
 
316
312
  async def start_initial_pairing(self) -> bool:
317
313
  # check if random_mac exist
@@ -354,7 +350,6 @@ class QolsysPluginRemote(QolsysPlugin):
354
350
  context.load_cert_chain(certfile=self._pki.cer_file_path, keyfile=self._pki.key_file_path)
355
351
  self.certificate_exchange_server = await asyncio.start_server(self.handle_key_exchange_client,
356
352
  self.settings.plugin_ip, pairing_port, ssl=context)
357
-
358
353
  LOGGER.debug("Certificate Exchange Server Waiting for Panel")
359
354
  LOGGER.debug("Press Pair Button in IQ Remote Config Page ...")
360
355
 
@@ -371,7 +366,7 @@ class QolsysPluginRemote(QolsysPlugin):
371
366
 
372
367
  # We have client sgined certificate at this point
373
368
  # Connect to Panel MQTT to send pairing command
374
- await self._task_manager.run(self.mqtt_connect_task(reconnect=False), self._mqtt_task_connect_label)
369
+ await self._task_manager.run(self.mqtt_connect_task(reconnect=False, run_forever=False), self._mqtt_task_connect_label)
375
370
  LOGGER.debug("Plugin Pairing Completed ")
376
371
  return True
377
372
 
@@ -381,9 +376,6 @@ class QolsysPluginRemote(QolsysPlugin):
381
376
  received_signed_client_certificate = False
382
377
  received_qolsys_cer = False
383
378
 
384
- signed_certificate_data = ""
385
- qolsys_certificate_data = ""
386
-
387
379
  try:
388
380
  continue_pairing = True
389
381
  while continue_pairing:
@@ -410,7 +402,7 @@ class QolsysPluginRemote(QolsysPlugin):
410
402
  await writer.drain()
411
403
 
412
404
  # Sending CSR File to panel
413
- with open(self._pki.csr_file_path, "rb") as file:
405
+ with Path.open(self._pki.csr_file_path, "rb") as file:
414
406
  content = file.read()
415
407
  LOGGER.debug("Sending to Panel: [CSR File Content]")
416
408
  writer.write(content)
@@ -421,36 +413,26 @@ class QolsysPluginRemote(QolsysPlugin):
421
413
 
422
414
  # Read signed certificate data
423
415
  if (received_panel_mac and not received_signed_client_certificate and not received_qolsys_cer):
416
+ request = await reader.readuntil(b"sent")
417
+ if request.endswith(b"sent"):
418
+ request = request[:-5]
424
419
 
425
- request = (await reader.readline())
426
- signed_certificate_data += request.decode()
427
-
428
- if "sent" in request.decode():
429
- certificates = [item for item in signed_certificate_data.split("sent") if item]
430
- LOGGER.debug("Saving [Signed Client Certificate]")
431
- with open(self._pki.secure_file_path, "wb") as f:
432
- f.write(certificates[0].encode())
433
- received_signed_client_certificate = True
434
-
435
- if len(certificates) > 1:
436
- qolsys_certificate_data += certificates[1]
420
+ LOGGER.debug("Saving [Signed Client Certificate]")
421
+ with Path.open(self._pki.secure_file_path, "wb") as f:
422
+ f.write(request)
423
+ received_signed_client_certificate = True
437
424
 
438
425
  # Read qolsys certificate data
439
426
  if (received_panel_mac and received_signed_client_certificate and not received_qolsys_cer):
440
-
441
- request = (await reader.readline())
442
- qolsys_certificate_data += request.decode()
443
-
444
- if "sent" in request.decode():
427
+ request = await reader.readuntil(b"sent")
428
+ if request.endswith(b"sent"):
429
+ request = request[:-5]
430
+
431
+ LOGGER.debug("Saving [Qolsys Certificate]")
432
+ with Path.open(self._pki.qolsys_cer_file_path, "wb") as f:
433
+ f.write(request)
434
+ received_qolsys_cer = True
445
435
  continue_pairing = False
446
- certificates = [item for item in qolsys_certificate_data.split("sent") if item]
447
-
448
- LOGGER.debug("Saving [Qolsys Certificate]")
449
- with open(self._pki.qolsys_cer_file_path, "w") as f:
450
- f.write(certificates[0])
451
- received_qolsys_cer = True
452
- continue_pairing = False
453
- writer.close()
454
436
 
455
437
  continue
456
438
 
@@ -462,6 +444,7 @@ class QolsysPluginRemote(QolsysPlugin):
462
444
 
463
445
  finally:
464
446
  writer.close()
447
+ await writer.wait_closed()
465
448
  self.certificate_exchange_server.close()
466
449
 
467
450
  async def send_command(self, topic: str, json_payload: str, request_id: str) -> dict:
@@ -1185,11 +1168,11 @@ class QolsysPluginRemote(QolsysPlugin):
1185
1168
  LOGGER.debug("MQTT:Receiving zwave_switch_binary command")
1186
1169
 
1187
1170
 
1188
- async def command_arm(self, partition_id: str, arming_type: str, user_code: str = "", exit_sounds: bool = False,
1171
+ async def command_arm(self, partition_id: str, arming_type: str, user_code: str = "", exit_sounds: bool = False, # noqa: PLR0913
1189
1172
  instant_arm: bool = False, entry_delay: bool = True) -> bool:
1190
1173
 
1191
- LOGGER.debug("MQTT: Sending arm command: partition%s, arming_type:%s, secure_arm:%s, exit_sounds:%s, instant_arm: %s, entry_delay:%s",
1192
- partition_id, arming_type, self.panel.SECURE_ARMING,exit_sounds,instant_arm,entry_delay)
1174
+ LOGGER.debug("MQTT: Sending arm command: partition%s, arming_type:%s, exit_sounds:%s, instant_arm: %s, entry_delay:%s",
1175
+ partition_id, arming_type,exit_sounds,instant_arm,entry_delay)
1193
1176
 
1194
1177
  user_id = 0
1195
1178
 
@@ -14,6 +14,7 @@ class QolsysSettings:
14
14
  self._panel_mac = ""
15
15
  self._panel_ip = ""
16
16
 
17
+ # Path
17
18
  self._config_directory: Path = Path()
18
19
  self._pki_directory: Path = Path()
19
20
  self._media_directory: Path = Path()
@@ -28,6 +29,10 @@ class QolsysSettings:
28
29
  self._mqtt_qos:int = 0
29
30
  self._mqtt_remote_client_id = ""
30
31
 
32
+ # Operation
33
+ self._motion_sensor_delay:bool = True
34
+ self._motion_sensor_delay_sec:int = 310
35
+
31
36
  @property
32
37
  def random_mac(self) -> str:
33
38
  return self._random_mac
@@ -60,6 +65,18 @@ class QolsysSettings:
60
65
  def mqtt_ping(self) -> int:
61
66
  return self._mqtt_ping
62
67
 
68
+ @property
69
+ def motion_sensor_delay(self) -> bool:
70
+ return self._motion_sensor_delay
71
+
72
+ @property
73
+ def motion_sensor_delay_sec(self) -> int:
74
+ return self._motion_sensor_delay_sec
75
+
76
+ @motion_sensor_delay.setter
77
+ def motion_sensor_delay(self, value: bool) -> None:
78
+ self._motion_sensor_delay = value
79
+
63
80
  @panel_ip.setter
64
81
  def panel_ip(self, panel_ip: str) -> None:
65
82
  self._panel_ip = panel_ip
@@ -131,7 +148,7 @@ class QolsysSettings:
131
148
  LOGGER.debug("Found Plugin IP: %s", self._plugin_ip)
132
149
  return True
133
150
 
134
- def check_config_directory(self, create: bool = True) -> bool:
151
+ def check_config_directory(self, create: bool = True) -> bool: # noqa: PLR0911
135
152
  if not self.config_directory.is_dir():
136
153
  if not create:
137
154
  LOGGER.debug("config_directory not found: %s", self.config_directory)
@@ -102,6 +102,7 @@ class QolsysState(QolsysObservable):
102
102
  return
103
103
 
104
104
  self.partitions.append(new_partition)
105
+ self.partitions.sort(key=lambda x: x.id, reverse=False)
105
106
  self.state_partition_observer.notify()
106
107
 
107
108
  def partition_delete(self, partition_id: str) -> None:
@@ -128,6 +129,8 @@ class QolsysState(QolsysObservable):
128
129
  return
129
130
 
130
131
  self.scenes.append(new_scene)
132
+ self.scenes.sort(key=lambda x: x.scene_id, reverse=False)
133
+
131
134
  self.state_scene_observer.notify()
132
135
 
133
136
  def scene_delete(self, scene_id: str) -> None:
@@ -144,7 +147,6 @@ class QolsysState(QolsysObservable):
144
147
  for zone in self.zones:
145
148
  if zone.zone_id == zone_id:
146
149
  return zone
147
-
148
150
  return None
149
151
 
150
152
  def zone_add(self, new_zone: QolsysZone) -> None:
@@ -154,6 +156,7 @@ class QolsysState(QolsysObservable):
154
156
  return
155
157
 
156
158
  self.zones.append(new_zone)
159
+ self.zones.sort(key=lambda x: x.zone_id, reverse=False)
157
160
  self.state_zone_observer.notify()
158
161
 
159
162
  def zone_delete(self, zone_id: str) -> None:
@@ -181,6 +184,7 @@ class QolsysState(QolsysObservable):
181
184
  return
182
185
 
183
186
  self.zwave_devices.append(new_zwave)
187
+ self.zwave_devices.sort(key=lambda x: x.node_id, reverse=False)
184
188
  self.state_zwave_observer.notify()
185
189
 
186
190
  def zwave_delete(self, node_id: str) -> None:
@@ -247,7 +251,7 @@ class QolsysState(QolsysObservable):
247
251
  for state_zwave in self.zwave_devices:
248
252
  if state_zwave.node_id not in db_zwave_list:
249
253
  LOGGER.debug("sync_data - delete ZWave%s", state_zwave.none_id)
250
- self.zwave_delete(int(state_zwave.node_id))
254
+ self.zwave_delete(state_zwave.node_id)
251
255
 
252
256
  def sync_scenes_data(self, db_scenes: list[QolsysScene]) -> None:
253
257
  db_scene_list = []
@@ -271,7 +275,7 @@ class QolsysState(QolsysObservable):
271
275
  for state_scene in self.scenes:
272
276
  if state_scene.scene_id not in db_scene_list:
273
277
  LOGGER.debug("sync_data - delete Scene%s", state_scene.scene_id)
274
- self.scene_delete(int(state_scene.scene_id))
278
+ self.scene_delete(state_scene.scene_id)
275
279
 
276
280
  # Add new scene
277
281
  for db_scene in db_scenes:
@@ -300,7 +304,7 @@ class QolsysState(QolsysObservable):
300
304
  for state_zone in self.zones:
301
305
  if state_zone.zone_id not in db_zone_list:
302
306
  LOGGER.debug("sync_data - delete Zone%s", state_zone.zone_id)
303
- self.zone_delete(int(state_zone.zone_id))
307
+ self.zone_delete(state_zone.zone_id)
304
308
 
305
309
  # Add new zone
306
310
  for db_zone in db_zones:
@@ -2,8 +2,6 @@ import asyncio
2
2
  import logging
3
3
  from collections.abc import Coroutine
4
4
 
5
- import aiomqtt
6
-
7
5
  LOGGER = logging.getLogger(__name__)
8
6
 
9
7
 
@@ -31,14 +29,21 @@ class QolsysTaskManager:
31
29
  task.add_done_callback(_done_callback)
32
30
  return task
33
31
 
32
+ def get_task(self, label:str) -> asyncio.Task | None:
33
+ for task in self._tasks:
34
+ if task.get_name() == label:
35
+ return task
36
+ return None
37
+
34
38
  def cancel(self, label: str) -> None:
35
39
  for task in self._tasks:
36
40
  if task.get_name() == label:
37
41
  task.cancel()
38
42
 
39
- def cancel_all(self) -> None:
43
+ async def cancel_all(self) -> None:
40
44
  for task in self._tasks:
41
45
  task.cancel()
46
+ await task
42
47
 
43
48
  async def wait_all(self) -> None:
44
49
  if self._tasks:
@@ -1,5 +1,8 @@
1
+ import asyncio
1
2
  import logging
2
3
 
4
+ from qolsys_controller.settings import QolsysSettings
5
+
3
6
  from .enum import ZoneSensorGroup, ZoneSensorType, ZoneStatus
4
7
  from .observable import QolsysObservable
5
8
 
@@ -8,9 +11,13 @@ LOGGER = logging.getLogger(__name__)
8
11
 
9
12
  class QolsysZone(QolsysObservable):
10
13
 
11
- def __init__(self, data: dict) -> None:
14
+ def __init__(self, data: dict, settings: QolsysSettings) -> None:
12
15
  super().__init__()
13
16
 
17
+ self._settings = settings
18
+
19
+ self._delay_task:asyncio.Task = None
20
+
14
21
  self._zone_id = data.get("zoneid", "")
15
22
  self._sensorname = data.get("sensorname", "")
16
23
  self._sensorstatus: ZoneStatus = ZoneStatus(data.get("sensorstatus", ""))
@@ -214,21 +221,42 @@ class QolsysZone(QolsysObservable):
214
221
  def averagedBm(self, value: str) -> None:
215
222
  if self._averagedBm != value:
216
223
  self._averagedBm = value
224
+
225
+ if value == "":
226
+ self._averagedBm = 0
227
+
217
228
  self.notify()
218
229
 
219
230
  @latestdBm.setter
220
231
  def latestdBm(self, value: str) -> None:
221
232
  if self._latestdBm != value:
222
233
  self.latestdBm = value
234
+
235
+ if value == "":
236
+ self._latestdBm = 0
237
+
223
238
  self.notify()
224
239
 
225
240
  @sensorstatus.setter
226
241
  def sensorstatus(self, new_value: ZoneStatus) -> None:
242
+ if self._settings.motion_sensor_delay and self._sensortype in {ZoneSensorType.MOTION, ZoneSensorType.PANEL_MOTION}:
243
+ if new_value == ZoneStatus.IDLE:
244
+ return
245
+ if self._delay_task is not None:
246
+ self._delay_task.cancel()
247
+ self._delay_task = asyncio.create_task(self.delay_zone(ZoneStatus.IDLE))
248
+
227
249
  if self._sensorstatus != new_value:
228
250
  LOGGER.debug("Zone%s (%s) - sensorstatus: %s", self._zone_id, self.sensorname, new_value)
229
251
  self._sensorstatus = new_value
230
252
  self.notify()
231
253
 
254
+ async def delay_zone(self,next_status: ZoneStatus) -> None:
255
+ await asyncio.sleep(self._settings.motion_sensor_delay_sec)
256
+ self._sensorstatus = next_status
257
+ LOGGER.debug("Zone%s (%s) - sensorstatus: %s", self._zone_id, self.sensorname, next_status)
258
+ self.notify()
259
+
232
260
  @battery_status.setter
233
261
  def battery_status(self, value: str) -> None:
234
262
  if self._battery_status != value: