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.
- qolsys_controller/controller.py +829 -20
- qolsys_controller/database/db.py +48 -29
- qolsys_controller/database/table.py +89 -60
- qolsys_controller/database/table_alarmedsensor.py +0 -2
- qolsys_controller/database/table_automation.py +0 -1
- qolsys_controller/database/table_country_locale.py +0 -1
- qolsys_controller/database/table_dashboard_msgs.py +1 -2
- qolsys_controller/database/table_dimmerlight.py +0 -1
- qolsys_controller/database/table_doorlock.py +0 -1
- qolsys_controller/database/table_eu_event.py +1 -2
- qolsys_controller/database/table_heat_map.py +0 -2
- qolsys_controller/database/table_history.py +4 -1
- qolsys_controller/database/table_iqremotesettings.py +0 -2
- qolsys_controller/database/table_iqrouter_network_config.py +0 -1
- qolsys_controller/database/table_iqrouter_user_device.py +0 -2
- qolsys_controller/database/table_master_slave.py +0 -1
- qolsys_controller/database/table_nest_device.py +0 -1
- qolsys_controller/database/table_output_rules.py +0 -1
- qolsys_controller/database/table_partition.py +0 -1
- qolsys_controller/database/table_pgm_outputs.py +0 -2
- qolsys_controller/database/table_powerg_device.py +0 -2
- qolsys_controller/database/table_qolsyssettings.py +0 -2
- qolsys_controller/database/table_scene.py +0 -2
- qolsys_controller/database/table_sensor.py +2 -2
- qolsys_controller/database/table_sensor_group.py +23 -0
- qolsys_controller/database/table_shades.py +0 -2
- qolsys_controller/database/table_smartsocket.py +0 -2
- qolsys_controller/database/table_state.py +0 -1
- qolsys_controller/database/table_tcc.py +0 -1
- qolsys_controller/database/table_thermostat.py +0 -1
- qolsys_controller/database/table_trouble_conditions.py +0 -2
- qolsys_controller/database/table_user.py +0 -2
- qolsys_controller/database/table_virtual_device.py +0 -2
- qolsys_controller/database/table_weather.py +0 -2
- qolsys_controller/database/table_zigbee_device.py +0 -1
- qolsys_controller/database/table_zwave_association_group.py +0 -1
- qolsys_controller/database/table_zwave_history.py +0 -1
- qolsys_controller/database/table_zwave_node.py +0 -1
- qolsys_controller/database/table_zwave_other.py +0 -1
- qolsys_controller/enum.py +37 -12
- qolsys_controller/enum_zwave.py +81 -36
- qolsys_controller/errors.py +9 -12
- qolsys_controller/mdns.py +7 -4
- qolsys_controller/mqtt_command.py +119 -0
- qolsys_controller/mqtt_command_queue.py +5 -4
- qolsys_controller/observable.py +2 -2
- qolsys_controller/panel.py +195 -151
- qolsys_controller/partition.py +129 -127
- qolsys_controller/pki.py +69 -97
- qolsys_controller/scene.py +30 -28
- qolsys_controller/settings.py +96 -50
- qolsys_controller/state.py +59 -34
- qolsys_controller/task_manager.py +8 -12
- qolsys_controller/users.py +25 -0
- qolsys_controller/utils_mqtt.py +8 -16
- qolsys_controller/weather.py +71 -0
- qolsys_controller/zone.py +242 -214
- qolsys_controller/zwave_device.py +108 -95
- qolsys_controller/zwave_dimmer.py +53 -50
- qolsys_controller/zwave_garagedoor.py +0 -1
- qolsys_controller/zwave_generic.py +2 -3
- qolsys_controller/zwave_lock.py +47 -44
- qolsys_controller/zwave_outlet.py +0 -1
- qolsys_controller/zwave_thermostat.py +112 -118
- qolsys_controller-0.0.62.dist-info/METADATA +89 -0
- qolsys_controller-0.0.62.dist-info/RECORD +69 -0
- {qolsys_controller-0.0.44.dist-info → qolsys_controller-0.0.62.dist-info}/WHEEL +1 -1
- qolsys_controller/plugin.py +0 -34
- qolsys_controller/plugin_c4.py +0 -17
- qolsys_controller/plugin_remote.py +0 -1298
- qolsys_controller-0.0.44.dist-info/METADATA +0 -93
- qolsys_controller-0.0.44.dist-info/RECORD +0 -68
- {qolsys_controller-0.0.44.dist-info → qolsys_controller-0.0.62.dist-info}/licenses/LICENSE +0 -0
qolsys_controller/controller.py
CHANGED
|
@@ -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 .
|
|
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
|
-
|
|
17
|
-
self.
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
self.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
39
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
49
|
-
|
|
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
|