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