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,48 +1,62 @@
1
1
  import logging
2
2
 
3
- from .enum_zwave import ThermostatFanMode, ThermostatMode
3
+ from .enum_zwave import (
4
+ BITMASK_SUPPORTED_THERMOSTAT_FAN_MODE,
5
+ BITMASK_SUPPORTED_THERMOSTAT_MODE,
6
+ BITMASK_SUPPORTED_THERMOSTAT_SETPOINT_MODE,
7
+ ThermostatFanMode,
8
+ ThermostatMode,
9
+ ThermostatSetpointMode,
10
+ ZWaveMultilevelSensorScale,
11
+ )
4
12
  from .zwave_device import QolsysZWaveDevice
5
13
 
6
14
  LOGGER = logging.getLogger(__name__)
7
15
 
8
16
 
9
17
  class QolsysThermostat(QolsysZWaveDevice):
10
-
11
- def __init__(self, thermostat_dict: dict, zwave_dict: dict) -> None:
12
-
18
+ def __init__(self, thermostat_dict: dict[str, str], zwave_dict: dict[str, str]) -> None:
13
19
  super().__init__(zwave_dict)
14
20
 
15
- self._thermostat_id = thermostat_dict.get("_id")
16
- self._thermostat_version = thermostat_dict.get("version", "")
17
- self._thermostat_opr = thermostat_dict.get("opr", "")
18
- self._thermostat_partition_id = thermostat_dict.get("partition_id", "")
19
- self._thermostat_name = thermostat_dict.get("thermostat_name", "")
20
- self._thermostat_node_id = thermostat_dict.get("node_id", "")
21
- self._thermostat_device_temp_unit = thermostat_dict.get("device_temp_unit", "") # 'F'
22
- self._thermostat_current_temp = thermostat_dict.get("current_temp", "") # "78.0"
23
- self._thermostat_target_cool_temp = thermostat_dict.get("target_cool_temp", "") # "78"
24
- self._thermostat_target_heat_temp = thermostat_dict.get("target_heat_temp", "") # "65"
25
- self._thermostat_target_temp = thermostat_dict.get("target_temp", "")
26
- self._thermostat_power_usage = thermostat_dict.get("power_usage", "")
27
- self._thermostat_mode = thermostat_dict.get("thermostat_mode", "") # "2"
28
- self._thermostat_mode_bitmask = thermostat_dict.get("thermostat_mode_bitmask", "") # "7,24"
29
- self._thermostat_fan_mode = thermostat_dict.get("fan_mode", "") # "0"
30
- self._thermostat_fan_mode_bitmask = thermostat_dict.get("fan_mode_bitmask", "") # "67"
31
- self._thermostat_set_point_mode = thermostat_dict.get("set_point_mode", "")
32
- self._thermostat_set_point_mode_bitmask = thermostat_dict.get("set_point_mode_bitmask", "") # "-122,1"
33
- self._thermostat_created_by = thermostat_dict.get("created_by", "")
34
- self._thermostat_created_date = thermostat_dict.get("created_date", "")
35
- self._thermostat_updated_by = thermostat_dict.get("updated_by", "")
36
- self._thermostat_last_updated_date = thermostat_dict.get("last_updated_date", "")
37
- self._thermostat_mode_updated_time = thermostat_dict.get("thermostat_mode_updated_time", "")
38
- self._thermostat_fan_mode_updated_time = thermostat_dict.get("fan_mode_updated_time", "")
39
- self._thermostat_set_point_mode_updated_time = thermostat_dict.get("set_point_mode_updated_time", "")
40
- self._thermostat_target_cool_temp_updated_time = thermostat_dict.get("target_cool_temp_updated_time", "")
41
- self._thermostat_target_heat_temp_updated_time = thermostat_dict.get("target_heat_temp_updated_time", "")
42
- self._thermostat_current_temp_updated_time = thermostat_dict.get("current_temp_updated_time", "")
43
- self._thermostat_endpoint = thermostat_dict.get("endpoint", "")
44
- self._thermostat_paired_status = thermostat_dict.get("paired_status", "")
45
- self._thermostat_configuration_parameter = thermostat_dict.get("configuration_parameter", "")
21
+ self._thermostat_id: str = thermostat_dict.get("_id", "")
22
+ self._thermostat_version: str = thermostat_dict.get("version", "")
23
+ self._thermostat_opr: str = thermostat_dict.get("opr", "")
24
+ self._thermostat_partition_id: str = thermostat_dict.get("partition_id", "")
25
+ self._thermostat_name: str = thermostat_dict.get("thermostat_name", "")
26
+ self._thermostat_node_id: str = thermostat_dict.get("node_id", "")
27
+ self._thermostat_device_temp_unit: str = thermostat_dict.get("device_temp_unit", "") # 'F'
28
+ self._thermostat_current_temp: str = thermostat_dict.get("current_temp", "") # "78.0"
29
+ self._thermostat_target_cool_temp: str = thermostat_dict.get("target_cool_temp", "") # "78"
30
+ self._thermostat_target_heat_temp: str = thermostat_dict.get("target_heat_temp", "") # "65"
31
+ self._thermostat_target_temp: str = thermostat_dict.get("target_temp", "")
32
+ self._thermostat_power_usage: str = thermostat_dict.get("power_usage", "")
33
+ self._thermostat_mode: str = thermostat_dict.get("thermostat_mode", "") # "2"
34
+ self._thermostat_mode_bitmask: str = thermostat_dict.get("thermostat_mode_bitmask", "") # "7,24"
35
+ self._thermostat_fan_mode: str = thermostat_dict.get("fan_mode", "") # "0"
36
+ self._thermostat_fan_mode_bitmask: str = thermostat_dict.get("fan_mode_bitmask", "") # "67"
37
+ self._thermostat_set_point_mode: str = thermostat_dict.get("set_point_mode", "")
38
+ self._thermostat_set_point_mode_bitmask: str = thermostat_dict.get("set_point_mode_bitmask", "") # "-122,1"
39
+ self._thermostat_created_by: str = thermostat_dict.get("created_by", "")
40
+ self._thermostat_created_date: str = thermostat_dict.get("created_date", "")
41
+ self._thermostat_updated_by: str = thermostat_dict.get("updated_by", "")
42
+ self._thermostat_last_updated_date: str = thermostat_dict.get("last_updated_date", "")
43
+ self._thermostat_mode_updated_time: str = thermostat_dict.get("thermostat_mode_updated_time", "")
44
+ self._thermostat_fan_mode_updated_time: str = thermostat_dict.get("fan_mode_updated_time", "")
45
+ self._thermostat_set_point_mode_updated_time: str = thermostat_dict.get("set_point_mode_updated_time", "")
46
+ self._thermostat_target_cool_temp_updated_time: str = thermostat_dict.get("target_cool_temp_updated_time", "")
47
+ self._thermostat_target_heat_temp_updated_time: str = thermostat_dict.get("target_heat_temp_updated_time", "")
48
+ self._thermostat_current_temp_updated_time: str = thermostat_dict.get("current_temp_updated_time", "")
49
+ self._thermostat_endpoint: str = thermostat_dict.get("endpoint", "")
50
+ self._thermostat_paired_status: str = thermostat_dict.get("paired_status", "")
51
+ self._thermostat_configuration_parameter: str = thermostat_dict.get("configuration_parameter", "")
52
+ self._thermostat_operating_state: str = thermostat_dict.get("operating_state", "")
53
+ self._thermostat_setpoint_capabilites = thermostat_dict.get("setpoint_capabilites", "")
54
+
55
+ self._thermostat_current_humidity: float | None = None
56
+
57
+ # -----------------------------
58
+ # properties + setters
59
+ # -----------------------------
46
60
 
47
61
  @property
48
62
  def thermostat_node_id(self) -> str:
@@ -52,44 +66,51 @@ class QolsysThermostat(QolsysZWaveDevice):
52
66
  def thermostat_name(self) -> str:
53
67
  return self._thermostat_name
54
68
 
55
- @property
56
- def thermostat_device_temp_unit(self) -> str:
57
- return self._thermostat_device_temp_unit
58
-
59
- @property
60
- def thermostat_current_temp(self) -> str:
61
- return self._thermostat_current_temp
62
-
63
- @property
64
- def thermostat_target_cool_temp(self) -> str:
65
- return self._thermostat_target_cool_temp
66
-
67
- @property
68
- def thermostat_target_heat_temp(self) -> str:
69
- return self._thermostat_target_heat_temp
69
+ @thermostat_name.setter
70
+ def thermostat_name(self, value: str) -> None:
71
+ if self._thermostat_name != value:
72
+ LOGGER.debug("Thermostat%s (%s) - name: %s", self.thermostat_node_id, self.thermostat_name, value)
73
+ self._thermostat_name = value
74
+ self.notify()
70
75
 
71
76
  @property
72
- def thermostat_target_temp(self) -> str:
73
- return self._thermostat_target_temp
77
+ def thermostat_operating_state(self) -> str:
78
+ return self._thermostat_operating_state
79
+
80
+ @thermostat_operating_state.setter
81
+ def thermostat_operating_state(self, value: str) -> None:
82
+ if self._thermostat_operating_state != value:
83
+ LOGGER.debug("Thermostat%s (%s) - operating_state: %s", self.thermostat_node_id, self.thermostat_name, value)
84
+ self._thermostat_operating_state = value
85
+ self.notify()
74
86
 
75
87
  @property
76
- def thermostat_mode(self) -> str:
77
- return self._thermostat_mode
88
+ def thermostat_configuration_parameter(self) -> str:
89
+ return self._thermostat_configuration_parameter
90
+
91
+ @thermostat_configuration_parameter.setter
92
+ def thermostat_configuration_parameter(self, value: str) -> None:
93
+ if self._thermostat_configuration_parameter != value:
94
+ LOGGER.debug(
95
+ "Thermostat%s (%s) - configuration_parameter: %s", self.thermostat_node_id, self.thermostat_name, value
96
+ )
97
+ self._thermostat_configuration_parameter = value
98
+ self.notify()
78
99
 
79
100
  @property
80
- def thermostat_fan_mode(self) -> str:
81
- return self.thermostat_fan_mode
101
+ def thermostat_setpoint_capabilites(self) -> str:
102
+ return self._thermostat_setpoint_capabilites
103
+
104
+ @thermostat_setpoint_capabilites.setter
105
+ def thermostat_setpoint_capabilites(self, value: str) -> None:
106
+ if self._thermostat_setpoint_capabilites != value:
107
+ LOGGER.debug("Thermostat%s (%s) - setpoint_capabilites: %s", self.thermostat_node_id, self.thermostat_name, value)
108
+ self._thermostat_setpoint_capabilites = value
109
+ self.notify()
82
110
 
83
111
  @property
84
- def thermostat_set_point_mode(self) -> str:
85
- return self._thermostat_set_point_mode
86
-
87
- @thermostat_name.setter
88
- def thermostat_name(self, value: str) -> None:
89
- if self._thermostat_name != value:
90
- LOGGER.debug("Thermostat%s (%s) - name: %s", self.thermostat_node_id, self.thermostat_name, value)
91
- self._thermostat_name = value
92
- self.notify()
112
+ def thermostat_device_temp_unit(self) -> str:
113
+ return self._thermostat_device_temp_unit
93
114
 
94
115
  @thermostat_device_temp_unit.setter
95
116
  def thermostat_device_temp_unit(self, value: str) -> None:
@@ -98,6 +119,10 @@ class QolsysThermostat(QolsysZWaveDevice):
98
119
  self._thermostat_device_temp_unit = value
99
120
  self.notify()
100
121
 
122
+ @property
123
+ def thermostat_current_temp(self) -> str:
124
+ return self._thermostat_current_temp
125
+
101
126
  @thermostat_current_temp.setter
102
127
  def thermostat_current_temp(self, value: str) -> None:
103
128
  if self._thermostat_current_temp != value:
@@ -105,6 +130,10 @@ class QolsysThermostat(QolsysZWaveDevice):
105
130
  self._thermostat_current_temp = value
106
131
  self.notify()
107
132
 
133
+ @property
134
+ def thermostat_target_cool_temp(self) -> str:
135
+ return self._thermostat_target_cool_temp
136
+
108
137
  @thermostat_target_cool_temp.setter
109
138
  def thermostat_target_cool_temp(self, value: str) -> None:
110
139
  if self._thermostat_target_cool_temp != value:
@@ -112,6 +141,10 @@ class QolsysThermostat(QolsysZWaveDevice):
112
141
  self._thermostat_target_cool_temp = value
113
142
  self.notify()
114
143
 
144
+ @property
145
+ def thermostat_target_heat_temp(self) -> str:
146
+ return self._thermostat_target_heat_temp
147
+
115
148
  @thermostat_target_heat_temp.setter
116
149
  def thermostat_target_heat_temp(self, value: str) -> None:
117
150
  if self._thermostat_target_heat_temp != value:
@@ -119,6 +152,10 @@ class QolsysThermostat(QolsysZWaveDevice):
119
152
  self._thermostat_target_heat_temp = value
120
153
  self.notify()
121
154
 
155
+ @property
156
+ def thermostat_target_temp(self) -> str:
157
+ return self._thermostat_target_temp
158
+
122
159
  @thermostat_target_temp.setter
123
160
  def thermostat_target_temp(self, value: str) -> None:
124
161
  if self._thermostat_target_temp != value:
@@ -126,20 +163,82 @@ class QolsysThermostat(QolsysZWaveDevice):
126
163
  self._thermostat_target_temp = value
127
164
  self.notify()
128
165
 
166
+ @property
167
+ def thermostat_mode(self) -> ThermostatMode | None:
168
+ value = self._thermostat_mode.strip("[]").split(",")
169
+
170
+ if len(value) > 1:
171
+ LOGGER.error(
172
+ "Thermostat%s (%s) - thermostat_mode has multiple values: %s",
173
+ self.thermostat_node_id,
174
+ self.thermostat_name,
175
+ value,
176
+ )
177
+ return None
178
+
179
+ try:
180
+ int_value = int(value[0])
181
+ for mode in ThermostatMode:
182
+ if int_value == mode.value:
183
+ return mode
184
+ return None
185
+ except ValueError:
186
+ LOGGER.error(
187
+ "Thermostat%s (%s) - thermostat_mode value is not an integer: %s",
188
+ self.thermostat_node_id,
189
+ self.thermostat_name,
190
+ value,
191
+ )
192
+ return None
193
+
129
194
  @thermostat_mode.setter
130
195
  def thermostat_mode(self, value: str) -> None:
131
- if self._thermostat_mode != value:
132
- LOGGER.debug("Thermostat%s (%s) - mode: %s", self.thermostat_node_id, self.thermostat_name, value)
133
- self._thermostat_mode = value
196
+ new_value = value.strip("[]")
197
+ if self._thermostat_mode != new_value:
198
+ LOGGER.debug("Thermostat%s (%s) - mode: %s", self.thermostat_node_id, self.thermostat_name, new_value)
199
+ self._thermostat_mode = new_value
134
200
  self.notify()
135
201
 
202
+ @property
203
+ def thermostat_fan_mode(self) -> ThermostatFanMode | None:
204
+ value = self._thermostat_fan_mode.strip("[]").split(",")
205
+
206
+ if len(value) > 1:
207
+ LOGGER.error(
208
+ "Thermostat%s (%s) - thermostat_fan_mode has multiple values: %s",
209
+ self.thermostat_node_id,
210
+ self.thermostat_name,
211
+ value,
212
+ )
213
+ return None
214
+
215
+ try:
216
+ int_value = int(value[0])
217
+ for mode in ThermostatFanMode:
218
+ if int_value == mode.value:
219
+ return mode
220
+ return None
221
+ except ValueError:
222
+ LOGGER.error(
223
+ "Thermostat%s (%s) - thermostat_fan_mode value is not an integer: %s",
224
+ self.thermostat_node_id,
225
+ self.thermostat_name,
226
+ value,
227
+ )
228
+ return None
229
+
136
230
  @thermostat_fan_mode.setter
137
231
  def thermostat_fan_mode(self, value: str) -> None:
138
- if self._thermostat_fan_mode != value:
139
- LOGGER.debug("Thermostat%s (%s) - fan_mode: %s", self.thermostat_node_id, self.thermostat_name, value)
140
- self._thermostat_fan_mode = value
232
+ new_value = value.strip("[]")
233
+ if self._thermostat_fan_mode != new_value:
234
+ LOGGER.debug("Thermostat%s (%s) - fan_mode: %s", self.thermostat_node_id, self.thermostat_name, new_value)
235
+ self._thermostat_fan_mode = new_value
141
236
  self.notify()
142
237
 
238
+ @property
239
+ def thermostat_set_point_mode(self) -> str:
240
+ return self._thermostat_set_point_mode.strip("[]")
241
+
143
242
  @thermostat_set_point_mode.setter
144
243
  def thermostat_set_point_mode(self, value: str) -> None:
145
244
  if self._thermostat_set_point_mode != value:
@@ -147,93 +246,116 @@ class QolsysThermostat(QolsysZWaveDevice):
147
246
  self._thermostat_set_point_mode = value
148
247
  self.notify()
149
248
 
150
- def update_thermostat(self, data: dict) -> None: # noqa: C901, PLR0912, PLR0915
249
+ @property
250
+ def thermostat_current_humidity(self) -> float | None:
251
+ sensor = self.multilevelsensor_value(ZWaveMultilevelSensorScale.RELATIVE_HUMIDITY)
252
+
253
+ if not sensor:
254
+ return None
255
+
256
+ if len(sensor) == 1:
257
+ return sensor[0].value
258
+
259
+ LOGGER.error("Multiple humidity sensor present")
260
+ return sensor[0].value
261
+
262
+ def update_raw(self, payload: bytes) -> None:
263
+ pass
264
+
265
+ def update_thermostat(self, data: dict[str, str]) -> None: # noqa: C901, PLR0912, PLR0915
151
266
  # Check if we are updating same none_id
152
267
  node_id_update = data.get("node_id", "")
153
- if node_id_update != self.thermostat_node_id_node_id:
268
+ if node_id_update != self.thermostat_node_id:
154
269
  LOGGER.error(
155
270
  "Updating Thermostat '%s' (%s) with Thermostat '%s' (different id)",
156
- self.thermostat_node_id, self.thermostat_name, node_id_update,
271
+ self.thermostat_node_id,
272
+ self.thermostat_name,
273
+ node_id_update,
157
274
  )
158
275
  return
159
276
 
160
277
  self.start_batch_update()
161
278
 
162
279
  if "version" in data:
163
- self._thermostat_version = data.get("version")
280
+ self._thermostat_version = data.get("version", "")
164
281
  if "opr" in data:
165
- self._thermostat_opr = data.get("opr")
282
+ self._thermostat_opr = data.get("opr", "")
166
283
  if "partition_id" in data:
167
- self._thermostat_partition_id = data.get("partition_id")
284
+ self._thermostat_partition_id = data.get("partition_id", "")
168
285
  if "thermostat_name" in data:
169
- self.thermostat_name = data.get("thermostat_name")
286
+ self.thermostat_name = data.get("thermostat_name", "")
170
287
  if "device_temp_unit" in data:
171
- self.thermostat_device_temp_unit = data.get("device_temp_unit")
288
+ self.thermostat_device_temp_unit = data.get("device_temp_unit", "")
172
289
  if "current_temp" in data:
173
- self.thermostat_current_temp = data.get("current_temp")
290
+ self.thermostat_current_temp = data.get("current_temp", "")
174
291
  if "target_cool_temp" in data:
175
- self.thermostat_target_cool_temp = data.get("target_cool_temp")
292
+ self.thermostat_target_cool_temp = data.get("target_cool_temp", "")
176
293
  if "target_heat_temp" in data:
177
- self.thermostat_target_heat_temp = data.get("target_heat_temp")
294
+ self.thermostat_target_heat_temp = data.get("target_heat_temp", "")
178
295
  if "target_temp" in data:
179
- self.thermostat_target_temp = data.get("target_temp")
296
+ self.thermostat_target_temp = data.get("target_temp", "")
180
297
  if "power_usage" in data:
181
- self._thermostat_power_usage = data.get("power_usage")
298
+ self._thermostat_power_usage = data.get("power_usage", "")
182
299
  if "thermostat_mode" in data:
183
- self.thermostat_mode = data.get("thermostat_mode")
300
+ self.thermostat_mode = data.get("thermostat_mode", "")
184
301
  if "thermostat_mode_bitmask" in data:
185
- self._thermostat_mode_bitmask = data.get("thermostat_mode_bitmask")
302
+ self._thermostat_mode_bitmask = data.get("thermostat_mode_bitmask", "")
186
303
  if "fan_mode" in data:
187
- self.thermostat_fan_mode = data.get("fan_mode")
304
+ self.thermostat_fan_mode = data.get("fan_mode", "")
188
305
  if "fan_mode_bitmask" in data:
189
- self._thermostat_fan_mode_bitmask = data.get("fan_mode_bitmask")
306
+ self._thermostat_fan_mode_bitmask = data.get("fan_mode_bitmask", "")
190
307
  if "set_point_mode" in data:
191
- self.thermostat_set_point_mode = data.get("set_point_mode")
308
+ self.thermostat_set_point_mode = data.get("set_point_mode", "")
192
309
  if "set_point_mode_bitmask" in data:
193
- self._thermostat_set_point_mode_bitmask = data.get("set_point_mode_bitmask")
310
+ self._thermostat_set_point_mode_bitmask = data.get("set_point_mode_bitmask", "")
194
311
  if "created_by" in data:
195
- self._thermostat_created_by = data.get("created_by")
312
+ self._thermostat_created_by = data.get("created_by", "")
196
313
  if "updated_by" in data:
197
- self._thermostat_updated_by = data.get("updated_by")
314
+ self._thermostat_updated_by = data.get("updated_by", "")
198
315
  if "last_updated_date" in data:
199
- self._thermostat_last_updated_date = data.get("last_updated_date")
316
+ self._thermostat_last_updated_date = data.get("last_updated_date", "")
200
317
  if "thermostat_mode_updated_time" in data:
201
- self._thermostat_mode_updated_time = data.get("thermostat_mode_updated_time")
318
+ self._thermostat_mode_updated_time = data.get("thermostat_mode_updated_time", "")
202
319
  if "fan_mode_updated_time" in data:
203
- self._thermostat_fan_mode_updated_time = data.get("fan_mode_updated_time")
320
+ self._thermostat_fan_mode_updated_time = data.get("fan_mode_updated_time", "")
204
321
  if "set_point_mode_updated_time" in data:
205
- self._thermostat_set_point_mode_updated_time = data.get("set_point_mode_updated_time")
322
+ self._thermostat_set_point_mode_updated_time = data.get("set_point_mode_updated_time", "")
206
323
  if "target_cool_temp_updated_time" in data:
207
- self._thermostat_target_cool_temp_updated_time = data.get("target_cool_temp_updated_time")
324
+ self._thermostat_target_cool_temp_updated_time = data.get("target_cool_temp_updated_time", "")
208
325
  if "target_heat_temp_updated_time" in data:
209
- self._thermostat_target_heat_temp_updated_time = data.get("target_heat_temp_updated_time")
326
+ self._thermostat_target_heat_temp_updated_time = data.get("target_heat_temp_updated_time", "")
210
327
  if "current_temp_updated_time" in data:
211
- self._thermostat_current_temp_updated_time = data.get("current_temp_updated_time")
328
+ self._thermostat_current_temp_updated_time = data.get("current_temp_updated_time", "")
212
329
  if "paired_status" in data:
213
- self._thermostat_paired_status = data.get("paired_status")
330
+ self._thermostat_paired_status = data.get("paired_status", "")
214
331
  if "endpoint" in data:
215
- self._thermostat_endpoint = data.get("endpoint")
332
+ self._thermostat_endpoint = data.get("endpoint", "")
216
333
  if "configuration_parameter" in data:
217
- self._thermostat_configuration_parameter = data.get("configuration_parameter")
334
+ self._thermostat_configuration_parameter = data.get("configuration_parameter", "")
335
+ if "operating_state" in data:
336
+ self.thermostat_operating_state = data.get("operating_state", "")
337
+ if "setpoint_capabilites" in data:
338
+ self.thermostat_setpoint_capabilites = data.get("setpoint_capabilites", "")
218
339
 
219
340
  self.end_batch_update()
220
341
 
221
- def to_dict_thermostat(self) -> dict:
342
+ def to_dict_thermostat(self) -> dict[str, str]:
222
343
  return {
223
344
  "_id": self._thermostat_id,
224
345
  "version": self._thermostat_version,
225
346
  "opr": self._thermostat_opr,
226
347
  "partition_id": self._thermostat_partition_id,
227
348
  "thermostat_name": self.thermostat_name,
349
+ "node_id": self.thermostat_node_id,
228
350
  "device_temp_unit": self.thermostat_device_temp_unit,
229
351
  "current_temp": self.thermostat_current_temp,
230
352
  "target_cool_temp": self.thermostat_target_cool_temp,
231
353
  "target_heat_temp": self.thermostat_target_heat_temp,
232
354
  "target_temp": self.thermostat_target_temp,
233
355
  "power_usage": self._thermostat_power_usage,
234
- "thermostat_mode": self.thermostat_mode,
356
+ "thermostat_mode": self._thermostat_mode,
235
357
  "thermostat_mode_bitmask": self._thermostat_mode_bitmask,
236
- "fan_mode": self.thermostat_fan_mode,
358
+ "fan_mode": self._thermostat_fan_mode,
237
359
  "fan_mode_bitmask": self._thermostat_fan_mode_bitmask,
238
360
  "set_point_mode": self.thermostat_set_point_mode,
239
361
  "set_point_mode_bitmask": self._thermostat_set_point_mode_bitmask,
@@ -248,59 +370,43 @@ class QolsysThermostat(QolsysZWaveDevice):
248
370
  "current_temp_updated_time": self._thermostat_current_temp_updated_time,
249
371
  "paired_status": self._thermostat_paired_status,
250
372
  "endpoint": self._thermostat_endpoint,
251
- "configuration_parameter": self._thermostat_configuration_parameter,
373
+ "configuration_parameter": self.thermostat_configuration_parameter,
374
+ "operating_state": self.thermostat_operating_state,
375
+ "setpoint_capabilites": self._thermostat_setpoint_capabilites,
252
376
  }
253
377
 
254
- def thermostat_mode(self) -> ThermostatMode:
255
- thermostat_mode = int(self._thermostat_mode)
256
- for mode in ThermostatMode:
257
- if thermostat_mode == mode:
258
- return mode
259
- return None
260
-
261
378
  def available_thermostat_mode(self) -> list[ThermostatMode]:
262
-
263
- int_list = [int(x) for x in self._thermostat_mode_bitmask.split(",")]
379
+ int_list = [int(x) for x in self._thermostat_mode_bitmask.strip("[]").split(",")]
264
380
  byte_array = bytes(int_list)
265
381
  bitmask = int.from_bytes(byte_array, byteorder="little")
266
382
 
267
- mode_array = []
268
- for mode in ThermostatMode:
269
- if mode.value & bitmask:
270
- mode_array.append(mode)
271
-
272
- return mode_array
383
+ supported = []
384
+ for bit, mode in BITMASK_SUPPORTED_THERMOSTAT_MODE.items():
385
+ if bitmask & (1 << bit):
386
+ supported.append(mode)
273
387
 
274
- def thermostat_fan_mode(self) -> ThermostatFanMode:
275
- thermostat_fan_mode = int(self._thermostat_fan_mode)
276
- for mode in ThermostatMode:
277
- if thermostat_fan_mode == mode:
278
- return mode
279
- return None
388
+ return supported
280
389
 
281
390
  def available_thermostat_fan_mode(self) -> list[ThermostatFanMode]:
282
-
283
- int_list = [int(x) for x in self._thermostat_fan_mode_bitmask.split(",")]
391
+ int_list = [int(x) for x in self._thermostat_fan_mode_bitmask.strip("[]").split(",")]
284
392
  byte_array = bytes(int_list)
285
393
  bitmask = int.from_bytes(byte_array, byteorder="little")
286
394
 
287
- fan_mode_array = []
288
- for mode in ThermostatFanMode:
289
- if mode.value & bitmask:
290
- fan_mode_array.append(mode)
395
+ supported = []
396
+ for bit, mode in BITMASK_SUPPORTED_THERMOSTAT_FAN_MODE.items():
397
+ if bitmask & (1 << bit):
398
+ supported.append(mode)
291
399
 
292
- return fan_mode_array
400
+ return supported
293
401
 
294
- def available_thermostat_set_point_mode(self) -> list[ThermostatMode]:
295
-
296
- int_list = [int(x) for x in self._thermostat_set_point_mode_bitmask.split(",")]
402
+ def available_thermostat_set_point_mode(self) -> list[ThermostatSetpointMode]:
403
+ int_list = [int(x) for x in self._thermostat_set_point_mode_bitmask.strip("[]").split(",")]
297
404
  byte_array = bytes(int_list)
298
405
  bitmask = int.from_bytes(byte_array, byteorder="little")
299
406
 
300
- set_point_mode_array = []
301
- for mode in ThermostatMode:
302
- if mode.value & bitmask:
303
- set_point_mode_array.append(mode)
304
-
305
- return set_point_mode_array
407
+ supported = []
408
+ for bit, mode in BITMASK_SUPPORTED_THERMOSTAT_SETPOINT_MODE.items():
409
+ if bitmask & (1 << bit):
410
+ supported.append(mode)
306
411
 
412
+ return supported
@@ -0,0 +1,89 @@
1
+ Metadata-Version: 2.4
2
+ Name: qolsys-controller
3
+ Version: 0.0.87
4
+ Summary: A Python module that emulates a virtual IQ Remote device, enabling full local control of a Qolsys IQ Panel
5
+ Project-URL: Homepage, https://github.com/EHylands/QolsysController
6
+ Project-URL: Issues, https://github.com/EHylands/QolsysController/issues
7
+ Project-URL: Repository, https://github.com/EHylands/QolsysController.git
8
+ Author: Eric Hylands
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Topic :: Home Automation
14
+ Requires-Python: >=3.12
15
+ Description-Content-Type: text/markdown
16
+
17
+ # Qolsys Controller - qolsys-controller
18
+
19
+ [![Build](https://github.com/EHylands/QolsysController/actions/workflows/build.yml/badge.svg)](https://github.com/EHylands/QolsysController/actions/workflows/build.yml)
20
+
21
+ A Python module that emulates a virtual IQ Remote device, enabling full **local control** of a Qolsys IQ Panel over MQTT — no cloud access required.
22
+
23
+ ## QolsysController
24
+ - ✅ Connects directly to the **Qolsys Panel's local MQTT server as an IQ Remote**
25
+ - 🔐 Pairs by only using **Installer Code** (same procedure as standard IQ Remote pairing)
26
+ - 🔢 Supports **4-digit user codes**
27
+ - ⚠️ Uses a **custom local usercode database** — panel's internal user code verification process is not yet supported
28
+
29
+ ## ✨ Functionality Highlights
30
+
31
+ | Category | Feature | Status |
32
+ |------------------------|--------------------------------------|--------|
33
+ | **Panel** | Diagnostic Sensors | ✅ |
34
+ | | Panel Scenes | ✅ |
35
+ | | Weather Forecast | ✅ |
36
+ | | (Alarm.com Weather to Panel) | |
37
+ | **Partition** | Arming Status | ✅ |
38
+ | | Alarm State | ✅ |
39
+ | | Home Instant Arming | ✅ |
40
+ | | Home Silent Disarming (Firmware 4.6.1)| ✅ |
41
+ | | Set Exit Sounds | ✅ |
42
+ | | Set Entry Delay | ✅ |
43
+ | | TTS | 🛠️ |
44
+ | **Zones** | Sensor Status | ✅ |
45
+ | | Tamper State | ✅ |
46
+ | | Battery Level | ✅ |
47
+ | | Temperature (supported PowerG device)| ✅ |
48
+ | | Light (supported PowerG device) | ✅ |
49
+ | | Average dBm | ✅ |
50
+ | | Latest dBm | ✅ |
51
+ | **Z-Wave Devices** | Battery Level | ✅ |
52
+ | | Node Status | ✅ |
53
+ | | Control Generic Devices | TBD |
54
+ | **Z-Wave Dimmers** | Binary Switch | ✅ |
55
+ | | Multi Level Dimmer | ✅ |
56
+ | **Z-Wave Door Locks** | Lock, Unlock | ✅ |
57
+ | **Z-Wave Thermostats** | Read device status | ✅ |
58
+ | | Write device status | ✅ |
59
+ | **Z-Wave Garage Doors**| | 🛠️ |
60
+ | **Z-Wave Outlets** | | 🛠️ |
61
+
62
+
63
+ ## ⚠️ Certificate Warning
64
+
65
+ During pairing, the main panel issues **only one signed client certificate** per virtual IQ Remote. If any key files are lost or deleted, re-pairing may become impossible.
66
+
67
+ A new PKI, including a new private key, can be recreated under specific circumstances, though the precise conditions remain unknown at this time.
68
+
69
+ **Important:**
70
+ Immediately back up the following files from the `pki/` directory after initial pairing:
71
+
72
+ - `.key` (private key)
73
+ - `.cer` (certificate)
74
+ - `.csr` (certificate signing request)
75
+ - `.secure` (signed client certificate)
76
+ - `.qolsys` (Qolsys Panel public certificate)
77
+
78
+ Store these files securely.
79
+
80
+ ## 📦 Installation
81
+
82
+ ```bash
83
+ git clone https://github.com/EHylands/QolsysController.git
84
+ cd qolsys_controller
85
+ pip3.12 install -r requirements.txt
86
+
87
+ # Change panel_ip and plugin_in in main.py file
88
+ python3.12 example.py
89
+ ```