zcc-helper 3.8.1.dev1__tar.gz → 3.9.dev1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {zcc_helper-3.8.1.dev1/zcc_helper.egg-info → zcc_helper-3.9.dev1}/PKG-INFO +1 -1
- zcc_helper-3.9.dev1/tests/test_controller.py +171 -0
- zcc_helper-3.9.dev1/tests/test_manufacture_info.py +35 -0
- {zcc_helper-3.8.1.dev1 → zcc_helper-3.9.dev1}/zcc/constants.py +1 -1
- {zcc_helper-3.8.1.dev1 → zcc_helper-3.9.dev1}/zcc/controller.py +145 -9
- {zcc_helper-3.8.1.dev1 → zcc_helper-3.9.dev1}/zcc/device.py +10 -3
- zcc_helper-3.9.dev1/zcc/manufacture_info.py +88 -0
- {zcc_helper-3.8.1.dev1 → zcc_helper-3.9.dev1/zcc_helper.egg-info}/PKG-INFO +1 -1
- {zcc_helper-3.8.1.dev1 → zcc_helper-3.9.dev1}/zcc_helper.egg-info/SOURCES.txt +2 -0
- zcc_helper-3.8.1.dev1/zcc/manufacture_info.py +0 -45
- {zcc_helper-3.8.1.dev1 → zcc_helper-3.9.dev1}/LICENSE.txt +0 -0
- {zcc_helper-3.8.1.dev1 → zcc_helper-3.9.dev1}/README.md +0 -0
- {zcc_helper-3.8.1.dev1 → zcc_helper-3.9.dev1}/setup.cfg +0 -0
- {zcc_helper-3.8.1.dev1 → zcc_helper-3.9.dev1}/setup.py +0 -0
- {zcc_helper-3.8.1.dev1 → zcc_helper-3.9.dev1}/tests/test_device.py +0 -0
- {zcc_helper-3.8.1.dev1 → zcc_helper-3.9.dev1}/zcc/__init__.py +0 -0
- {zcc_helper-3.8.1.dev1 → zcc_helper-3.9.dev1}/zcc/__main__.py +0 -0
- {zcc_helper-3.8.1.dev1 → zcc_helper-3.9.dev1}/zcc/description.py +0 -0
- {zcc_helper-3.8.1.dev1 → zcc_helper-3.9.dev1}/zcc/discovery.py +0 -0
- {zcc_helper-3.8.1.dev1 → zcc_helper-3.9.dev1}/zcc/errors.py +0 -0
- {zcc_helper-3.8.1.dev1 → zcc_helper-3.9.dev1}/zcc/protocol.py +0 -0
- {zcc_helper-3.8.1.dev1 → zcc_helper-3.9.dev1}/zcc/socket.py +0 -0
- {zcc_helper-3.8.1.dev1 → zcc_helper-3.9.dev1}/zcc/trace.py +0 -0
- {zcc_helper-3.8.1.dev1 → zcc_helper-3.9.dev1}/zcc/watchdog.py +0 -0
- {zcc_helper-3.8.1.dev1 → zcc_helper-3.9.dev1}/zcc.py +0 -0
- {zcc_helper-3.8.1.dev1 → zcc_helper-3.9.dev1}/zcc_helper.egg-info/dependency_links.txt +0 -0
- {zcc_helper-3.8.1.dev1 → zcc_helper-3.9.dev1}/zcc_helper.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import unittest
|
|
3
|
+
|
|
4
|
+
from zcc.controller import ControlPoint
|
|
5
|
+
from zcc.description import ControlPointDescription
|
|
6
|
+
from zcc.device import ControlPointDevice
|
|
7
|
+
from zcc.manufacture_info import ControlPointManufactureInfo
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ControllerFirmwareTest(unittest.TestCase):
|
|
11
|
+
|
|
12
|
+
def setUp(self):
|
|
13
|
+
self.loop = asyncio.new_event_loop()
|
|
14
|
+
asyncio.set_event_loop(self.loop)
|
|
15
|
+
description = ControlPointDescription(host="127.0.0.1", port=12121)
|
|
16
|
+
self.controller = ControlPoint(description)
|
|
17
|
+
|
|
18
|
+
def tearDown(self):
|
|
19
|
+
self.controller.disconnect()
|
|
20
|
+
self.loop.close()
|
|
21
|
+
|
|
22
|
+
def test_flags_devices_behind_same_model_and_hwversion(self):
|
|
23
|
+
older = self._add_device(
|
|
24
|
+
"old_1",
|
|
25
|
+
ControlPointManufactureInfo(
|
|
26
|
+
identifier="old",
|
|
27
|
+
model="2GV Switch",
|
|
28
|
+
hwVersion="4",
|
|
29
|
+
firmwareVersion="1.20.50",
|
|
30
|
+
),
|
|
31
|
+
)
|
|
32
|
+
self._add_device(
|
|
33
|
+
"new_1",
|
|
34
|
+
ControlPointManufactureInfo(
|
|
35
|
+
identifier="new",
|
|
36
|
+
model="2GV Switch",
|
|
37
|
+
hwVersion="4",
|
|
38
|
+
firmwareVersion="3.3.4.2",
|
|
39
|
+
),
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
self.controller._ControlPoint__update_obsolete_firmware()
|
|
43
|
+
|
|
44
|
+
self.assertEqual(
|
|
45
|
+
self.controller.obsolete_firmware,
|
|
46
|
+
{"old_1": "1.20.50 < 3.3.4.2"},
|
|
47
|
+
)
|
|
48
|
+
self.assertEqual(self.controller.obsolete_firmware_devices, [older])
|
|
49
|
+
self.assertEqual(self.controller.obsolete_devices, [older])
|
|
50
|
+
|
|
51
|
+
def test_separates_peer_groups_by_hwversion(self):
|
|
52
|
+
self._add_device(
|
|
53
|
+
"older_1",
|
|
54
|
+
ControlPointManufactureInfo(
|
|
55
|
+
identifier="older",
|
|
56
|
+
model="2GV Switch",
|
|
57
|
+
hwVersion="4",
|
|
58
|
+
firmwareVersion="1.20.50",
|
|
59
|
+
),
|
|
60
|
+
)
|
|
61
|
+
self._add_device(
|
|
62
|
+
"different_hw_1",
|
|
63
|
+
ControlPointManufactureInfo(
|
|
64
|
+
identifier="different_hw",
|
|
65
|
+
model="2GV Switch",
|
|
66
|
+
hwVersion="5",
|
|
67
|
+
firmwareVersion="9.9.9",
|
|
68
|
+
),
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
self.controller._ControlPoint__update_obsolete_firmware()
|
|
72
|
+
|
|
73
|
+
self.assertEqual(self.controller.obsolete_firmware, {})
|
|
74
|
+
|
|
75
|
+
def test_flags_missing_manufacture_info(self):
|
|
76
|
+
device = ControlPointDevice(self.controller, "missing_1")
|
|
77
|
+
self.controller.devices[device.identifier] = device
|
|
78
|
+
|
|
79
|
+
self.controller._ControlPoint__update_obsolete_firmware()
|
|
80
|
+
|
|
81
|
+
self.assertEqual(
|
|
82
|
+
self.controller.obsolete_firmware,
|
|
83
|
+
{"missing_1": "missing manufacture_info"},
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
def test_flags_missing_firmware_version(self):
|
|
87
|
+
self._add_device(
|
|
88
|
+
"missing_fw_1",
|
|
89
|
+
ControlPointManufactureInfo(
|
|
90
|
+
identifier="missing_fw",
|
|
91
|
+
model="2GV Switch",
|
|
92
|
+
hwVersion="4",
|
|
93
|
+
firmwareVersion=None,
|
|
94
|
+
),
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
self.controller._ControlPoint__update_obsolete_firmware()
|
|
98
|
+
|
|
99
|
+
self.assertEqual(
|
|
100
|
+
self.controller.obsolete_firmware,
|
|
101
|
+
{"missing_fw_1": "missing firmwareVersion"},
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
def test_describe_reports_obsolete_firmware_warning(self):
|
|
105
|
+
old_device = self._add_device(
|
|
106
|
+
"old_1",
|
|
107
|
+
ControlPointManufactureInfo(
|
|
108
|
+
identifier="old",
|
|
109
|
+
model="2GV Switch",
|
|
110
|
+
hwVersion="4",
|
|
111
|
+
firmwareVersion="1.20.50",
|
|
112
|
+
),
|
|
113
|
+
)
|
|
114
|
+
old_device.properties = {"name": "Entry Switch", "roomName": "Hall"}
|
|
115
|
+
self._add_device(
|
|
116
|
+
"new_1",
|
|
117
|
+
ControlPointManufactureInfo(
|
|
118
|
+
identifier="new",
|
|
119
|
+
model="2GV Switch",
|
|
120
|
+
hwVersion="4",
|
|
121
|
+
firmwareVersion="3.3.4.2",
|
|
122
|
+
),
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
self.controller._ControlPoint__update_obsolete_firmware()
|
|
126
|
+
|
|
127
|
+
description = self.controller.describe()
|
|
128
|
+
|
|
129
|
+
self.assertIn("obsolete firmware", description)
|
|
130
|
+
self.assertIn("fw=1.20.50, fw<3.3.4.2", description)
|
|
131
|
+
|
|
132
|
+
def test_tracks_missing_states_and_reports_inline_marker(self):
|
|
133
|
+
missing_device = self._add_device(
|
|
134
|
+
"missing_1",
|
|
135
|
+
ControlPointManufactureInfo(
|
|
136
|
+
identifier="missing",
|
|
137
|
+
model="2GV Switch",
|
|
138
|
+
hwVersion="4",
|
|
139
|
+
firmwareVersion="3.3.4.2",
|
|
140
|
+
),
|
|
141
|
+
)
|
|
142
|
+
missing_device.properties = {
|
|
143
|
+
"name": "Entry Dimmer",
|
|
144
|
+
"roomName": "Hall",
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
self.controller._ControlPoint__update_missing_states()
|
|
148
|
+
|
|
149
|
+
self.assertEqual(
|
|
150
|
+
self.controller.missing_states,
|
|
151
|
+
{"missing_1": "missing states"},
|
|
152
|
+
)
|
|
153
|
+
self.assertEqual(self.controller.warnings, [missing_device])
|
|
154
|
+
|
|
155
|
+
description = self.controller.describe()
|
|
156
|
+
|
|
157
|
+
self.assertIn("incomplete data", description)
|
|
158
|
+
self.assertIn("{ MISSING }", description)
|
|
159
|
+
|
|
160
|
+
def _add_device(self, identifier, manufacture_info):
|
|
161
|
+
device = ControlPointDevice(self.controller, identifier)
|
|
162
|
+
device.manufacture_info = manufacture_info
|
|
163
|
+
self.controller.devices[identifier] = device
|
|
164
|
+
self.controller.manufacture_infos[manufacture_info.identifier] = (
|
|
165
|
+
manufacture_info
|
|
166
|
+
)
|
|
167
|
+
return device
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
if __name__ == '__main__':
|
|
171
|
+
unittest.main()
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
|
|
3
|
+
from zcc.manufacture_info import (
|
|
4
|
+
ControlPointManufactureInfo,
|
|
5
|
+
compare_firmware_versions,
|
|
6
|
+
normalise_firmware_version,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ManufactureInfoTest(unittest.TestCase):
|
|
11
|
+
|
|
12
|
+
def test_normalise_firmware_version(self):
|
|
13
|
+
self.assertEqual(normalise_firmware_version("2.10.4.2"), (2, 10, 4, 2))
|
|
14
|
+
self.assertIsNone(normalise_firmware_version("invalid"))
|
|
15
|
+
|
|
16
|
+
def test_compare_firmware_versions(self):
|
|
17
|
+
self.assertEqual(compare_firmware_versions("2.10.4.2", "2.14.4.2"), -1)
|
|
18
|
+
self.assertEqual(compare_firmware_versions("1.2", "1.2.0"), 0)
|
|
19
|
+
self.assertEqual(compare_firmware_versions("3.0", "2.9.9"), 1)
|
|
20
|
+
|
|
21
|
+
def test_cohort_key_requires_model_and_hwversion(self):
|
|
22
|
+
manufacture_info = ControlPointManufactureInfo(
|
|
23
|
+
identifier="device",
|
|
24
|
+
model="2GV Switch",
|
|
25
|
+
hwVersion="4",
|
|
26
|
+
firmwareVersion="3.3.4.2",
|
|
27
|
+
)
|
|
28
|
+
self.assertEqual(manufacture_info.cohort_key, ("2GV Switch", "4"))
|
|
29
|
+
|
|
30
|
+
manufacture_info.hwVersion = None
|
|
31
|
+
self.assertIsNone(manufacture_info.cohort_key)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
if __name__ == '__main__':
|
|
35
|
+
unittest.main()
|
|
@@ -23,7 +23,11 @@ from zcc.constants import LEVEL_BY_VERBOSITY, NAME, VERSION
|
|
|
23
23
|
from zcc.description import ControlPointDescription
|
|
24
24
|
from zcc.device import ControlPointDevice
|
|
25
25
|
from zcc.errors import ControlPointError
|
|
26
|
-
from zcc.manufacture_info import
|
|
26
|
+
from zcc.manufacture_info import (
|
|
27
|
+
ControlPointManufactureInfo,
|
|
28
|
+
compare_firmware_versions,
|
|
29
|
+
normalise_firmware_version,
|
|
30
|
+
)
|
|
27
31
|
from zcc.protocol import ControlPointProtocol
|
|
28
32
|
from zcc.socket import ControlPointSocket
|
|
29
33
|
from zcc.watchdog import ControlPointWatchdog
|
|
@@ -69,6 +73,9 @@ class ControlPoint:
|
|
|
69
73
|
|
|
70
74
|
self.devices: Dict[ControlPointDevice] = {}
|
|
71
75
|
self.manufacture_infos: Dict[ControlPointManufactureInfo] = {}
|
|
76
|
+
self.missing_states: Dict[str, str] = {}
|
|
77
|
+
self.obsolete_firmware: Dict[str, str] = {}
|
|
78
|
+
self.obsolete_firmware_baselines: Dict[tuple[str, str], str] = {}
|
|
72
79
|
|
|
73
80
|
self.actions_received = 0
|
|
74
81
|
self.manufacture_info_received = 0
|
|
@@ -148,7 +155,34 @@ class ControlPoint:
|
|
|
148
155
|
@property
|
|
149
156
|
def warnings(self) -> List[ControlPointDevice]:
|
|
150
157
|
"""Return a list of devices with empty or missing states (incomplete data)."""
|
|
151
|
-
|
|
158
|
+
|
|
159
|
+
return [
|
|
160
|
+
self.devices[identifier]
|
|
161
|
+
for identifier in self.missing_states
|
|
162
|
+
if identifier in self.devices
|
|
163
|
+
]
|
|
164
|
+
|
|
165
|
+
@property
|
|
166
|
+
def missing_state_devices(self) -> List[ControlPointDevice]:
|
|
167
|
+
"""Backward-compatible alias for devices flagged with missing states."""
|
|
168
|
+
|
|
169
|
+
return self.warnings
|
|
170
|
+
|
|
171
|
+
@property
|
|
172
|
+
def obsolete_firmware_devices(self) -> List[ControlPointDevice]:
|
|
173
|
+
"""Return a list of devices flagged with obsolete firmware."""
|
|
174
|
+
|
|
175
|
+
return [
|
|
176
|
+
self.devices[identifier]
|
|
177
|
+
for identifier in self.obsolete_firmware
|
|
178
|
+
if identifier in self.devices
|
|
179
|
+
]
|
|
180
|
+
|
|
181
|
+
@property
|
|
182
|
+
def obsolete_devices(self) -> List[ControlPointDevice]:
|
|
183
|
+
"""Backward-compatible alias for devices flagged with obsolete firmware."""
|
|
184
|
+
|
|
185
|
+
return self.obsolete_firmware_devices
|
|
152
186
|
|
|
153
187
|
async def connect(self, fast: bool = False) -> bool:
|
|
154
188
|
"""Connect to ZCC, build device table and subscribe to updates"""
|
|
@@ -314,13 +348,18 @@ class ControlPoint:
|
|
|
314
348
|
description += header + "\n"
|
|
315
349
|
|
|
316
350
|
# Add warnings section if there are devices with incomplete data
|
|
317
|
-
if self.
|
|
318
|
-
description += "| WARNING: One or more devices have
|
|
351
|
+
if self.missing_states:
|
|
352
|
+
description += "| WARNING: One or more devices have missing states. Check for state None below and upgrade firmware with Zimi app |\n"
|
|
353
|
+
description += "+" + "-" * 130 + "+\n"
|
|
354
|
+
if self.obsolete_firmware:
|
|
355
|
+
description += "| WARNING: One or more devices have obsolete firmware. Check for fw<new_version below and upgrade firmware with Zimi app |\n"
|
|
319
356
|
description += "+" + "-" * 130 + "+\n"
|
|
320
357
|
|
|
321
358
|
description_details = []
|
|
322
359
|
for key in self.manufacture_infos:
|
|
323
|
-
description_details.append(
|
|
360
|
+
description_details.append(
|
|
361
|
+
self.__describe_manufacture_info(self.manufacture_infos[key])
|
|
362
|
+
)
|
|
324
363
|
for key in self.devices:
|
|
325
364
|
description_details.append(self.devices[key].describe())
|
|
326
365
|
description_details.sort()
|
|
@@ -427,10 +466,12 @@ class ControlPoint:
|
|
|
427
466
|
|
|
428
467
|
for key in self.devices.keys():
|
|
429
468
|
identifier_msb = self.devices[key].identifier.split("_")[0]
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
469
|
+
manufacture_info = self.manufacture_infos.get(identifier_msb)
|
|
470
|
+
if manufacture_info:
|
|
471
|
+
self.devices[key].manufacture_info = manufacture_info
|
|
472
|
+
|
|
473
|
+
self.__update_missing_states()
|
|
474
|
+
self.__update_obsolete_firmware()
|
|
434
475
|
|
|
435
476
|
async def __get_states(self):
|
|
436
477
|
"""Get latest state data from controller and reset watchdog."""
|
|
@@ -673,6 +714,8 @@ class ControlPoint:
|
|
|
673
714
|
)
|
|
674
715
|
self.devices[identifier].notify_observers()
|
|
675
716
|
|
|
717
|
+
self.__update_missing_states()
|
|
718
|
+
|
|
676
719
|
if (
|
|
677
720
|
self.actions_received >= self.num_control_points
|
|
678
721
|
and not self.actions_ready.done()
|
|
@@ -720,6 +763,99 @@ class ControlPoint:
|
|
|
720
763
|
):
|
|
721
764
|
self.manufacture_info_ready.set_result(True)
|
|
722
765
|
|
|
766
|
+
def __update_obsolete_firmware(self):
|
|
767
|
+
"""Flag devices that are behind their peer firmware cohort."""
|
|
768
|
+
|
|
769
|
+
self.obsolete_firmware = {}
|
|
770
|
+
self.obsolete_firmware_baselines = {}
|
|
771
|
+
|
|
772
|
+
for manufacture_info in self.manufacture_infos.values():
|
|
773
|
+
cohort_key = manufacture_info.cohort_key
|
|
774
|
+
if cohort_key is None:
|
|
775
|
+
continue
|
|
776
|
+
if normalise_firmware_version(manufacture_info.firmwareVersion) is None:
|
|
777
|
+
continue
|
|
778
|
+
|
|
779
|
+
latest_firmware = self.obsolete_firmware_baselines.get(cohort_key)
|
|
780
|
+
if latest_firmware is None:
|
|
781
|
+
self.obsolete_firmware_baselines[cohort_key] = (
|
|
782
|
+
manufacture_info.firmwareVersion
|
|
783
|
+
)
|
|
784
|
+
continue
|
|
785
|
+
|
|
786
|
+
if (
|
|
787
|
+
compare_firmware_versions(
|
|
788
|
+
manufacture_info.firmwareVersion, latest_firmware
|
|
789
|
+
)
|
|
790
|
+
== 1
|
|
791
|
+
):
|
|
792
|
+
self.obsolete_firmware_baselines[cohort_key] = (
|
|
793
|
+
manufacture_info.firmwareVersion
|
|
794
|
+
)
|
|
795
|
+
|
|
796
|
+
for identifier, device in self.devices.items():
|
|
797
|
+
reason = self.__get_obsolete_firmware_reason(device)
|
|
798
|
+
if reason:
|
|
799
|
+
self.obsolete_firmware[identifier] = reason
|
|
800
|
+
self.logger.warning(
|
|
801
|
+
"Flagging obsolete firmware for %s: %s", identifier, reason
|
|
802
|
+
)
|
|
803
|
+
|
|
804
|
+
def __get_obsolete_firmware_reason(
|
|
805
|
+
self, device: ControlPointDevice
|
|
806
|
+
) -> str | None:
|
|
807
|
+
"""Return the reason a device is flagged as obsolete."""
|
|
808
|
+
|
|
809
|
+
manufacture_info = getattr(device, "manufacture_info", None)
|
|
810
|
+
if manufacture_info is None:
|
|
811
|
+
return "missing manufacture_info"
|
|
812
|
+
if not manufacture_info.model:
|
|
813
|
+
return "missing model"
|
|
814
|
+
if not manufacture_info.hwVersion:
|
|
815
|
+
return "missing hwVersion"
|
|
816
|
+
if not manufacture_info.firmwareVersion:
|
|
817
|
+
return "missing firmwareVersion"
|
|
818
|
+
if normalise_firmware_version(manufacture_info.firmwareVersion) is None:
|
|
819
|
+
return "invalid firmwareVersion"
|
|
820
|
+
|
|
821
|
+
baseline = self.obsolete_firmware_baselines.get(manufacture_info.cohort_key)
|
|
822
|
+
if baseline is None:
|
|
823
|
+
return "unable to compare firmwareVersion"
|
|
824
|
+
|
|
825
|
+
comparison = compare_firmware_versions(
|
|
826
|
+
manufacture_info.firmwareVersion, baseline
|
|
827
|
+
)
|
|
828
|
+
if comparison is None:
|
|
829
|
+
return "invalid firmwareVersion"
|
|
830
|
+
if comparison < 0:
|
|
831
|
+
return f"{manufacture_info.firmwareVersion} < {baseline}"
|
|
832
|
+
|
|
833
|
+
return None
|
|
834
|
+
|
|
835
|
+
def __describe_manufacture_info(
|
|
836
|
+
self, manufacture_info: ControlPointManufactureInfo
|
|
837
|
+
) -> str:
|
|
838
|
+
"""Render manufacture info, annotating obsolete firmware entries."""
|
|
839
|
+
|
|
840
|
+
description = manufacture_info.describe().rstrip("\n")
|
|
841
|
+
newer_firmware = self.obsolete_firmware_baselines.get(manufacture_info.cohort_key)
|
|
842
|
+
if newer_firmware and any(
|
|
843
|
+
device.identifier.split("_")[0] == manufacture_info.identifier
|
|
844
|
+
for device in self.obsolete_firmware_devices
|
|
845
|
+
):
|
|
846
|
+
description = description.replace(
|
|
847
|
+
" )", f", fw<{newer_firmware} )"
|
|
848
|
+
)
|
|
849
|
+
return description + "\n"
|
|
850
|
+
|
|
851
|
+
def __update_missing_states(self):
|
|
852
|
+
"""Track devices that do not yet have usable state payloads."""
|
|
853
|
+
|
|
854
|
+
self.missing_states = {}
|
|
855
|
+
for identifier, device in self.devices.items():
|
|
856
|
+
if not device.states:
|
|
857
|
+
self.missing_states[identifier] = "missing states"
|
|
858
|
+
|
|
723
859
|
def __update_properties(self, properties):
|
|
724
860
|
"""Updates zcc gateway properties"""
|
|
725
861
|
|
|
@@ -78,9 +78,9 @@ class ControlPointDevice:
|
|
|
78
78
|
if fan_speed:
|
|
79
79
|
description += "/" + str(fan_speed)
|
|
80
80
|
except IndexError:
|
|
81
|
-
description = "
|
|
81
|
+
description = "None"
|
|
82
82
|
except KeyError:
|
|
83
|
-
description = "
|
|
83
|
+
description = "None"
|
|
84
84
|
return description
|
|
85
85
|
|
|
86
86
|
@property
|
|
@@ -115,12 +115,19 @@ class ControlPointDevice:
|
|
|
115
115
|
|
|
116
116
|
def describe(self) -> str:
|
|
117
117
|
"""Returns a description of a device"""
|
|
118
|
+
actions = self.__actions
|
|
119
|
+
if self.controller and self.identifier in self.controller.missing_states:
|
|
120
|
+
if actions.endswith("}"):
|
|
121
|
+
actions = actions[:-1] + " MISSING }"
|
|
122
|
+
else:
|
|
123
|
+
actions = "{ MISSING }"
|
|
124
|
+
|
|
118
125
|
return "%-40s %-38.38s %-8.8s %-8s %s\n" % (
|
|
119
126
|
self.identifier,
|
|
120
127
|
self.location,
|
|
121
128
|
self.type,
|
|
122
129
|
self.__states,
|
|
123
|
-
|
|
130
|
+
actions,
|
|
124
131
|
)
|
|
125
132
|
|
|
126
133
|
@property
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""ControlPointManufactureInfo Class."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Tuple
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def normalise_firmware_version(version: str) -> Tuple[int, ...] | None:
|
|
8
|
+
"""Convert a dotted firmware version string to a comparable tuple."""
|
|
9
|
+
|
|
10
|
+
if not version:
|
|
11
|
+
return None
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
return tuple(int(part) for part in version.split("."))
|
|
15
|
+
except (AttributeError, TypeError, ValueError):
|
|
16
|
+
return None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def compare_firmware_versions(current: str, other: str) -> int | None:
|
|
20
|
+
"""Compare two dotted firmware versions."""
|
|
21
|
+
|
|
22
|
+
current_version = normalise_firmware_version(current)
|
|
23
|
+
other_version = normalise_firmware_version(other)
|
|
24
|
+
|
|
25
|
+
if current_version is None or other_version is None:
|
|
26
|
+
return None
|
|
27
|
+
|
|
28
|
+
max_length = max(len(current_version), len(other_version))
|
|
29
|
+
current_version += (0,) * (max_length - len(current_version))
|
|
30
|
+
other_version += (0,) * (max_length - len(other_version))
|
|
31
|
+
|
|
32
|
+
if current_version < other_version:
|
|
33
|
+
return -1
|
|
34
|
+
if current_version > other_version:
|
|
35
|
+
return 1
|
|
36
|
+
|
|
37
|
+
return 0
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class ControlPointManufactureInfo:
|
|
42
|
+
"""Data class to store ControlPoint manufacture_info."""
|
|
43
|
+
|
|
44
|
+
identifier: str = None
|
|
45
|
+
manufacturer: str = None
|
|
46
|
+
model: str = None
|
|
47
|
+
hwVersion: str = None
|
|
48
|
+
firmwareVersion: str = None
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def cohort_key(self) -> Tuple[str, str] | None:
|
|
52
|
+
"""Return the comparison cohort for peer firmware checks."""
|
|
53
|
+
|
|
54
|
+
if not self.model or not self.hwVersion:
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
return (self.model, self.hwVersion)
|
|
58
|
+
|
|
59
|
+
def describe(self) -> str:
|
|
60
|
+
"""Returns a description of a device"""
|
|
61
|
+
return "%-40s %-40.40s %-8.8s %s\n" % (
|
|
62
|
+
self.identifier,
|
|
63
|
+
f"{self.model} ({self.name})",
|
|
64
|
+
"device",
|
|
65
|
+
f"( hw={self.hwVersion}, fw={self.firmwareVersion} )",
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def name(self) -> str:
|
|
70
|
+
"""Returns simplified name of the device."""
|
|
71
|
+
|
|
72
|
+
if not self.model:
|
|
73
|
+
return "N/A"
|
|
74
|
+
|
|
75
|
+
if "Blind" in self.model:
|
|
76
|
+
return "Cover"
|
|
77
|
+
if "Fan" in self.model:
|
|
78
|
+
return "Fan"
|
|
79
|
+
if "Garage" in self.model:
|
|
80
|
+
return "Garage"
|
|
81
|
+
if "Door" in self.model:
|
|
82
|
+
return "Cover"
|
|
83
|
+
if "GPO" in self.model:
|
|
84
|
+
return "Outlet"
|
|
85
|
+
if "Switch" in self.model:
|
|
86
|
+
return "Switch"
|
|
87
|
+
|
|
88
|
+
return self.model
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
"""ControlPointManufactureInfo Class."""
|
|
2
|
-
|
|
3
|
-
from dataclasses import dataclass
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
@dataclass
|
|
7
|
-
class ControlPointManufactureInfo:
|
|
8
|
-
"""Data class to store ControlPoint manufacture_info."""
|
|
9
|
-
|
|
10
|
-
identifier: str = None
|
|
11
|
-
manufacturer: str = None
|
|
12
|
-
model: str = None
|
|
13
|
-
hwVersion: str = None
|
|
14
|
-
firmwareVersion: str = None
|
|
15
|
-
|
|
16
|
-
def describe(self) -> str:
|
|
17
|
-
"""Returns a description of a device"""
|
|
18
|
-
return "%-40s %-40.40s %-8.8s %s\n" % (
|
|
19
|
-
self.identifier,
|
|
20
|
-
f"{self.model} ({self.name})",
|
|
21
|
-
"device",
|
|
22
|
-
f"( hw={self.hwVersion}, fw={self.firmwareVersion} )",
|
|
23
|
-
)
|
|
24
|
-
|
|
25
|
-
@property
|
|
26
|
-
def name(self) -> str:
|
|
27
|
-
"""Returns simplified name of the device."""
|
|
28
|
-
|
|
29
|
-
if not self.model:
|
|
30
|
-
return "N/A"
|
|
31
|
-
|
|
32
|
-
if "Blind" in self.model:
|
|
33
|
-
return "Cover"
|
|
34
|
-
if "Fan" in self.model:
|
|
35
|
-
return "Fan"
|
|
36
|
-
if "Garage" in self.model:
|
|
37
|
-
return "Garage"
|
|
38
|
-
if "Door" in self.model:
|
|
39
|
-
return "Cover"
|
|
40
|
-
if "GPO" in self.model:
|
|
41
|
-
return "Outlet"
|
|
42
|
-
if "Switch" in self.model:
|
|
43
|
-
return "Switch"
|
|
44
|
-
|
|
45
|
-
return self.model
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|