conson-xp 2.0.1__py3-none-any.whl → 2.0.3__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.
- {conson_xp-2.0.1.dist-info → conson_xp-2.0.3.dist-info}/METADATA +1 -1
- {conson_xp-2.0.1.dist-info → conson_xp-2.0.3.dist-info}/RECORD +12 -12
- xp/__init__.py +1 -1
- xp/models/homekit/homekit_config.py +8 -0
- xp/services/actiontable/actiontable_serializer.py +4 -4
- xp/services/conbus/actiontable/actiontable_upload_service.py +8 -1
- xp/services/term/homekit_accessory_driver.py +67 -11
- xp/services/term/homekit_service.py +168 -12
- xp/term/homekit.py +23 -0
- {conson_xp-2.0.1.dist-info → conson_xp-2.0.3.dist-info}/WHEEL +0 -0
- {conson_xp-2.0.1.dist-info → conson_xp-2.0.3.dist-info}/entry_points.txt +0 -0
- {conson_xp-2.0.1.dist-info → conson_xp-2.0.3.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
conson_xp-2.0.
|
|
2
|
-
conson_xp-2.0.
|
|
3
|
-
conson_xp-2.0.
|
|
4
|
-
conson_xp-2.0.
|
|
5
|
-
xp/__init__.py,sha256=
|
|
1
|
+
conson_xp-2.0.3.dist-info/METADATA,sha256=MacSE_YD6_kKpZQfPXnzZAs9YbYR__cvxg1d4ISQLXg,11319
|
|
2
|
+
conson_xp-2.0.3.dist-info/WHEEL,sha256=tsUv_t7BDeJeRHaSrczbGeuK-TtDpGsWi_JfpzD255I,90
|
|
3
|
+
conson_xp-2.0.3.dist-info/entry_points.txt,sha256=1OcdIcDM1hz3ljCXgybaPUh1IOFEwkaTgLIW9u9zGeg,50
|
|
4
|
+
conson_xp-2.0.3.dist-info/licenses/LICENSE,sha256=rxj6woMM-r3YCyGq_UHFtbh7kHTAJgrccH6O-33IDE4,1419
|
|
5
|
+
xp/__init__.py,sha256=Cd7Ua5HD9WOxxHs-xepjLjWZf4c-ue1kISZWRKiqgwM,181
|
|
6
6
|
xp/cli/__init__.py,sha256=QjnKB1KaI2aIyKlzrnvCwfbBuUj8HNgwNMvNJVQofbI,81
|
|
7
7
|
xp/cli/__main__.py,sha256=l2iKwMdat5rTGd3JWs-uGksnYYDDffp_Npz05QdKEeU,117
|
|
8
8
|
xp/cli/commands/__init__.py,sha256=9TGP3uTxAU-s-kVChvQ-Fn3-HVYj-QpeQ05Is-20HRo,4788
|
|
@@ -81,7 +81,7 @@ xp/models/config/__init__.py,sha256=gEZnX9eE3DjFtLtF32riEjJQLypqQRbyPauBI4Cowbs,
|
|
|
81
81
|
xp/models/config/conson_module_config.py,sha256=t1G0LnNNMnjs3ahhz4-Z_5SlEv2FCrcRq13OmvZ2pvA,3009
|
|
82
82
|
xp/models/homekit/__init__.py,sha256=5HDSOClCu0ArK3IICn3_LDMMLBAzLjBxUUSF73bxSSk,34
|
|
83
83
|
xp/models/homekit/homekit_accessory.py,sha256=ANjDWlFxeNTstl7lKdmf6vMOC0wc005vpiD6awRcptA,1052
|
|
84
|
-
xp/models/homekit/homekit_config.py,sha256=
|
|
84
|
+
xp/models/homekit/homekit_config.py,sha256=YOhODQpURg_1OU0i-4qMglU4E37feNKKtY1uuZRXSoY,3759
|
|
85
85
|
xp/models/log_entry.py,sha256=tAiNwouCP2d4jKiHJY9a-2iAi8LWTpG-TZsOPDIstlA,4423
|
|
86
86
|
xp/models/protocol/__init__.py,sha256=TJ_CJKchA-xgQiv5vCo_ndBBZjrcaTmjT74bR0T-5Cw,38
|
|
87
87
|
xp/models/protocol/conbus_protocol.py,sha256=gFaXK1VY74aVQhMH69Dr-dTDbDuQDQGs8vKmXqK_te8,9318
|
|
@@ -112,7 +112,7 @@ xp/models/term/telegram_display.py,sha256=tXWeEtoIBSnScjha3ZHV9UPICmtBF2bkoLVIjQ
|
|
|
112
112
|
xp/models/write_config_type.py,sha256=IqgguaHgKvz4Qt-WaSVu3J2VaXgtS-br9Yp8q_xkIkY,895
|
|
113
113
|
xp/services/__init__.py,sha256=W9YZyrkh7vm--ZHhAXNQiOYQs5yhhmUHXP5I0Lf1XBg,782
|
|
114
114
|
xp/services/actiontable/__init__.py,sha256=z6js4EuJ6xKHaseTEhuEvKo1tr9K1XyQiruReJtBiPY,26
|
|
115
|
-
xp/services/actiontable/actiontable_serializer.py,sha256=
|
|
115
|
+
xp/services/actiontable/actiontable_serializer.py,sha256=GTpC9_bOdxqQl7cVhpEH9aGusZNRn5WcahwPc2XUWaY,8393
|
|
116
116
|
xp/services/actiontable/download_state_machine.py,sha256=lqNYN9LGGK2KiVUsmvyRfryWRB4-NOfsp7-9GrFubK4,9978
|
|
117
117
|
xp/services/actiontable/msactiontable_serializer.py,sha256=RRL6TZ1gpSQw81kAiw2BV3jTqm4fCJC0pWIcO26Cmos,174
|
|
118
118
|
xp/services/actiontable/msactiontable_xp20_serializer.py,sha256=5K6FxgbV2F4brumNaOH6M8qPyCxIfaqCGOPIYDmFdnk,6998
|
|
@@ -124,7 +124,7 @@ xp/services/conbus/actiontable/__init__.py,sha256=oD6vRk_Ye-eZ9s_hldAgtRJFu4mfAn
|
|
|
124
124
|
xp/services/conbus/actiontable/actiontable_download_service.py,sha256=41Hr1753IBpUeHQqO57uS7qxOB0rJt8qCpznzKlUPOM,15028
|
|
125
125
|
xp/services/conbus/actiontable/actiontable_list_service.py,sha256=oTDSpBkp-MJeaF5bhRnwkSy3na55xqQ4e2ykJzbMCUo,3236
|
|
126
126
|
xp/services/conbus/actiontable/actiontable_show_service.py,sha256=WISY2VsmSlceGa5_9lpFO-gs5TnTjv6YidQksUjCapk,3058
|
|
127
|
-
xp/services/conbus/actiontable/actiontable_upload_service.py,sha256=
|
|
127
|
+
xp/services/conbus/actiontable/actiontable_upload_service.py,sha256=bIAN3Ca3BHTPtZYfPijAgsWP91k-uNFAW7-i-LoTE4I,13553
|
|
128
128
|
xp/services/conbus/conbus_blink_all_service.py,sha256=toDIZDXBGBYnEishcdnJrVzkmfPi7g5nCDXuyA_wFCs,8536
|
|
129
129
|
xp/services/conbus/conbus_blink_service.py,sha256=ggLuzeq_UsgCoxRxg2bsNs9p8Lw_shjsj-niRzb5dKk,7953
|
|
130
130
|
xp/services/conbus/conbus_custom_service.py,sha256=9OIRC2CG_rN96vbv_EZXf7BrX_abhqi5MZx0Se8fEhU,7826
|
|
@@ -166,12 +166,12 @@ xp/services/telegram/telegram_output_service.py,sha256=9deqtcPndRqJ-3XQUWlJhXaVc
|
|
|
166
166
|
xp/services/telegram/telegram_service.py,sha256=jPu0Xrh3IpvqPLyuQT5Vf8HHw00vBingONHdxf_9TkI,13315
|
|
167
167
|
xp/services/telegram/telegram_version_service.py,sha256=oXnZ_K7OQ7xD-GEj3zDYp52KlkqVuHpO4bf7gMlC_w4,10574
|
|
168
168
|
xp/services/term/__init__.py,sha256=BIeOK042bMR-0l6MA80wdW5VuHlpWOXtRER9IG5ilQA,245
|
|
169
|
-
xp/services/term/homekit_accessory_driver.py,sha256=
|
|
170
|
-
xp/services/term/homekit_service.py,sha256=
|
|
169
|
+
xp/services/term/homekit_accessory_driver.py,sha256=asIZz-1aJQv7m0oP28vwMWPh8x6WsMHN9pxJ0jIdUPk,7833
|
|
170
|
+
xp/services/term/homekit_service.py,sha256=FNm01Sp5F4bjJqRLMpl_FGbPgeuUkWxcu7sCBOihR0c,30892
|
|
171
171
|
xp/services/term/protocol_monitor_service.py,sha256=5YBI0Nu7B7gMhaTbUhL6k9LSRfnCIj6CwrCYHiMHavA,10067
|
|
172
172
|
xp/services/term/state_monitor_service.py,sha256=EK9tNBfamAIV0z0EMsXDYWC-rXv6l6k_bHsC8xyEFSo,17116
|
|
173
173
|
xp/term/__init__.py,sha256=Xg2DhBeI3xQJLfc7_BPWI1por-rUXemyer5OtOt9Cus,51
|
|
174
|
-
xp/term/homekit.py,sha256=
|
|
174
|
+
xp/term/homekit.py,sha256=HJH3dZQsdp5rqcuV4EWJbytk7glCyDmj27614nRbIyI,9909
|
|
175
175
|
xp/term/homekit.tcss,sha256=A1f5-V3mvxAMZK_ERq8lLjNcOWH0U5tblIBbeL3OYYM,1382
|
|
176
176
|
xp/term/protocol.py,sha256=6MX3mduLei-AgLGaIe8lfOSu4Hi0y3KGePFFM2ssstc,3475
|
|
177
177
|
xp/term/protocol.tcss,sha256=r_KfxrbpycGHLVXqZc6INBBcUJME0hLrAZkF1oqnab4,2126
|
|
@@ -191,4 +191,4 @@ xp/utils/logging.py,sha256=wJ1d-yg97NiZUrt2F8iDMcmnHVwC-PErcI-7dpyiRDc,3777
|
|
|
191
191
|
xp/utils/serialization.py,sha256=TS1OwpTOemSvXsCGw3js4JkYYFEqkzrPe8V9QYQefdw,4684
|
|
192
192
|
xp/utils/state_machine.py,sha256=W9AY4ntRZnFeHAa5d43hm37j53uJPlqkRvWTPiBhJ_0,2464
|
|
193
193
|
xp/utils/time_utils.py,sha256=K17godWpL18VEypbTlvNOEDG6R3huYnf29yjkcnwRpU,3796
|
|
194
|
-
conson_xp-2.0.
|
|
194
|
+
conson_xp-2.0.3.dist-info/RECORD,,
|
xp/__init__.py
CHANGED
|
@@ -66,6 +66,10 @@ class HomekitAccessoryConfig(BaseModel):
|
|
|
66
66
|
on_action: on code for the accessory.
|
|
67
67
|
off_action: off code for the accessory.
|
|
68
68
|
toggle_action: Optional toggle action code for the accessory.
|
|
69
|
+
dimup_action: Optional dim up action code for the dimmable accessory.
|
|
70
|
+
dimdown_action: Optional dim down action code for the dimmable accessory.
|
|
71
|
+
levelup_action: Optional level up action code for the dimmable accessory.
|
|
72
|
+
leveldown_action: Optional level down action code for the dimmable accessory.
|
|
69
73
|
hap_accessory: Optional HAP accessory identifier.
|
|
70
74
|
"""
|
|
71
75
|
|
|
@@ -78,6 +82,10 @@ class HomekitAccessoryConfig(BaseModel):
|
|
|
78
82
|
on_action: str
|
|
79
83
|
off_action: str
|
|
80
84
|
toggle_action: Optional[str] = None
|
|
85
|
+
dimup_action: Optional[str] = None
|
|
86
|
+
dimdown_action: Optional[str] = None
|
|
87
|
+
levelup_action: Optional[str] = None
|
|
88
|
+
leveldown_action: Optional[str] = None
|
|
81
89
|
hap_accessory: Optional[int] = None
|
|
82
90
|
|
|
83
91
|
|
|
@@ -65,8 +65,8 @@ class ActionTableSerializer(ActionTableSerializerProtocol):
|
|
|
65
65
|
link_number = de_bcd(data[i + 1])
|
|
66
66
|
module_input = de_bcd(data[i + 2])
|
|
67
67
|
|
|
68
|
-
# Extract output
|
|
69
|
-
module_output = lower3(data[i + 3])
|
|
68
|
+
# Extract output (0-indexed in wire format, convert to 1-indexed) and command
|
|
69
|
+
module_output = lower3(data[i + 3]) + 1
|
|
70
70
|
command_raw = upper5(data[i + 3])
|
|
71
71
|
|
|
72
72
|
parameter_raw = byte_to_unsigned(data[i + 4])
|
|
@@ -125,8 +125,8 @@ class ActionTableSerializer(ActionTableSerializerProtocol):
|
|
|
125
125
|
link_byte = to_bcd(entry.link_number)
|
|
126
126
|
input_byte = to_bcd(entry.module_input)
|
|
127
127
|
|
|
128
|
-
# Combine output (lower 3 bits) and command (upper 5 bits)
|
|
129
|
-
output_command_byte = (entry.module_output & 0x07) | (
|
|
128
|
+
# Combine output (lower 3 bits, 0-indexed) and command (upper 5 bits)
|
|
129
|
+
output_command_byte = ((entry.module_output - 1) & 0x07) | (
|
|
130
130
|
(entry.command.value & 0x1F) << 3
|
|
131
131
|
)
|
|
132
132
|
|
|
@@ -81,6 +81,7 @@ class ActionTableUploadService:
|
|
|
81
81
|
# Upload state
|
|
82
82
|
self.upload_data_chunks: list[str] = []
|
|
83
83
|
self.current_chunk_index: int = 0
|
|
84
|
+
self._eof_sent: bool = False
|
|
84
85
|
|
|
85
86
|
# Set up logging
|
|
86
87
|
self.logger = logging.getLogger(__name__)
|
|
@@ -173,7 +174,7 @@ class ActionTableUploadService:
|
|
|
173
174
|
)
|
|
174
175
|
self.current_chunk_index += 1
|
|
175
176
|
self.on_progress.emit(".")
|
|
176
|
-
|
|
177
|
+
elif not self._eof_sent:
|
|
177
178
|
# All chunks sent, send EOF
|
|
178
179
|
self.logger.debug("All chunks sent, sending EOF")
|
|
179
180
|
self.conbus_protocol.send_telegram(
|
|
@@ -182,7 +183,13 @@ class ActionTableUploadService:
|
|
|
182
183
|
system_function=SystemFunction.EOF,
|
|
183
184
|
data_value="00",
|
|
184
185
|
)
|
|
186
|
+
self.on_progress.emit("END")
|
|
187
|
+
self.logger.debug("EOF sent, waiting for last ACK")
|
|
188
|
+
self._eof_sent = True
|
|
189
|
+
else:
|
|
190
|
+
self.logger.debug("Last ACK received, closing connection")
|
|
185
191
|
self.on_finish.emit(True)
|
|
192
|
+
|
|
186
193
|
elif reply_telegram.system_function == SystemFunction.NAK:
|
|
187
194
|
self.logger.debug("Received NAK during upload")
|
|
188
195
|
self.failed("Upload failed: NAK received")
|
|
@@ -10,9 +10,18 @@ from pyhap.const import CATEGORY_LIGHTBULB, CATEGORY_OUTLET
|
|
|
10
10
|
|
|
11
11
|
from xp.models.homekit.homekit_config import HomekitConfig
|
|
12
12
|
|
|
13
|
+
# Callback type: (accessory_name, is_on, brightness_or_none)
|
|
14
|
+
OnSetCallback = Callable[[str, bool, Optional[int]], None]
|
|
15
|
+
|
|
13
16
|
|
|
14
17
|
class XPAccessory(Accessory):
|
|
15
|
-
"""
|
|
18
|
+
"""
|
|
19
|
+
Single accessory wrapping a Conbus output.
|
|
20
|
+
|
|
21
|
+
Attributes:
|
|
22
|
+
logger: Logger instance for this accessory.
|
|
23
|
+
current_brightness: Current brightness value 0-100.
|
|
24
|
+
"""
|
|
16
25
|
|
|
17
26
|
def __init__(
|
|
18
27
|
self,
|
|
@@ -35,12 +44,19 @@ class XPAccessory(Accessory):
|
|
|
35
44
|
super().__init__(driver._driver, display_name, aid=aid)
|
|
36
45
|
self._hk_driver = driver
|
|
37
46
|
self._accessory_id = name
|
|
47
|
+
self._is_dimmable = service_type == "dimminglight"
|
|
48
|
+
self._char_brightness: Optional[object] = None
|
|
49
|
+
self._current_brightness: int = 100
|
|
38
50
|
self.logger = logging.getLogger(__name__)
|
|
39
51
|
|
|
40
|
-
if
|
|
52
|
+
if self._is_dimmable:
|
|
41
53
|
self.category = CATEGORY_LIGHTBULB
|
|
42
54
|
serv = self.add_preload_service("Lightbulb", chars=["On", "Brightness"])
|
|
43
|
-
|
|
55
|
+
self._char_brightness = serv.configure_char(
|
|
56
|
+
"Brightness",
|
|
57
|
+
setter_callback=self._set_brightness,
|
|
58
|
+
value=self._current_brightness,
|
|
59
|
+
)
|
|
44
60
|
elif service_type == "outlet":
|
|
45
61
|
self.category = CATEGORY_OUTLET
|
|
46
62
|
serv = self.add_preload_service("Outlet")
|
|
@@ -58,16 +74,36 @@ class XPAccessory(Accessory):
|
|
|
58
74
|
value: True for on, False for off.
|
|
59
75
|
"""
|
|
60
76
|
if self._hk_driver._on_set:
|
|
61
|
-
self._hk_driver._on_set(self._accessory_id, value)
|
|
77
|
+
self._hk_driver._on_set(self._accessory_id, value, None)
|
|
78
|
+
|
|
79
|
+
def _set_brightness(self, value: int) -> None:
|
|
80
|
+
"""
|
|
81
|
+
Handle HomeKit set brightness request.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
value: Brightness value 0-100.
|
|
85
|
+
"""
|
|
86
|
+
if self._hk_driver._on_set:
|
|
87
|
+
self._hk_driver._on_set(self._accessory_id, True, value)
|
|
88
|
+
self._current_brightness = value
|
|
62
89
|
|
|
63
|
-
def update_state(self, is_on: bool) -> None:
|
|
90
|
+
def update_state(self, is_on: bool, brightness: Optional[int] = None) -> None:
|
|
64
91
|
"""
|
|
65
92
|
Update accessory state from Conbus event.
|
|
66
93
|
|
|
67
94
|
Args:
|
|
68
95
|
is_on: True if accessory is on, False otherwise.
|
|
96
|
+
brightness: Optional brightness value 0-100.
|
|
69
97
|
"""
|
|
70
98
|
self._char_on.set_value(is_on)
|
|
99
|
+
if brightness is not None and self._char_brightness:
|
|
100
|
+
self._char_brightness.set_value(brightness) # type: ignore[attr-defined]
|
|
101
|
+
self._current_brightness = brightness
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def current_brightness(self) -> int:
|
|
105
|
+
"""Get current brightness value."""
|
|
106
|
+
return self._current_brightness
|
|
71
107
|
|
|
72
108
|
|
|
73
109
|
class HomekitAccessoryDriver:
|
|
@@ -84,14 +120,15 @@ class HomekitAccessoryDriver:
|
|
|
84
120
|
self._homekit_config = homekit_config
|
|
85
121
|
self._driver: Optional[AccessoryDriver] = None
|
|
86
122
|
self._accessories: Dict[str, XPAccessory] = {}
|
|
87
|
-
self._on_set: Optional[
|
|
123
|
+
self._on_set: Optional[OnSetCallback] = None
|
|
88
124
|
|
|
89
|
-
def set_callback(self, on_set:
|
|
125
|
+
def set_callback(self, on_set: OnSetCallback) -> None:
|
|
90
126
|
"""
|
|
91
127
|
Set callback for HomeKit set events.
|
|
92
128
|
|
|
93
129
|
Args:
|
|
94
|
-
on_set: Callback(accessory_name, is_on) called when HomeKit app
|
|
130
|
+
on_set: Callback(accessory_name, is_on, brightness) called when HomeKit app changes state.
|
|
131
|
+
brightness is None for on/off only, or 0-100 for dimming.
|
|
95
132
|
"""
|
|
96
133
|
self._on_set = on_set
|
|
97
134
|
|
|
@@ -157,15 +194,34 @@ class HomekitAccessoryDriver:
|
|
|
157
194
|
except Exception as e:
|
|
158
195
|
self.logger.error(f"Error stopping AccessoryDriver: {e}", exc_info=True)
|
|
159
196
|
|
|
160
|
-
def update_state(
|
|
197
|
+
def update_state(
|
|
198
|
+
self, accessory_name: str, is_on: bool, brightness: Optional[int] = None
|
|
199
|
+
) -> None:
|
|
161
200
|
"""
|
|
162
201
|
Update accessory state from Conbus event.
|
|
163
202
|
|
|
164
203
|
Args:
|
|
165
204
|
accessory_name: Accessory name to update.
|
|
166
205
|
is_on: True if accessory is on, False otherwise.
|
|
206
|
+
brightness: Optional brightness value 0-100.
|
|
167
207
|
"""
|
|
168
|
-
|
|
169
|
-
|
|
208
|
+
acc = self._accessories.get(accessory_name)
|
|
209
|
+
if acc:
|
|
210
|
+
acc.update_state(is_on, brightness)
|
|
170
211
|
else:
|
|
171
212
|
self.logger.warning(f"Unknown accessory name: {accessory_name}")
|
|
213
|
+
|
|
214
|
+
def get_brightness(self, accessory_name: str) -> int:
|
|
215
|
+
"""
|
|
216
|
+
Get current brightness for an accessory.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
accessory_name: Accessory name.
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
Current brightness 0-100, defaults to 100 if not found.
|
|
223
|
+
"""
|
|
224
|
+
acc = self._accessories.get(accessory_name)
|
|
225
|
+
if acc:
|
|
226
|
+
return acc.current_brightness
|
|
227
|
+
return 100
|
|
@@ -81,6 +81,9 @@ class HomekitService:
|
|
|
81
81
|
# Set up HomeKit callback
|
|
82
82
|
self._accessory_driver.set_callback(self._on_homekit_set)
|
|
83
83
|
|
|
84
|
+
# Track active level action: (accessory_id, action_type) or None
|
|
85
|
+
self._active_level_action: Optional[tuple[str, str]] = None
|
|
86
|
+
|
|
84
87
|
# Connect to protocol signals
|
|
85
88
|
self._connect_signals()
|
|
86
89
|
|
|
@@ -297,23 +300,82 @@ class HomekitService:
|
|
|
297
300
|
await self._accessory_driver.stop()
|
|
298
301
|
self.cleanup()
|
|
299
302
|
|
|
300
|
-
def _on_homekit_set(
|
|
303
|
+
def _on_homekit_set(
|
|
304
|
+
self, accessory_name: str, is_on: bool, brightness: Optional[int]
|
|
305
|
+
) -> None:
|
|
301
306
|
"""
|
|
302
|
-
Handle HomeKit app
|
|
307
|
+
Handle HomeKit app set request (on/off or brightness).
|
|
303
308
|
|
|
304
309
|
Args:
|
|
305
310
|
accessory_name: Accessory name from HomeKit.
|
|
306
311
|
is_on: True for on, False for off.
|
|
312
|
+
brightness: Brightness value 0-100, or None for on/off only.
|
|
307
313
|
"""
|
|
308
314
|
config = self._find_accessory_config(accessory_name)
|
|
309
|
-
if config:
|
|
315
|
+
if not config:
|
|
316
|
+
self.logger.warning(f"No config found for accessory: {accessory_name}")
|
|
317
|
+
return
|
|
318
|
+
|
|
319
|
+
if brightness is not None:
|
|
320
|
+
# Handle brightness change
|
|
321
|
+
self._handle_brightness_change(accessory_name, config, brightness)
|
|
322
|
+
else:
|
|
323
|
+
# Handle on/off toggle
|
|
310
324
|
action = config.on_action if is_on else config.off_action
|
|
311
325
|
self.send_action(action)
|
|
312
326
|
self.on_status_message.emit(
|
|
313
327
|
f"HomeKit: {accessory_name} {'ON' if is_on else 'OFF'}"
|
|
314
328
|
)
|
|
329
|
+
|
|
330
|
+
def _handle_brightness_change(
|
|
331
|
+
self,
|
|
332
|
+
accessory_name: str,
|
|
333
|
+
config: "HomekitAccessoryConfig",
|
|
334
|
+
target_brightness: int,
|
|
335
|
+
) -> None:
|
|
336
|
+
"""
|
|
337
|
+
Handle brightness change by sending dimup/dimdown actions.
|
|
338
|
+
|
|
339
|
+
Calculates delta from current brightness and sends appropriate
|
|
340
|
+
number of LEVELINC or LEVELDEC commands (step = 10%).
|
|
341
|
+
|
|
342
|
+
Args:
|
|
343
|
+
accessory_name: Accessory name.
|
|
344
|
+
config: Accessory configuration.
|
|
345
|
+
target_brightness: Target brightness 0-100.
|
|
346
|
+
"""
|
|
347
|
+
current = self._accessory_driver.get_brightness(accessory_name)
|
|
348
|
+
delta = target_brightness - current
|
|
349
|
+
|
|
350
|
+
if delta == 0:
|
|
351
|
+
return
|
|
352
|
+
|
|
353
|
+
# Determine action and steps (10% per step)
|
|
354
|
+
step_size = 10
|
|
355
|
+
steps = abs(delta) // step_size
|
|
356
|
+
|
|
357
|
+
if delta > 0:
|
|
358
|
+
# Increase brightness
|
|
359
|
+
if not config.dimup_action:
|
|
360
|
+
self.logger.warning(f"No dimup_action for {accessory_name}")
|
|
361
|
+
return
|
|
362
|
+
action = config.dimup_action
|
|
363
|
+
direction = "+"
|
|
315
364
|
else:
|
|
316
|
-
|
|
365
|
+
# Decrease brightness
|
|
366
|
+
if not config.dimdown_action:
|
|
367
|
+
self.logger.warning(f"No dimdown_action for {accessory_name}")
|
|
368
|
+
return
|
|
369
|
+
action = config.dimdown_action
|
|
370
|
+
direction = "-"
|
|
371
|
+
|
|
372
|
+
# Send action for each step
|
|
373
|
+
for _ in range(steps):
|
|
374
|
+
self.send_action(action)
|
|
375
|
+
|
|
376
|
+
self.on_status_message.emit(
|
|
377
|
+
f"HomeKit: {accessory_name} {current}% → {target_brightness}% ({direction}{steps * step_size}%)"
|
|
378
|
+
)
|
|
317
379
|
|
|
318
380
|
def send_action(self, action: str) -> None:
|
|
319
381
|
"""
|
|
@@ -421,12 +483,15 @@ class HomekitService:
|
|
|
421
483
|
Returns:
|
|
422
484
|
True if command was sent, False otherwise.
|
|
423
485
|
"""
|
|
486
|
+
config = self._find_accessory_config_by_id(accessory_id)
|
|
424
487
|
state = self._accessory_states.get(accessory_id)
|
|
425
|
-
if not state:
|
|
488
|
+
if not config or not state or not config.dimup_action:
|
|
489
|
+
self.logger.warning(f"No config for accessory {accessory_id}")
|
|
426
490
|
return False
|
|
427
|
-
|
|
428
|
-
self.
|
|
429
|
-
|
|
491
|
+
|
|
492
|
+
self.send_action(config.dimup_action)
|
|
493
|
+
self.on_status_message.emit(f"Dim+ {state.accessory_name}")
|
|
494
|
+
return True
|
|
430
495
|
|
|
431
496
|
def decrease_dimmer(self, accessory_id: str) -> bool:
|
|
432
497
|
"""
|
|
@@ -438,12 +503,103 @@ class HomekitService:
|
|
|
438
503
|
Returns:
|
|
439
504
|
True if command was sent, False otherwise.
|
|
440
505
|
"""
|
|
506
|
+
config = self._find_accessory_config_by_id(accessory_id)
|
|
441
507
|
state = self._accessory_states.get(accessory_id)
|
|
442
|
-
if not state:
|
|
508
|
+
if not config or not state or not config.dimdown_action:
|
|
509
|
+
self.logger.warning(f"No config for accessory {accessory_id}")
|
|
443
510
|
return False
|
|
444
|
-
|
|
445
|
-
self.
|
|
446
|
-
|
|
511
|
+
|
|
512
|
+
self.send_action(config.dimdown_action)
|
|
513
|
+
self.on_status_message.emit(f"Dim- {state.accessory_name}")
|
|
514
|
+
return True
|
|
515
|
+
|
|
516
|
+
def levelup_selected(self, accessory_id: str) -> bool:
|
|
517
|
+
"""
|
|
518
|
+
Increase level for accessory (toggle Make/Break).
|
|
519
|
+
|
|
520
|
+
First press sends Make (M), second press sends Break (B).
|
|
521
|
+
|
|
522
|
+
Args:
|
|
523
|
+
accessory_id: Accessory ID (e.g., "A12_1").
|
|
524
|
+
|
|
525
|
+
Returns:
|
|
526
|
+
True if command was sent, False otherwise.
|
|
527
|
+
"""
|
|
528
|
+
config = self._find_accessory_config_by_id(accessory_id)
|
|
529
|
+
state = self._accessory_states.get(accessory_id)
|
|
530
|
+
if not config or not state or not config.levelup_action:
|
|
531
|
+
self.logger.warning(f"No config for accessory {accessory_id}")
|
|
532
|
+
return False
|
|
533
|
+
|
|
534
|
+
return self._send_level_action(
|
|
535
|
+
accessory_id, "levelup", config.levelup_action, state.accessory_name
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
def leveldown_selected(self, accessory_id: str) -> bool:
|
|
539
|
+
"""
|
|
540
|
+
Decrease level for accessory (toggle Make/Break).
|
|
541
|
+
|
|
542
|
+
First press sends Make (M), second press sends Break (B).
|
|
543
|
+
|
|
544
|
+
Args:
|
|
545
|
+
accessory_id: Accessory ID (e.g., "A12_1").
|
|
546
|
+
|
|
547
|
+
Returns:
|
|
548
|
+
True if command was sent, False otherwise.
|
|
549
|
+
"""
|
|
550
|
+
config = self._find_accessory_config_by_id(accessory_id)
|
|
551
|
+
state = self._accessory_states.get(accessory_id)
|
|
552
|
+
if not config or not state or not config.leveldown_action:
|
|
553
|
+
self.logger.warning(f"No config for accessory {accessory_id}")
|
|
554
|
+
return False
|
|
555
|
+
|
|
556
|
+
return self._send_level_action(
|
|
557
|
+
accessory_id, "leveldown", config.leveldown_action, state.accessory_name
|
|
558
|
+
)
|
|
559
|
+
|
|
560
|
+
def _send_level_action(
|
|
561
|
+
self, accessory_id: str, action_type: str, action: str, name: str
|
|
562
|
+
) -> bool:
|
|
563
|
+
"""
|
|
564
|
+
Send level action with Make/Break toggle.
|
|
565
|
+
|
|
566
|
+
Args:
|
|
567
|
+
accessory_id: Accessory ID.
|
|
568
|
+
action_type: "levelup" or "leveldown".
|
|
569
|
+
action: Action code (e.g., "E02L13I15").
|
|
570
|
+
name: Accessory name for status message.
|
|
571
|
+
|
|
572
|
+
Returns:
|
|
573
|
+
True if command was sent.
|
|
574
|
+
"""
|
|
575
|
+
current = self._active_level_action
|
|
576
|
+
|
|
577
|
+
# If same action is active, send Break and clear
|
|
578
|
+
if current and current[0] == accessory_id and current[1] == action_type:
|
|
579
|
+
self._conbus_protocol.send_raw_telegram(f"{action}B")
|
|
580
|
+
self._active_level_action = None
|
|
581
|
+
direction = "+" if action_type == "levelup" else "-"
|
|
582
|
+
self.on_status_message.emit(f"Level{direction} {name} [B]")
|
|
583
|
+
return True
|
|
584
|
+
|
|
585
|
+
# If different action is active, send Break for it first
|
|
586
|
+
if current:
|
|
587
|
+
old_config = self._find_accessory_config_by_id(current[0])
|
|
588
|
+
if old_config:
|
|
589
|
+
old_action = (
|
|
590
|
+
old_config.levelup_action
|
|
591
|
+
if current[1] == "levelup"
|
|
592
|
+
else old_config.leveldown_action
|
|
593
|
+
)
|
|
594
|
+
if old_action:
|
|
595
|
+
self._conbus_protocol.send_raw_telegram(f"{old_action}B")
|
|
596
|
+
|
|
597
|
+
# Send Make for new action
|
|
598
|
+
self._conbus_protocol.send_raw_telegram(f"{action}M")
|
|
599
|
+
self._active_level_action = (accessory_id, action_type)
|
|
600
|
+
direction = "+" if action_type == "levelup" else "-"
|
|
601
|
+
self.on_status_message.emit(f"Level{direction} {name} [M]")
|
|
602
|
+
return True
|
|
447
603
|
|
|
448
604
|
def refresh_all(self) -> None:
|
|
449
605
|
"""
|
xp/term/homekit.py
CHANGED
|
@@ -41,6 +41,8 @@ class HomekitApp(App[None]):
|
|
|
41
41
|
("minus", "turn_off_selected", "Off"),
|
|
42
42
|
("plus", "dim_up", "Dim+"),
|
|
43
43
|
("quotation_mark", "dim_down", "Dim-"),
|
|
44
|
+
("asterisk", "level_up", "Level+"),
|
|
45
|
+
("ç", "level_down", "Level-"),
|
|
44
46
|
]
|
|
45
47
|
|
|
46
48
|
def __init__(self, homekit_service: HomekitService) -> None:
|
|
@@ -106,12 +108,17 @@ class HomekitApp(App[None]):
|
|
|
106
108
|
- - : Turn OFF
|
|
107
109
|
- + : Dim up
|
|
108
110
|
- " : Dim down
|
|
111
|
+
- * : Level up
|
|
112
|
+
- ç : Level down
|
|
109
113
|
|
|
110
114
|
Args:
|
|
111
115
|
event: Key press event.
|
|
112
116
|
"""
|
|
113
117
|
key = event.key
|
|
114
118
|
|
|
119
|
+
# Debug: show received key
|
|
120
|
+
self.homekit_service.on_status_message.emit(f"Key: {key}")
|
|
121
|
+
|
|
115
122
|
# Selection keys (a-z0-9)
|
|
116
123
|
if len(key) == 1 and (("a" <= key <= "z") or ("0" <= key <= "9")):
|
|
117
124
|
accessory_id = self.homekit_service.select_accessory(key)
|
|
@@ -140,6 +147,12 @@ class HomekitApp(App[None]):
|
|
|
140
147
|
elif key in ("quotation_mark", '"'):
|
|
141
148
|
self.homekit_service.decrease_dimmer(self.selected_accessory_id)
|
|
142
149
|
event.prevent_default()
|
|
150
|
+
elif key in ("asterisk", "star", "*"):
|
|
151
|
+
self.homekit_service.levelup_selected(self.selected_accessory_id)
|
|
152
|
+
event.prevent_default()
|
|
153
|
+
elif key in ("cedille", "ç"):
|
|
154
|
+
self.homekit_service.leveldown_selected(self.selected_accessory_id)
|
|
155
|
+
event.prevent_default()
|
|
143
156
|
|
|
144
157
|
def _select_row(self, action_key: str) -> None:
|
|
145
158
|
"""
|
|
@@ -243,6 +256,16 @@ class HomekitApp(App[None]):
|
|
|
243
256
|
if self.selected_accessory_id:
|
|
244
257
|
self.homekit_service.decrease_dimmer(self.selected_accessory_id)
|
|
245
258
|
|
|
259
|
+
def action_level_up(self) -> None:
|
|
260
|
+
"""Increase level on selected accessory."""
|
|
261
|
+
if self.selected_accessory_id:
|
|
262
|
+
self.homekit_service.levelup_selected(self.selected_accessory_id)
|
|
263
|
+
|
|
264
|
+
def action_level_down(self) -> None:
|
|
265
|
+
"""Decrease level on selected accessory."""
|
|
266
|
+
if self.selected_accessory_id:
|
|
267
|
+
self.homekit_service.leveldown_selected(self.selected_accessory_id)
|
|
268
|
+
|
|
246
269
|
async def on_unmount(self) -> None:
|
|
247
270
|
"""Stop AccessoryDriver and clean up service when app unmounts."""
|
|
248
271
|
await self.homekit_service.stop()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|