qolsys-controller 0.0.44__py3-none-any.whl → 0.0.62__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.

Potentially problematic release.


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

Files changed (73) hide show
  1. qolsys_controller/controller.py +829 -20
  2. qolsys_controller/database/db.py +48 -29
  3. qolsys_controller/database/table.py +89 -60
  4. qolsys_controller/database/table_alarmedsensor.py +0 -2
  5. qolsys_controller/database/table_automation.py +0 -1
  6. qolsys_controller/database/table_country_locale.py +0 -1
  7. qolsys_controller/database/table_dashboard_msgs.py +1 -2
  8. qolsys_controller/database/table_dimmerlight.py +0 -1
  9. qolsys_controller/database/table_doorlock.py +0 -1
  10. qolsys_controller/database/table_eu_event.py +1 -2
  11. qolsys_controller/database/table_heat_map.py +0 -2
  12. qolsys_controller/database/table_history.py +4 -1
  13. qolsys_controller/database/table_iqremotesettings.py +0 -2
  14. qolsys_controller/database/table_iqrouter_network_config.py +0 -1
  15. qolsys_controller/database/table_iqrouter_user_device.py +0 -2
  16. qolsys_controller/database/table_master_slave.py +0 -1
  17. qolsys_controller/database/table_nest_device.py +0 -1
  18. qolsys_controller/database/table_output_rules.py +0 -1
  19. qolsys_controller/database/table_partition.py +0 -1
  20. qolsys_controller/database/table_pgm_outputs.py +0 -2
  21. qolsys_controller/database/table_powerg_device.py +0 -2
  22. qolsys_controller/database/table_qolsyssettings.py +0 -2
  23. qolsys_controller/database/table_scene.py +0 -2
  24. qolsys_controller/database/table_sensor.py +2 -2
  25. qolsys_controller/database/table_sensor_group.py +23 -0
  26. qolsys_controller/database/table_shades.py +0 -2
  27. qolsys_controller/database/table_smartsocket.py +0 -2
  28. qolsys_controller/database/table_state.py +0 -1
  29. qolsys_controller/database/table_tcc.py +0 -1
  30. qolsys_controller/database/table_thermostat.py +0 -1
  31. qolsys_controller/database/table_trouble_conditions.py +0 -2
  32. qolsys_controller/database/table_user.py +0 -2
  33. qolsys_controller/database/table_virtual_device.py +0 -2
  34. qolsys_controller/database/table_weather.py +0 -2
  35. qolsys_controller/database/table_zigbee_device.py +0 -1
  36. qolsys_controller/database/table_zwave_association_group.py +0 -1
  37. qolsys_controller/database/table_zwave_history.py +0 -1
  38. qolsys_controller/database/table_zwave_node.py +0 -1
  39. qolsys_controller/database/table_zwave_other.py +0 -1
  40. qolsys_controller/enum.py +37 -12
  41. qolsys_controller/enum_zwave.py +81 -36
  42. qolsys_controller/errors.py +9 -12
  43. qolsys_controller/mdns.py +7 -4
  44. qolsys_controller/mqtt_command.py +119 -0
  45. qolsys_controller/mqtt_command_queue.py +5 -4
  46. qolsys_controller/observable.py +2 -2
  47. qolsys_controller/panel.py +195 -151
  48. qolsys_controller/partition.py +129 -127
  49. qolsys_controller/pki.py +69 -97
  50. qolsys_controller/scene.py +30 -28
  51. qolsys_controller/settings.py +96 -50
  52. qolsys_controller/state.py +59 -34
  53. qolsys_controller/task_manager.py +8 -12
  54. qolsys_controller/users.py +25 -0
  55. qolsys_controller/utils_mqtt.py +8 -16
  56. qolsys_controller/weather.py +71 -0
  57. qolsys_controller/zone.py +242 -214
  58. qolsys_controller/zwave_device.py +108 -95
  59. qolsys_controller/zwave_dimmer.py +53 -50
  60. qolsys_controller/zwave_garagedoor.py +0 -1
  61. qolsys_controller/zwave_generic.py +2 -3
  62. qolsys_controller/zwave_lock.py +47 -44
  63. qolsys_controller/zwave_outlet.py +0 -1
  64. qolsys_controller/zwave_thermostat.py +112 -118
  65. qolsys_controller-0.0.62.dist-info/METADATA +89 -0
  66. qolsys_controller-0.0.62.dist-info/RECORD +69 -0
  67. {qolsys_controller-0.0.44.dist-info → qolsys_controller-0.0.62.dist-info}/WHEEL +1 -1
  68. qolsys_controller/plugin.py +0 -34
  69. qolsys_controller/plugin_c4.py +0 -17
  70. qolsys_controller/plugin_remote.py +0 -1298
  71. qolsys_controller-0.0.44.dist-info/METADATA +0 -93
  72. qolsys_controller-0.0.44.dist-info/RECORD +0 -68
  73. {qolsys_controller-0.0.44.dist-info → qolsys_controller-0.0.62.dist-info}/licenses/LICENSE +0 -0
@@ -1,23 +1,65 @@
1
1
  #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+
4
+ import asyncio
5
+ import base64
6
+ import contextlib
7
+ import json
2
8
  import logging
9
+ import random
10
+ import ssl
11
+ import time
12
+ from typing import Any
13
+
14
+ import aiofiles
15
+ import aiomqtt
16
+
17
+ from qolsys_controller.mqtt_command import (
18
+ MQTTCommand,
19
+ MQTTCommand_Panel,
20
+ MQTTCommand_ZWave,
21
+ )
3
22
 
23
+ from .enum import PartitionAlarmState, PartitionArmingType, PartitionSystemStatus
24
+ from .enum_zwave import ThermostatFanMode, ThermostatMode, ZwaveCommand, ZwaveDeviceClass
25
+ from .errors import QolsysMqttError, QolsysSslError
26
+ from .mdns import QolsysMDNS
27
+ from .mqtt_command_queue import QolsysMqttCommandQueue
28
+ from .observable import QolsysObservable
4
29
  from .panel import QolsysPanel
5
- from .plugin_c4 import QolsysPluginC4
6
- from .plugin_remote import QolsysPluginRemote
30
+ from .pki import QolsysPKI
7
31
  from .settings import QolsysSettings
8
32
  from .state import QolsysState
33
+ from .task_manager import QolsysTaskManager
34
+ from .utils_mqtt import generate_random_mac
9
35
 
10
36
  LOGGER = logging.getLogger(__name__)
11
37
 
12
- class QolsysController:
13
38
 
39
+ class QolsysController:
14
40
  def __init__(self) -> None:
41
+ # QolsysController
42
+ self._state = QolsysState(self)
43
+ self._settings = QolsysSettings(self)
44
+ self._panel = QolsysPanel(self)
15
45
 
16
- # QolsysController Information
17
- self.plugin = None
18
- self._state = QolsysState()
19
- self._settings = QolsysSettings()
20
- self._panel = QolsysPanel(settings=self.settings, state=self.state)
46
+ self.connected = False
47
+ self.connected_observer = QolsysObservable()
48
+
49
+ # PKI
50
+ self._pki = QolsysPKI(settings=self.settings)
51
+
52
+ # Plugin
53
+ self.certificate_exchange_server: asyncio.Server | None = None
54
+ self._task_manager = QolsysTaskManager()
55
+ self._mqtt_command_queue = QolsysMqttCommandQueue()
56
+
57
+ # MQTT Client
58
+ self.aiomqtt: aiomqtt.Client | None = None
59
+ self._mqtt_task_config_label: str = "mqtt_task_config"
60
+ self._mqtt_task_listen_label: str = "mqtt_task_listen"
61
+ self._mqtt_task_connect_label: str = "mqtt_task_connect"
62
+ self._mqtt_task_ping_label: str = "mqtt_task_ping"
21
63
 
22
64
  @property
23
65
  def state(self) -> QolsysState:
@@ -31,19 +73,786 @@ class QolsysController:
31
73
  def settings(self) -> QolsysSettings:
32
74
  return self._settings
33
75
 
34
- def select_plugin(self, plugin: str) -> None:
76
+ @property
77
+ def mqtt_command_queue(self) -> QolsysMqttCommandQueue:
78
+ return self._mqtt_command_queue
79
+
80
+ def is_paired(self) -> bool:
81
+ return (
82
+ self._pki.id != ""
83
+ and self._pki.check_key_file()
84
+ and self._pki.check_cer_file()
85
+ and self._pki.check_qolsys_cer_file()
86
+ and self._pki.check_secure_file()
87
+ and self.settings.check_panel_ip()
88
+ and self.settings.check_plugin_ip()
89
+ )
90
+
91
+ async def config(self, start_pairing: bool) -> Any:
92
+ return await self._task_manager.run(self.config_task(start_pairing), self._mqtt_task_config_label)
93
+
94
+ async def config_task(self, start_pairing: bool) -> bool:
95
+ LOGGER.debug("Configuring Plugin")
96
+
97
+ # Check and created config_directory
98
+ if not self.settings.check_config_directory(create=start_pairing):
99
+ return False
100
+
101
+ # Read user file for access code
102
+ loop = asyncio.get_running_loop()
103
+ if not loop.run_in_executor(None, self.panel.read_users_file):
104
+ return False
105
+
106
+ # Config PKI
107
+ if self.settings.auto_discover_pki:
108
+ if self._pki.auto_discover_pki():
109
+ self.settings.random_mac = self._pki.formatted_id()
110
+ else:
111
+ self._pki.set_id(self.settings.random_mac)
112
+
113
+ # Set mqtt_remote_client_id
114
+ self.settings.mqtt_remote_client_id = "qolsys-controller-" + self._pki.formatted_id()
115
+ LOGGER.debug("Using MQTT remoteClientID: %s", self.settings.mqtt_remote_client_id)
116
+
117
+ # Check if plugin is paired
118
+ if self.is_paired():
119
+ LOGGER.debug("Panel is Paired")
120
+
121
+ else:
122
+ LOGGER.debug("Panel not paired")
123
+
124
+ if not start_pairing:
125
+ LOGGER.debug("Aborting pairing.")
126
+ return False
127
+
128
+ if not await self.start_initial_pairing():
129
+ LOGGER.debug("Error Pairing with Panel")
130
+ return False
131
+
132
+ LOGGER.debug("Starting Plugin Operation")
133
+
134
+ # Everything is configured
135
+ return True
136
+
137
+ async def start_operation(self) -> None:
138
+ await self._task_manager.run(self.mqtt_connect_task(reconnect=True, run_forever=True), self._mqtt_task_connect_label)
139
+
140
+ async def stop_operation(self) -> None:
141
+ LOGGER.debug("Stopping Plugin Operation")
142
+
143
+ if self.certificate_exchange_server is not None:
144
+ self.certificate_exchange_server.close()
145
+
146
+ if self.aiomqtt is not None:
147
+ await self.aiomqtt.__aexit__(None, None, None)
148
+ self.aiomqtt = None
149
+
150
+ self._task_manager.cancel(self._mqtt_task_connect_label)
151
+ self._task_manager.cancel(self._mqtt_task_listen_label)
152
+ self._task_manager.cancel(self._mqtt_task_ping_label)
153
+ self._task_manager.cancel(self._mqtt_task_config_label)
154
+
155
+ self.connected = False
156
+ self.connected_observer.notify()
157
+
158
+ async def mqtt_connect_task(self, reconnect: bool, run_forever: bool) -> None:
159
+ # Configure TLS parameters for MQTT connection
160
+ tls_params = aiomqtt.TLSParameters(
161
+ ca_certs=str(self._pki.qolsys_cer_file_path),
162
+ certfile=str(self._pki.secure_file_path),
163
+ keyfile=str(self._pki.key_file_path),
164
+ cert_reqs=ssl.CERT_REQUIRED,
165
+ tls_version=ssl.PROTOCOL_TLSv1_2,
166
+ ciphers="ALL:@SECLEVEL=0",
167
+ )
168
+
169
+ LOGGER.debug("MQTT: Connecting ...")
170
+
171
+ self._task_manager.cancel(self._mqtt_task_listen_label)
172
+ self._task_manager.cancel(self._mqtt_task_ping_label)
173
+
174
+ while True:
175
+ try:
176
+ self.aiomqtt = aiomqtt.Client(
177
+ hostname=self.settings.panel_ip,
178
+ port=8883,
179
+ tls_params=tls_params,
180
+ tls_insecure=True,
181
+ clean_session=True,
182
+ timeout=self.settings.mqtt_timeout,
183
+ identifier=self.settings.mqtt_remote_client_id,
184
+ )
185
+
186
+ await self.aiomqtt.__aenter__()
187
+
188
+ LOGGER.info("MQTT: Client Connected")
189
+
190
+ # Subscribe to panel internal database updates
191
+ await self.aiomqtt.subscribe("iq2meid")
192
+
193
+ # Subscribte to MQTT private response
194
+ await self.aiomqtt.subscribe("response_" + self.settings.random_mac, qos=self.settings.mqtt_qos)
195
+
196
+ # Subscribe to Z-Wave response
197
+ await self.aiomqtt.subscribe("ZWAVE_RESPONSE", qos=self.settings.mqtt_qos)
198
+
199
+ # Only log all traffic for debug purposes
200
+ if self.settings.log_mqtt_mesages:
201
+ # Subscribe to MQTT commands send to panel by other devices
202
+ await self.aiomqtt.subscribe("mastermeid", qos=self.settings.mqtt_qos)
203
+
204
+ # Subscribe to all topics
205
+ # await self.aiomqtt.subscribe("#", qos=self.settings.mqtt_qos)
206
+
207
+ self._task_manager.run(self.mqtt_listen_task(), self._mqtt_task_listen_label)
208
+ self._task_manager.run(self.mqtt_ping_task(), self._mqtt_task_ping_label)
209
+
210
+ response_connect = await self.command_connect()
211
+
212
+ self.panel.imei = response_connect.get("master_imei", "")
213
+ self.panel.product_type = response_connect.get("primary_product_type", "")
214
+
215
+ await self.command_pingevent()
216
+ await self.command_pair_status_request()
217
+
218
+ response_database = await self.command_sync_database()
219
+ LOGGER.debug("MQTT: Updating State from syncdatabase")
220
+ self.panel.load_database(response_database.get("fulldbdata")) # type: ignore[arg-type]
221
+ self.panel.dump()
222
+ self.state.dump()
223
+
224
+ self.connected = True
225
+ self.connected_observer.notify()
226
+
227
+ if not run_forever:
228
+ self.connected = False
229
+ self.connected_observer.notify()
230
+ self._task_manager.cancel(self._mqtt_task_listen_label)
231
+ self._task_manager.cancel(self._mqtt_task_ping_label)
232
+ await self.aiomqtt.__aexit__(None, None, None)
233
+
234
+ break
235
+
236
+ except aiomqtt.MqttError as err:
237
+ # Receive pannel network error
238
+ self.connected = False
239
+ self.connected_observer.notify()
240
+ self.aiomqtt = None
241
+
242
+ if reconnect:
243
+ LOGGER.debug("MQTT Error - %s: Connect - Reconnecting in %s seconds ...", err, self.settings.mqtt_timeout)
244
+ await asyncio.sleep(self.settings.mqtt_timeout)
245
+ else:
246
+ raise QolsysMqttError from err
247
+
248
+ except ssl.SSLError as err:
249
+ # SSL error is and authentication error with invalid certificates en pki
250
+ # We cannot recover from this error automaticly
251
+ # Pannels need to be re-paired
252
+ self.connected = False
253
+ self.connected_observer.notify()
254
+ self.aiomqtt = None
255
+ raise QolsysSslError from err
256
+
257
+ async def mqtt_ping_task(self) -> None:
258
+ while True:
259
+ if self.aiomqtt is not None and self.connected:
260
+ with contextlib.suppress(aiomqtt.MqttError):
261
+ await self.command_pingevent()
262
+
263
+ await asyncio.sleep(self.settings.mqtt_ping)
264
+
265
+ async def mqtt_listen_task(self) -> None:
266
+ try:
267
+ async for message in self.aiomqtt.messages: # type: ignore[union-attr]
268
+ if self.settings.log_mqtt_mesages: # noqa: SIM102
269
+ if isinstance(message.payload, bytes):
270
+ LOGGER.debug("MQTT TOPIC: %s\n%s", message.topic, message.payload.decode())
271
+
272
+ # Panel response to MQTT Commands
273
+ if message.topic.matches("response_" + self.settings.random_mac): # noqa: SIM102
274
+ if isinstance(message.payload, bytes):
275
+ data = json.loads(message.payload.decode())
276
+ await self._mqtt_command_queue.handle_response(data)
277
+
278
+ # Panel updates to IQ2MEID database
279
+ if message.topic.matches("iq2meid"): # noqa: SIM102
280
+ if isinstance(message.payload, bytes):
281
+ data = json.loads(message.payload.decode())
282
+ self.panel.parse_iq2meid_message(data)
283
+
284
+ # Panel Z-Wave response
285
+ if message.topic.matches("ZWAVE_RESPONSE"): # noqa: SIM102
286
+ if isinstance(message.payload, bytes):
287
+ data = json.loads(message.payload.decode())
288
+ zwave = data.get("ZWAVE_RESPONSE", "")
289
+ decoded_payload = base64.b64decode(zwave.get("ZWAVE_PAYLOAD", "")).hex()
290
+ LOGGER.debug(
291
+ "Z-Wave Response: Node(%s) - Status(%s) - Payload(%s)",
292
+ zwave.get("NODE_ID", ""),
293
+ zwave.get("ZWAVE_COMMAND_STATUS", ""),
294
+ decoded_payload,
295
+ )
296
+
297
+ except aiomqtt.MqttError as err:
298
+ self.connected = False
299
+ self.connected_observer.notify()
300
+
301
+ LOGGER.debug("%s: Listen - Reconnecting in %s seconds ...", err, self.settings.mqtt_timeout)
302
+ await asyncio.sleep(self.settings.mqtt_timeout)
303
+ self._task_manager.run(self.mqtt_connect_task(reconnect=True, run_forever=True), self._mqtt_task_connect_label)
304
+
305
+ async def start_initial_pairing(self) -> bool:
306
+ # check if random_mac exist
307
+ if self.settings.random_mac == "":
308
+ LOGGER.debug("Creating random_mac")
309
+ self.settings.random_mac = generate_random_mac()
310
+ self._pki.create(self.settings.random_mac, key_size=self.settings.key_size)
311
+
312
+ # Check if PKI is valid
313
+ self._pki.set_id(self.settings.random_mac)
314
+ LOGGER.debug("Checking PKI")
315
+ if not (self._pki.check_key_file() and self._pki.check_cer_file() and self._pki.check_csr_file()):
316
+ LOGGER.error("PKI Error")
317
+ return False
318
+
319
+ LOGGER.debug("Starting Pairing Process")
320
+
321
+ if not self.settings.check_plugin_ip():
322
+ LOGGER.error("Plugin IP Address not configured")
323
+ return False
324
+
325
+ # If we dont allready have client signed certificate, start the pairing server
326
+ if not self._pki.check_secure_file() or not self._pki.check_qolsys_cer_file() or not self.settings.check_panel_ip():
327
+ # High Level Random Pairing Port
328
+ pairing_port = random.randint(50000, 55000)
329
+
330
+ # Start Pairing mDNS Brodcast
331
+ LOGGER.debug("Starting mDNS Service Discovery: %s:%s", self.settings.plugin_ip, str(pairing_port))
332
+ mdns_server = QolsysMDNS(self.settings.plugin_ip, pairing_port)
333
+ await mdns_server.start_mdns()
334
+
335
+ # Start Key Exchange Server
336
+ LOGGER.debug("Starting Certificate Exchange Server")
337
+ context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
338
+ context.load_cert_chain(certfile=self._pki.cer_file_path, keyfile=self._pki.key_file_path)
339
+ self.certificate_exchange_server = await asyncio.start_server(
340
+ self.handle_key_exchange_client, self.settings.plugin_ip, pairing_port, ssl=context
341
+ )
342
+ LOGGER.debug("Certificate Exchange Server Waiting for Panel")
343
+ LOGGER.debug("Press Pair Button in IQ Remote Config Page ...")
344
+
345
+ async with self.certificate_exchange_server:
346
+ try:
347
+ await self.certificate_exchange_server.serve_forever()
348
+
349
+ except asyncio.CancelledError:
350
+ LOGGER.debug("Stoping Certificate Exchange Server")
351
+ await self.certificate_exchange_server.wait_closed()
352
+ LOGGER.debug("Stoping mDNS Service Discovery")
353
+ await mdns_server.stop_mdns()
354
+
355
+ LOGGER.debug("Sending MQTT Pairing Request to Panel")
356
+
357
+ # We have client sgined certificate at this point
358
+ # Connect to Panel MQTT to send pairing command
359
+ await self._task_manager.run(self.mqtt_connect_task(reconnect=False, run_forever=False), self._mqtt_task_connect_label)
360
+ LOGGER.debug("Plugin Pairing Completed ")
361
+ return True
362
+
363
+ async def handle_key_exchange_client(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None: # noqa: PLR0915
364
+ received_panel_mac = False
365
+ received_signed_client_certificate = False
366
+ received_qolsys_cer = False
367
+
368
+ try:
369
+ continue_pairing = True
370
+ while continue_pairing:
371
+ # Plugin is receiving panel_mac from panel
372
+ if not received_panel_mac and not received_signed_client_certificate and not received_qolsys_cer:
373
+ request = await reader.read(2048)
374
+ mac = request.decode()
375
+
376
+ address, port = writer.get_extra_info("peername")
377
+ LOGGER.debug("Panel Connected from: %s:%s", address, port)
378
+ LOGGER.debug("Receiving from Panel: %s", mac)
379
+
380
+ # Remove \x00 and \x01 from received string
381
+ self.settings.panel_mac = "".join(char for char in mac if char.isprintable())
382
+ self.settings.panel_ip = address
383
+ received_panel_mac = True
384
+
385
+ # Sending random_mac to panel
386
+ message = b"\x00\x11" + self.settings.random_mac.encode()
387
+ LOGGER.debug("Sending to Panel: %s", message.decode())
388
+ writer.write(message)
389
+ await writer.drain()
390
+
391
+ # Sending CSR File to panel
392
+ async with aiofiles.open(self._pki.csr_file_path, mode="rb") as f:
393
+ content = await f.read()
394
+ LOGGER.debug("Sending to Panel: [CSR File Content]")
395
+ writer.write(content)
396
+ writer.write(b"sent")
397
+ await writer.drain()
398
+
399
+ continue
400
+
401
+ # Read signed certificate data
402
+ if received_panel_mac and not received_signed_client_certificate and not received_qolsys_cer:
403
+ request = await reader.readuntil(b"sent")
404
+ if request.endswith(b"sent"):
405
+ request = request[:-4]
406
+
407
+ LOGGER.debug("Saving [Signed Client Certificate]")
408
+ async with aiofiles.open(self._pki.secure_file_path, mode="wb") as f:
409
+ await f.write(request)
410
+ received_signed_client_certificate = True
411
+
412
+ # Read qolsys certificate data
413
+ if received_panel_mac and received_signed_client_certificate and not received_qolsys_cer:
414
+ request = await reader.readuntil(b"sent")
415
+ if request.endswith(b"sent"):
416
+ request = request[:-4]
417
+
418
+ LOGGER.debug("Saving [Qolsys Certificate]")
419
+ async with aiofiles.open(self._pki.qolsys_cer_file_path, mode="wb") as f:
420
+ await f.write(request)
421
+ received_qolsys_cer = True
422
+ continue_pairing = False
423
+
424
+ continue
425
+
426
+ except asyncio.CancelledError:
427
+ LOGGER.exception("Key Exchange Server asyncio CancelledError")
428
+
429
+ except Exception:
430
+ LOGGER.exception("Key Exchange Server error")
431
+
432
+ finally:
433
+ writer.close()
434
+ await writer.wait_closed()
435
+ if self.certificate_exchange_server:
436
+ self.certificate_exchange_server.close()
437
+
438
+ async def command_connect(self) -> dict[str, Any]:
439
+ LOGGER.debug("MQTT: Sending connect command")
440
+
441
+ dhcpInfo = {
442
+ "ipaddress": "",
443
+ "gateway": "",
444
+ "netmask": "",
445
+ "dns1": "",
446
+ "dns2": "",
447
+ "dhcpServer": "",
448
+ "leaseDuration": "",
449
+ }
450
+
451
+ command = MQTTCommand(self, "connect_v204")
452
+ command.append("ipAddress", self.settings.plugin_ip)
453
+ command.append("pairing_request", True)
454
+ command.append("macAddress", self.settings.random_mac)
455
+ command.append("remoteClientID", self.settings.mqtt_remote_client_id)
456
+ command.append("softwareVersion", "4.4.1")
457
+ command.append("productType", "tab07_rk68")
458
+ command.append("bssid", "")
459
+ command.append("lastUpdateChecksum", "2132501716")
460
+ command.append("dealerIconsCheckSum", "")
461
+ command.append("remote_feature_support_version", "1")
462
+ command.append("current_battery_status", "Normal")
463
+ command.append("remote_panel_battery_percentage", 100)
464
+ command.append("remote_panel_battery_temperature", 430)
465
+ command.append("remote_panel_battery_status", 3)
466
+ command.append("remote_panel_battery_scale", 100)
467
+ command.append("remote_panel_battery_voltage", 4102)
468
+ command.append("remote_panel_battery_present", True)
469
+ command.append("remote_panel_battery_technology", "")
470
+ command.append("remote_panel_battery_level", 100)
471
+ command.append("remote_panel_battery_health", 2)
472
+ command.append("remote_panel_plugged", 1)
473
+ command.append("dhcpInfo", json.dumps(dhcpInfo))
474
+
475
+ response = await command.send_command()
476
+ LOGGER.debug("MQTT: Receiving connect command")
477
+ return response
478
+
479
+ async def command_pairing_request(self) -> dict[str, Any]:
480
+ LOGGER.debug("MQTT: Sending pairing_request command")
481
+ command = MQTTCommand(self, "connect_v204")
482
+
483
+ dhcpInfo = {
484
+ "ipaddress": "",
485
+ "gateway": "",
486
+ "netmask": "",
487
+ "dns1": "",
488
+ "dns2": "",
489
+ "dhcpServer": "",
490
+ "leaseDuration": "",
491
+ }
492
+
493
+ command.append("pairing_request", True)
494
+ command.append("ipAddress", self.settings.plugin_ip)
495
+ command.append("macAddress", self.settings.random_mac)
496
+ command.append("remoteClientID", self.settings.mqtt_remote_client_id)
497
+ command.append("softwareVersion", "4.4.1")
498
+ command.append("productType", "tab07_rk68")
499
+ command.append("bssid", "")
500
+ command.append("lastUpdateChecksum", "2132501716")
501
+ command.append("dealerIconsCheckSum", "")
502
+ command.append("remote_feature_support_version", "1")
503
+ command.append("dhcpInfo", json.dumps(dhcpInfo))
504
+
505
+ response = await command.send_command()
506
+ LOGGER.debug("MQTT: Receiving pairing_request command")
507
+ return response
508
+
509
+ async def command_pingevent(self) -> dict[str, Any]:
510
+ LOGGER.debug("MQTT: Sending pingevent command")
511
+ command = MQTTCommand(self, "pingevent")
512
+ command.append("remote_panel_status", "Active")
513
+ command.append("macAddress", self.settings.random_mac)
514
+ command.append("ipAddress", self.settings.plugin_ip)
515
+ command.append("current_battery_status", "Normal")
516
+ command.append("remote_panel_battery_percentage", 100)
517
+ command.append("remote_panel_battery_temperature", 430)
518
+ command.append("remote_panel_battery_status", 3)
519
+ command.append("remote_panel_battery_scale", 100)
520
+ command.append("remote_panel_battery_voltage", 4102)
521
+ command.append("remote_panel_battery_present", True)
522
+ command.append("remote_panel_battery_technology", "")
523
+ command.append("remote_panel_battery_level", 100)
524
+ command.append("remote_panel_battery_health", 2)
525
+ command.append("remote_panel_plugged", 1)
526
+
527
+ response = await command.send_command()
528
+ LOGGER.debug("MQTT: Receiving pingevent command")
529
+ return response
530
+
531
+ async def command_timesync(self) -> dict[str, Any]:
532
+ LOGGER.debug("MQTT: Sending timeSync command")
533
+ command = MQTTCommand(self, "timeSync")
534
+ command.append("startTimestamp", int(time.time()))
535
+ response = await command.send_command()
536
+ LOGGER.debug("MQTT: Receiving timeSync command")
537
+ return response
538
+
539
+ async def command_sync_database(self) -> dict[str, Any]:
540
+ LOGGER.debug("MQTT: Sending syncdatabase command")
541
+ command = MQTTCommand(self, "syncdatabase")
542
+ response = await command.send_command()
543
+ LOGGER.debug("MQTT: Receiving syncdatabase command")
544
+ return response
545
+
546
+ async def command_acstatus(self) -> dict[str, Any]:
547
+ LOGGER.debug("MQTT: Sending acStatus command")
548
+ command = MQTTCommand(self, "acStatus")
549
+ command.append("acStatus", "Connected")
550
+ response = await command.send_command()
551
+ LOGGER.debug("MQTT: Receiving acStatus command")
552
+ return response
553
+
554
+ async def command_dealer_logo(self) -> dict[str, Any]:
555
+ LOGGER.debug("MQTT: Sending dealerLogo command")
556
+ command = MQTTCommand(self, "dealerLogo")
557
+ response = await command.send_command()
558
+ LOGGER.debug("MQTT: Receiving dealerLogo command")
559
+ return response
560
+
561
+ async def command_pair_status_request(self) -> dict[str, Any]:
562
+ LOGGER.debug("MQTT: Sending pair_status_request command")
563
+ command = MQTTCommand(self, "pair_status_request")
564
+ response = await command.send_command()
565
+ LOGGER.debug("MQTT: Receiving pair_status_request command")
566
+ return response
567
+
568
+ async def command_disconnect(self) -> dict[str, Any]:
569
+ LOGGER.debug("MQTT: Sending disconnect command")
570
+ command = MQTTCommand(self, "disconnect")
571
+ response = await command.send_command()
572
+ LOGGER.debug("MQTT: Receiving disconnect command")
573
+ return response
574
+
575
+ async def command_ui_delay(self, partition_id: str, silent_disarming: bool = False) -> dict[str, Any] | None:
576
+ LOGGER.debug("MQTT: Sending ui_delay command")
577
+ command = MQTTCommand_Panel(self)
578
+
579
+ # partition state needs to be sent for ui_delay to work
580
+ partition = self.state.partition(partition_id)
581
+ if not partition:
582
+ LOGGER.error("command_ui_delay error: invalid partition %s", partition_id)
583
+ return None
584
+
585
+ arming_command = {
586
+ "operation_name": "ui_delay",
587
+ "panel_status": partition.system_status,
588
+ "userID": 0,
589
+ "partitionID": partition_id, # STR EXPECTED
590
+ "silentDisarming": silent_disarming,
591
+ "operation_source": 1,
592
+ "macAddress": self.settings.random_mac,
593
+ }
594
+
595
+ ipcRequest = [
596
+ {
597
+ "dataType": "string",
598
+ "dataValue": json.dumps(arming_command),
599
+ }
600
+ ]
601
+
602
+ command.append("ipcRequest", ipcRequest)
603
+ response = await command.send_command()
604
+ LOGGER.debug("MQTT: Receiving ui_delay command")
605
+ return response
606
+
607
+ async def command_disarm(
608
+ self, partition_id: str, user_code: str = "", silent_disarming: bool = False
609
+ ) -> dict[str, Any] | None:
610
+ partition = self.state.partition(partition_id)
611
+ if not partition:
612
+ LOGGER.error("MQTT: disarm command error - Unknow Partition")
613
+ return None
614
+
615
+ # Do local user code verification
616
+ user_id = 1
617
+ if self.settings.check_user_code_on_disarm:
618
+ user_id = self.panel.check_user(user_code)
619
+ if user_id == -1:
620
+ LOGGER.debug("MQTT: disarm command error - user_code error")
621
+ return None
622
+
623
+ async def get_mqtt_disarm_command(silent_disarming: bool) -> str:
624
+ if partition.alarm_state == PartitionAlarmState.ALARM:
625
+ return "disarm_from_emergency"
626
+ if partition.system_status in {
627
+ PartitionSystemStatus.ARM_AWAY_EXIT_DELAY,
628
+ PartitionSystemStatus.ARM_STAY_EXIT_DELAY,
629
+ PartitionSystemStatus.ARM_NIGHT_EXIT_DELAY,
630
+ }:
631
+ return "disarm_from_openlearn_sensor"
632
+ if partition.system_status in {
633
+ PartitionSystemStatus.ARM_AWAY,
634
+ PartitionSystemStatus.ARM_STAY,
635
+ PartitionSystemStatus.ARM_NIGHT,
636
+ }:
637
+ await self.command_ui_delay(partition_id, silent_disarming)
638
+ return "disarm_the_panel_from_entry_delay"
639
+
640
+ return "disarm_from_openlearn_sensor"
641
+
642
+ mqtt_disarm_command = await get_mqtt_disarm_command(silent_disarming)
643
+ LOGGER.debug("MQTT: Sending disarm command - check_user_code:%s", self.settings.check_user_code_on_disarm)
644
+
645
+ disarm_command = {
646
+ "operation_name": mqtt_disarm_command,
647
+ "userID": user_id,
648
+ "partitionID": int(partition_id), # INT EXPECTED
649
+ "operation_source": 1,
650
+ "macAddress": self.settings.random_mac,
651
+ }
652
+
653
+ ipc_request = [
654
+ {
655
+ "dataType": "string",
656
+ "dataValue": json.dumps(disarm_command),
657
+ }
658
+ ]
659
+
660
+ command = MQTTCommand_Panel(self)
661
+ command.append_ipc_request(ipc_request)
662
+ response = await command.send_command()
663
+ LOGGER.debug("MQTT: Receiving disarm command")
664
+ return response
665
+
666
+ async def command_arm(
667
+ self,
668
+ partition_id: str,
669
+ arming_type: PartitionArmingType,
670
+ user_code: str = "",
671
+ exit_sounds: bool = False,
672
+ instant_arm: bool = False,
673
+ entry_delay: bool = True,
674
+ ) -> dict[str, str] | None:
675
+ LOGGER.debug(
676
+ "MQTT: Sending arm command: partition%s, arming_type:%s, exit_sounds:%s, instant_arm: %s, entry_delay:%s",
677
+ partition_id,
678
+ arming_type,
679
+ exit_sounds,
680
+ instant_arm,
681
+ entry_delay,
682
+ )
683
+
684
+ user_id = 0
685
+
686
+ partition = self.state.partition(partition_id)
687
+ if not partition:
688
+ LOGGER.debug("MQTT: arm command error - Unknow Partition")
689
+ return None
690
+
691
+ if self.panel.SECURE_ARMING == "true" and self.settings.check_user_code_on_arm:
692
+ # Do local user code verification to arm if secure arming is enabled
693
+ user_id = self.panel.check_user(user_code)
694
+ if user_id == -1:
695
+ LOGGER.debug("MQTT: arm command error - user_code error")
696
+ return None
697
+
698
+ exitSoundValue = "ON"
699
+ if not exit_sounds:
700
+ exitSoundValue = "OFF"
701
+
702
+ entryDelay = "ON"
703
+ if not entry_delay:
704
+ entryDelay = "OFF"
705
+
706
+ arming_command = {
707
+ "operation_name": arming_type,
708
+ "bypass_zoneid_set": "[]",
709
+ "userID": user_id,
710
+ "partitionID": int(partition_id), # Expect Int
711
+ "exitSoundValue": exitSoundValue,
712
+ "entryDelayValue": entryDelay,
713
+ "multiplePartitionsSelected": False,
714
+ "instant_arming": instant_arm,
715
+ "final_exit_arming_selected": False,
716
+ "manually_selected_zones": "[]",
717
+ "operation_source": 1,
718
+ "macAddress": self.settings.random_mac,
719
+ }
720
+
721
+ ipc_request = [
722
+ {
723
+ "dataType": "string",
724
+ "dataValue": json.dumps(arming_command),
725
+ }
726
+ ]
727
+
728
+ command = MQTTCommand_Panel(self)
729
+ command.append_ipc_request(ipc_request)
730
+ response = await command.send_command()
731
+ LOGGER.debug("MQTT: Receiving arm command: partition%s", partition_id)
732
+ return response
733
+
734
+ async def command_panel_execute_scene(self, scene_id: str) -> dict[str, Any] | None:
735
+ LOGGER.debug("MQTT: Sending execute_scene command")
736
+ scene = self.state.scene(scene_id)
737
+ if not scene:
738
+ LOGGER.debug("MQTT: command_execute_scene Erro - Unknow Scene: %s", scene_id)
739
+ return None
740
+
741
+ scene_command = {
742
+ "operation_name": "execute_scene",
743
+ "scene_id": scene.scene_id,
744
+ "operation_source": 1,
745
+ "macAddress": self.settings.random_mac,
746
+ }
747
+
748
+ ipc_request = [
749
+ {
750
+ "dataType": "string",
751
+ "dataValue": json.dumps(scene_command),
752
+ }
753
+ ]
754
+
755
+ command = MQTTCommand_Panel(self)
756
+ command.append_ipc_request(ipc_request)
757
+ response = await command.send_command()
758
+ LOGGER.debug("MQTT: Receiving execute_scene command")
759
+ return response
760
+
761
+ async def command_zwave_switch_binary_set(self, node_id: str, status: bool) -> dict[str, Any] | None:
762
+ LOGGER.debug("MQTT: Sending set_zwave_switch_binary command - Node(%s) - Status(%s)", node_id, status)
763
+ zwave_node = self.state.zwave_device(node_id)
764
+
765
+ if not zwave_node:
766
+ LOGGER.error("switch_binary_set - Invalid node_id %s", node_id)
767
+ return None
768
+
769
+ if zwave_node.generic_device_type not in (ZwaveDeviceClass.SwitchBinary, ZwaveDeviceClass.RemoteSwitchBinary):
770
+ LOGGER.error("switch_binary_set used on invalid %s", zwave_node.generic_device_type)
771
+ return None
772
+
773
+ level = 0
774
+ if status:
775
+ level = 255
776
+
777
+ command = MQTTCommand_ZWave(self, node_id, [ZwaveCommand.SwitchBinary, 1, level])
778
+ response = await command.send_command()
779
+ LOGGER.debug("MQTT: Receiving set_zwave_switch_binary command")
780
+ return response
781
+
782
+ async def command_zwave_switch_multilevel_set(self, node_id: str, level: int) -> dict[str, Any] | None:
783
+ LOGGER.debug("MQTT: Sending switch_multilevel_set command - Node(%s) - Level(%s)", node_id, level)
784
+
785
+ zwave_node = self.state.zwave_device(node_id)
786
+ if not zwave_node:
787
+ LOGGER.error("switch_multilevel_set - Invalid node_id %s", node_id)
788
+ return None
789
+
790
+ if zwave_node.generic_device_type not in (ZwaveDeviceClass.SwitchMultilevel, ZwaveDeviceClass.RemoteSwitchMultilevel):
791
+ LOGGER.error("switch_multilevel_set used on invalid %s", zwave_node.generic_device_type)
792
+ return None
793
+
794
+ command = MQTTCommand_ZWave(self, node_id, [ZwaveCommand.SwitchMultilevel, 1, level])
795
+ response = await command.send_command()
796
+ LOGGER.debug("MQTT: Receiving set_zwave_multilevel_switch command")
797
+ return response
798
+
799
+ async def command_zwave_doorlock_set(self, node_id: str, locked: bool) -> dict[str, Any] | None:
800
+ LOGGER.debug("MQTT: Sending zwave_doorlock_set command - Node(%s) - Locked(%s)", node_id, locked)
801
+
802
+ zwave_node = self.state.zwave_device(node_id)
803
+ if not zwave_node:
804
+ LOGGER.error("doorlock_set - Invalid node_id %s", node_id)
805
+ return None
806
+
807
+ # 0 unlocked, 255 locked
808
+ lock_mode = 0
809
+ if locked:
810
+ lock_mode = 255
811
+
812
+ command = MQTTCommand_ZWave(self, node_id, [ZwaveCommand.DoorLock, 1, lock_mode])
813
+ response = await command.send_command()
814
+ LOGGER.debug("MQTT: Receiving zwave_doorlock_set command")
815
+ return response
816
+
817
+ async def command_zwave_thermostat_setpoint_set(
818
+ self, node_id: str, mode: ThermostatMode, setpoint: float
819
+ ) -> dict[str, Any] | None:
820
+ LOGGER.debug(
821
+ "MQTT: Sending zwave_thermostat_setpoint_set - Node(%s) - Mode(%s) - Setpoint(%s)", node_id, mode, setpoint
822
+ )
823
+
824
+ zwave_node = self.state.zwave_device(node_id)
825
+ if not zwave_node:
826
+ LOGGER.error("thermostat_setpoint_set - Invalid node_id %s", node_id)
827
+ return None
828
+
829
+ command = MQTTCommand_ZWave(self, node_id, [ZwaveCommand.ThermostatSetPoint, 1, mode, int(setpoint)])
830
+ response = await command.send_command()
831
+ LOGGER.debug("MQTT: Receiving zwave_thermostat_mode_set command")
832
+ return response
833
+
834
+ async def command_zwave_thermostat_mode_set(self, node_id: str, mode: ThermostatMode) -> dict[str, Any] | None:
835
+ LOGGER.debug("MQTT: Sending zwave_thermostat_mode_set command - Node(%s) - Mode(%s)", node_id, mode)
836
+
837
+ zwave_node = self.state.zwave_device(node_id)
838
+ if not zwave_node:
839
+ LOGGER.error("thermostat_mode_set - Invalid node_id %s", node_id)
840
+ return None
35
841
 
36
- match plugin:
842
+ command = MQTTCommand_ZWave(self, node_id, [ZwaveCommand.ThermostatMode, 1, mode])
843
+ response = await command.send_command()
844
+ LOGGER.debug("MQTT: Receiving zwave_thermostat_mode_set command")
845
+ return response
37
846
 
38
- case "c4":
39
- LOGGER.debug("C4 Plugin Selected")
40
- self.plugin = QolsysPluginC4(self.state, self.panel, self.settings)
41
- return
847
+ async def command_zwave_thermostat_fan_mode_set(self, node_id: str, fan_mode: ThermostatFanMode) -> dict[str, Any] | None:
848
+ LOGGER.debug("MQTT: Sending zwave_thermostat_fan_mode_set command - Node(%s) - FanMode(%s)", node_id, fan_mode)
42
849
 
43
- case "remote":
44
- LOGGER.debug("Remote Plugin Selected")
45
- self.plugin = QolsysPluginRemote(self.state, self.panel, self.settings)
46
- return
850
+ zwave_node = self.state.zwave_device(node_id)
851
+ if not zwave_node:
852
+ LOGGER.error("thermostat_fan_mode_set - Invalid node_id %s", node_id)
853
+ return None
47
854
 
48
- case _:
49
- LOGGER.debug("Unknow Plugin Selected")
855
+ command = MQTTCommand_ZWave(self, node_id, [ZwaveCommand.ThermostatFanMode, 1, fan_mode])
856
+ response = await command.send_command()
857
+ LOGGER.debug("MQTT: Receiving zwave_thermostat_fan_mode_set command")
858
+ return response