conson-xp 1.52.0__py3-none-any.whl → 2.0.1__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-1.52.0.dist-info → conson_xp-2.0.1.dist-info}/METADATA +1 -11
- {conson_xp-1.52.0.dist-info → conson_xp-2.0.1.dist-info}/RECORD +20 -39
- xp/__init__.py +1 -1
- xp/cli/commands/__init__.py +0 -4
- xp/cli/commands/term/term_commands.py +1 -1
- xp/cli/main.py +23 -7
- xp/models/conbus/conbus_client_config.py +2 -0
- xp/models/protocol/conbus_protocol.py +30 -25
- xp/models/term/accessory_state.py +1 -1
- xp/services/protocol/__init__.py +2 -3
- xp/services/protocol/conbus_event_protocol.py +6 -6
- xp/services/term/homekit_accessory_driver.py +5 -2
- xp/services/term/homekit_service.py +118 -11
- xp/term/homekit.py +140 -8
- xp/term/homekit.tcss +4 -4
- xp/term/widgets/room_list.py +61 -3
- xp/utils/dependencies.py +24 -154
- xp/cli/commands/homekit/__init__.py +0 -3
- xp/cli/commands/homekit/homekit.py +0 -120
- xp/cli/commands/homekit/homekit_start_commands.py +0 -44
- xp/services/homekit/__init__.py +0 -1
- xp/services/homekit/homekit_cache_service.py +0 -313
- xp/services/homekit/homekit_conbus_service.py +0 -99
- xp/services/homekit/homekit_config_validator.py +0 -327
- xp/services/homekit/homekit_conson_validator.py +0 -130
- xp/services/homekit/homekit_dimminglight.py +0 -189
- xp/services/homekit/homekit_dimminglight_service.py +0 -155
- xp/services/homekit/homekit_hap_service.py +0 -351
- xp/services/homekit/homekit_lightbulb.py +0 -125
- xp/services/homekit/homekit_lightbulb_service.py +0 -91
- xp/services/homekit/homekit_module_service.py +0 -60
- xp/services/homekit/homekit_outlet.py +0 -175
- xp/services/homekit/homekit_outlet_service.py +0 -127
- xp/services/homekit/homekit_service.py +0 -371
- xp/services/protocol/protocol_factory.py +0 -84
- xp/services/protocol/telegram_protocol.py +0 -270
- {conson_xp-1.52.0.dist-info → conson_xp-2.0.1.dist-info}/WHEEL +0 -0
- {conson_xp-1.52.0.dist-info → conson_xp-2.0.1.dist-info}/entry_points.txt +0 -0
- {conson_xp-1.52.0.dist-info → conson_xp-2.0.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -89,7 +89,7 @@ class HomekitService:
|
|
|
89
89
|
|
|
90
90
|
def _initialize_accessory_states(self) -> None:
|
|
91
91
|
"""Initialize accessory states from HomekitConfig and ConsonModuleListConfig."""
|
|
92
|
-
action_keys = "
|
|
92
|
+
action_keys = "abcdefghijklmnopqrstuvwxyz0123456789"
|
|
93
93
|
action_index = 0
|
|
94
94
|
sort_order = 0
|
|
95
95
|
|
|
@@ -180,6 +180,23 @@ class HomekitService:
|
|
|
180
180
|
return accessory
|
|
181
181
|
return None
|
|
182
182
|
|
|
183
|
+
def _find_accessory_config_by_id(
|
|
184
|
+
self, accessory_id: str
|
|
185
|
+
) -> Optional[HomekitAccessoryConfig]:
|
|
186
|
+
"""
|
|
187
|
+
Find accessory config by accessory ID.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
accessory_id: Accessory ID (e.g., "A12_1").
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
HomekitAccessoryConfig if found, None otherwise.
|
|
194
|
+
"""
|
|
195
|
+
state = self._accessory_states.get(accessory_id)
|
|
196
|
+
if not state:
|
|
197
|
+
return None
|
|
198
|
+
return self._find_accessory_config_by_output(state.serial_number, state.output)
|
|
199
|
+
|
|
183
200
|
def _connect_signals(self) -> None:
|
|
184
201
|
"""Connect to protocol signals."""
|
|
185
202
|
self._conbus_protocol.on_connection_made.connect(self._on_connection_made)
|
|
@@ -291,13 +308,23 @@ class HomekitService:
|
|
|
291
308
|
config = self._find_accessory_config(accessory_name)
|
|
292
309
|
if config:
|
|
293
310
|
action = config.on_action if is_on else config.off_action
|
|
294
|
-
self.
|
|
311
|
+
self.send_action(action)
|
|
295
312
|
self.on_status_message.emit(
|
|
296
313
|
f"HomeKit: {accessory_name} {'ON' if is_on else 'OFF'}"
|
|
297
314
|
)
|
|
298
315
|
else:
|
|
299
316
|
self.logger.warning(f"No config found for accessory: {accessory_name}")
|
|
300
317
|
|
|
318
|
+
def send_action(self, action: str) -> None:
|
|
319
|
+
"""
|
|
320
|
+
Send an action telegram to the conbus protocol.
|
|
321
|
+
|
|
322
|
+
Args:
|
|
323
|
+
action: The action string to send (e.g., "E00L00I00").
|
|
324
|
+
"""
|
|
325
|
+
self._conbus_protocol.send_raw_telegram(f"{action}M")
|
|
326
|
+
self._conbus_protocol.send_raw_telegram(f"{action}B")
|
|
327
|
+
|
|
301
328
|
def toggle_connection(self) -> None:
|
|
302
329
|
"""
|
|
303
330
|
Toggle connection state between connected and disconnected.
|
|
@@ -313,31 +340,111 @@ class HomekitService:
|
|
|
313
340
|
else:
|
|
314
341
|
self.connect()
|
|
315
342
|
|
|
316
|
-
def
|
|
343
|
+
def select_accessory(self, action_key: str) -> Optional[str]:
|
|
344
|
+
"""
|
|
345
|
+
Get accessory ID for action key.
|
|
346
|
+
|
|
347
|
+
Args:
|
|
348
|
+
action_key: Action key (a-z0-9).
|
|
349
|
+
|
|
350
|
+
Returns:
|
|
351
|
+
Accessory ID if found, None otherwise.
|
|
317
352
|
"""
|
|
318
|
-
|
|
353
|
+
return self._action_map.get(action_key)
|
|
319
354
|
|
|
320
|
-
|
|
355
|
+
def toggle_selected(self, accessory_id: str) -> bool:
|
|
356
|
+
"""
|
|
357
|
+
Toggle accessory by ID.
|
|
321
358
|
|
|
322
359
|
Args:
|
|
323
|
-
|
|
360
|
+
accessory_id: Accessory ID (e.g., "A12_1").
|
|
324
361
|
|
|
325
362
|
Returns:
|
|
326
363
|
True if toggle was sent, False otherwise.
|
|
327
364
|
"""
|
|
328
|
-
accessory_id = self._action_map.get(action_key)
|
|
329
|
-
if not accessory_id:
|
|
330
|
-
return False
|
|
331
|
-
|
|
332
365
|
state = self._accessory_states.get(accessory_id)
|
|
333
366
|
if not state or not state.toggle_action:
|
|
334
367
|
self.logger.warning(f"No toggle_action for accessory {accessory_id}")
|
|
335
368
|
return False
|
|
336
369
|
|
|
337
|
-
self.
|
|
370
|
+
self.send_action(state.toggle_action)
|
|
338
371
|
self.on_status_message.emit(f"Toggling {state.accessory_name}")
|
|
339
372
|
return True
|
|
340
373
|
|
|
374
|
+
def turn_on_selected(self, accessory_id: str) -> bool:
|
|
375
|
+
"""
|
|
376
|
+
Turn on accessory by ID.
|
|
377
|
+
|
|
378
|
+
Args:
|
|
379
|
+
accessory_id: Accessory ID (e.g., "A12_1").
|
|
380
|
+
|
|
381
|
+
Returns:
|
|
382
|
+
True if on command was sent, False otherwise.
|
|
383
|
+
"""
|
|
384
|
+
config = self._find_accessory_config_by_id(accessory_id)
|
|
385
|
+
state = self._accessory_states.get(accessory_id)
|
|
386
|
+
if not config or not state:
|
|
387
|
+
self.logger.warning(f"No config for accessory {accessory_id}")
|
|
388
|
+
return False
|
|
389
|
+
|
|
390
|
+
self.send_action(config.on_action)
|
|
391
|
+
self.on_status_message.emit(f"Turning ON {state.accessory_name}")
|
|
392
|
+
return True
|
|
393
|
+
|
|
394
|
+
def turn_off_selected(self, accessory_id: str) -> bool:
|
|
395
|
+
"""
|
|
396
|
+
Turn off accessory by ID.
|
|
397
|
+
|
|
398
|
+
Args:
|
|
399
|
+
accessory_id: Accessory ID (e.g., "A12_1").
|
|
400
|
+
|
|
401
|
+
Returns:
|
|
402
|
+
True if off command was sent, False otherwise.
|
|
403
|
+
"""
|
|
404
|
+
config = self._find_accessory_config_by_id(accessory_id)
|
|
405
|
+
state = self._accessory_states.get(accessory_id)
|
|
406
|
+
if not config or not state:
|
|
407
|
+
self.logger.warning(f"No config for accessory {accessory_id}")
|
|
408
|
+
return False
|
|
409
|
+
|
|
410
|
+
self.send_action(config.off_action)
|
|
411
|
+
self.on_status_message.emit(f"Turning OFF {state.accessory_name}")
|
|
412
|
+
return True
|
|
413
|
+
|
|
414
|
+
def increase_dimmer(self, accessory_id: str) -> bool:
|
|
415
|
+
"""
|
|
416
|
+
Increase dimmer level for accessory.
|
|
417
|
+
|
|
418
|
+
Args:
|
|
419
|
+
accessory_id: Accessory ID (e.g., "A12_1").
|
|
420
|
+
|
|
421
|
+
Returns:
|
|
422
|
+
True if command was sent, False otherwise.
|
|
423
|
+
"""
|
|
424
|
+
state = self._accessory_states.get(accessory_id)
|
|
425
|
+
if not state:
|
|
426
|
+
return False
|
|
427
|
+
# TODO: Implement dimmer control
|
|
428
|
+
self.on_status_message.emit(f"Dimmer+ {state.accessory_name} (not implemented)")
|
|
429
|
+
return False
|
|
430
|
+
|
|
431
|
+
def decrease_dimmer(self, accessory_id: str) -> bool:
|
|
432
|
+
"""
|
|
433
|
+
Decrease dimmer level for accessory.
|
|
434
|
+
|
|
435
|
+
Args:
|
|
436
|
+
accessory_id: Accessory ID (e.g., "A12_1").
|
|
437
|
+
|
|
438
|
+
Returns:
|
|
439
|
+
True if command was sent, False otherwise.
|
|
440
|
+
"""
|
|
441
|
+
state = self._accessory_states.get(accessory_id)
|
|
442
|
+
if not state:
|
|
443
|
+
return False
|
|
444
|
+
# TODO: Implement dimmer control
|
|
445
|
+
self.on_status_message.emit(f"Dimmer- {state.accessory_name} (not implemented)")
|
|
446
|
+
return False
|
|
447
|
+
|
|
341
448
|
def refresh_all(self) -> None:
|
|
342
449
|
"""
|
|
343
450
|
Refresh all module states.
|
xp/term/homekit.py
CHANGED
|
@@ -4,6 +4,7 @@ from pathlib import Path
|
|
|
4
4
|
from typing import Any, Optional
|
|
5
5
|
|
|
6
6
|
from textual.app import App, ComposeResult
|
|
7
|
+
from textual.widgets import DataTable
|
|
7
8
|
|
|
8
9
|
from xp.services.term.homekit_service import HomekitService
|
|
9
10
|
from xp.term.widgets.room_list import RoomListWidget
|
|
@@ -14,11 +15,13 @@ class HomekitApp(App[None]):
|
|
|
14
15
|
"""
|
|
15
16
|
Textual app for HomeKit accessory monitoring.
|
|
16
17
|
|
|
17
|
-
Displays rooms and accessories with real-time state updates
|
|
18
|
-
|
|
18
|
+
Displays rooms and accessories with real-time state updates.
|
|
19
|
+
Select accessory with action key, then perform action on selection.
|
|
19
20
|
|
|
20
21
|
Attributes:
|
|
21
22
|
homekit_service: HomekitService for accessory state operations.
|
|
23
|
+
selected_accessory_id: Currently selected accessory ID.
|
|
24
|
+
_last_cursor_row: Last cursor row for direction detection.
|
|
22
25
|
CSS_PATH: Path to CSS stylesheet file.
|
|
23
26
|
BINDINGS: Keyboard bindings for app actions.
|
|
24
27
|
TITLE: Application title displayed in header.
|
|
@@ -32,7 +35,12 @@ class HomekitApp(App[None]):
|
|
|
32
35
|
BINDINGS = [
|
|
33
36
|
("Q", "quit", "Quit"),
|
|
34
37
|
("C", "toggle_connection", "Connect"),
|
|
35
|
-
("
|
|
38
|
+
("R", "refresh_all", "Refresh"),
|
|
39
|
+
("space", "toggle_selected", "Toggle"),
|
|
40
|
+
("full_stop", "turn_on_selected", "On"),
|
|
41
|
+
("minus", "turn_off_selected", "Off"),
|
|
42
|
+
("plus", "dim_up", "Dim+"),
|
|
43
|
+
("quotation_mark", "dim_down", "Dim-"),
|
|
36
44
|
]
|
|
37
45
|
|
|
38
46
|
def __init__(self, homekit_service: HomekitService) -> None:
|
|
@@ -44,6 +52,8 @@ class HomekitApp(App[None]):
|
|
|
44
52
|
"""
|
|
45
53
|
super().__init__()
|
|
46
54
|
self.homekit_service: HomekitService = homekit_service
|
|
55
|
+
self.selected_accessory_id: Optional[str] = None
|
|
56
|
+
self._last_cursor_row: int = 0
|
|
47
57
|
self.room_list_widget: Optional[RoomListWidget] = None
|
|
48
58
|
self.footer_widget: Optional[StatusFooterWidget] = None
|
|
49
59
|
|
|
@@ -87,17 +97,114 @@ class HomekitApp(App[None]):
|
|
|
87
97
|
|
|
88
98
|
def on_key(self, event: Any) -> None:
|
|
89
99
|
"""
|
|
90
|
-
Handle key press events for action keys.
|
|
100
|
+
Handle key press events for selection and action keys.
|
|
91
101
|
|
|
92
|
-
|
|
102
|
+
Selection keys (a-z0-9): Select accessory row.
|
|
103
|
+
Action keys (on selected accessory):
|
|
104
|
+
- Space: Toggle
|
|
105
|
+
- . : Turn ON
|
|
106
|
+
- - : Turn OFF
|
|
107
|
+
- + : Dim up
|
|
108
|
+
- " : Dim down
|
|
93
109
|
|
|
94
110
|
Args:
|
|
95
111
|
event: Key press event.
|
|
96
112
|
"""
|
|
97
|
-
key = event.key
|
|
98
|
-
|
|
99
|
-
|
|
113
|
+
key = event.key
|
|
114
|
+
|
|
115
|
+
# Selection keys (a-z0-9)
|
|
116
|
+
if len(key) == 1 and (("a" <= key <= "z") or ("0" <= key <= "9")):
|
|
117
|
+
accessory_id = self.homekit_service.select_accessory(key)
|
|
118
|
+
if accessory_id:
|
|
119
|
+
self.selected_accessory_id = accessory_id
|
|
120
|
+
self._select_row(key)
|
|
100
121
|
event.prevent_default()
|
|
122
|
+
return
|
|
123
|
+
|
|
124
|
+
# Action keys (require selection)
|
|
125
|
+
if not self.selected_accessory_id:
|
|
126
|
+
return
|
|
127
|
+
|
|
128
|
+
if key == "space":
|
|
129
|
+
self.homekit_service.toggle_selected(self.selected_accessory_id)
|
|
130
|
+
event.prevent_default()
|
|
131
|
+
elif key in ("full_stop", "."):
|
|
132
|
+
self.homekit_service.turn_on_selected(self.selected_accessory_id)
|
|
133
|
+
event.prevent_default()
|
|
134
|
+
elif key in ("minus", "-"):
|
|
135
|
+
self.homekit_service.turn_off_selected(self.selected_accessory_id)
|
|
136
|
+
event.prevent_default()
|
|
137
|
+
elif key in ("plus", "+"):
|
|
138
|
+
self.homekit_service.increase_dimmer(self.selected_accessory_id)
|
|
139
|
+
event.prevent_default()
|
|
140
|
+
elif key in ("quotation_mark", '"'):
|
|
141
|
+
self.homekit_service.decrease_dimmer(self.selected_accessory_id)
|
|
142
|
+
event.prevent_default()
|
|
143
|
+
|
|
144
|
+
def _select_row(self, action_key: str) -> None:
|
|
145
|
+
"""
|
|
146
|
+
Select row in RoomListWidget by action key.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
action_key: Action key to select.
|
|
150
|
+
"""
|
|
151
|
+
if self.room_list_widget:
|
|
152
|
+
self.room_list_widget.select_by_action_key(action_key)
|
|
153
|
+
|
|
154
|
+
def on_data_table_row_highlighted(self, event: DataTable.RowHighlighted) -> None:
|
|
155
|
+
"""
|
|
156
|
+
Handle row highlight changes from arrow key navigation.
|
|
157
|
+
|
|
158
|
+
Updates selected_accessory_id when cursor moves via arrow keys.
|
|
159
|
+
Skips non-accessory rows (layout rows) automatically.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
event: Row highlighted event from DataTable.
|
|
163
|
+
"""
|
|
164
|
+
if not self.room_list_widget or not event.row_key:
|
|
165
|
+
return
|
|
166
|
+
|
|
167
|
+
accessory_id = self.room_list_widget.get_accessory_id_for_row(event.row_key)
|
|
168
|
+
if accessory_id:
|
|
169
|
+
self.selected_accessory_id = accessory_id
|
|
170
|
+
self._last_cursor_row = event.cursor_row
|
|
171
|
+
else:
|
|
172
|
+
# Non-accessory row (layout), skip to next valid row
|
|
173
|
+
self._skip_to_accessory_row(event.cursor_row)
|
|
174
|
+
|
|
175
|
+
def _skip_to_accessory_row(self, current_row: int) -> None:
|
|
176
|
+
"""
|
|
177
|
+
Skip cursor to the nearest accessory row.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
current_row: Current cursor row index.
|
|
181
|
+
"""
|
|
182
|
+
if not self.room_list_widget or not self.room_list_widget.table:
|
|
183
|
+
return
|
|
184
|
+
|
|
185
|
+
table = self.room_list_widget.table
|
|
186
|
+
row_count = table.row_count
|
|
187
|
+
|
|
188
|
+
# Determine direction based on last position
|
|
189
|
+
direction = 1 if current_row >= self._last_cursor_row else -1
|
|
190
|
+
|
|
191
|
+
# Search for next accessory row in direction
|
|
192
|
+
next_row = current_row + direction
|
|
193
|
+
while 0 <= next_row < row_count:
|
|
194
|
+
row_key = self.room_list_widget.get_row_key_at_index(next_row)
|
|
195
|
+
if row_key and self.room_list_widget.get_accessory_id_for_row(row_key):
|
|
196
|
+
table.move_cursor(row=next_row)
|
|
197
|
+
return
|
|
198
|
+
next_row += direction
|
|
199
|
+
|
|
200
|
+
# If not found in direction, try opposite direction
|
|
201
|
+
next_row = current_row - direction
|
|
202
|
+
while 0 <= next_row < row_count:
|
|
203
|
+
row_key = self.room_list_widget.get_row_key_at_index(next_row)
|
|
204
|
+
if row_key and self.room_list_widget.get_accessory_id_for_row(row_key):
|
|
205
|
+
table.move_cursor(row=next_row)
|
|
206
|
+
return
|
|
207
|
+
next_row -= direction
|
|
101
208
|
|
|
102
209
|
def action_toggle_connection(self) -> None:
|
|
103
210
|
"""
|
|
@@ -111,6 +218,31 @@ class HomekitApp(App[None]):
|
|
|
111
218
|
"""Refresh all module data on 'r' key press."""
|
|
112
219
|
self.homekit_service.refresh_all()
|
|
113
220
|
|
|
221
|
+
def action_toggle_selected(self) -> None:
|
|
222
|
+
"""Toggle selected accessory."""
|
|
223
|
+
if self.selected_accessory_id:
|
|
224
|
+
self.homekit_service.toggle_selected(self.selected_accessory_id)
|
|
225
|
+
|
|
226
|
+
def action_turn_on_selected(self) -> None:
|
|
227
|
+
"""Turn on selected accessory."""
|
|
228
|
+
if self.selected_accessory_id:
|
|
229
|
+
self.homekit_service.turn_on_selected(self.selected_accessory_id)
|
|
230
|
+
|
|
231
|
+
def action_turn_off_selected(self) -> None:
|
|
232
|
+
"""Turn off selected accessory."""
|
|
233
|
+
if self.selected_accessory_id:
|
|
234
|
+
self.homekit_service.turn_off_selected(self.selected_accessory_id)
|
|
235
|
+
|
|
236
|
+
def action_dim_up(self) -> None:
|
|
237
|
+
"""Increase dimmer on selected accessory."""
|
|
238
|
+
if self.selected_accessory_id:
|
|
239
|
+
self.homekit_service.increase_dimmer(self.selected_accessory_id)
|
|
240
|
+
|
|
241
|
+
def action_dim_down(self) -> None:
|
|
242
|
+
"""Decrease dimmer on selected accessory."""
|
|
243
|
+
if self.selected_accessory_id:
|
|
244
|
+
self.homekit_service.decrease_dimmer(self.selected_accessory_id)
|
|
245
|
+
|
|
114
246
|
async def on_unmount(self) -> None:
|
|
115
247
|
"""Stop AccessoryDriver and clean up service when app unmounts."""
|
|
116
248
|
await self.homekit_service.stop()
|
xp/term/homekit.tcss
CHANGED
|
@@ -45,13 +45,13 @@ DataTable > .datatable--header {
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
DataTable > .datatable--cursor {
|
|
48
|
-
background: $
|
|
49
|
-
color: $
|
|
48
|
+
background: $success;
|
|
49
|
+
color: $background;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
DataTable:focus > .datatable--cursor {
|
|
53
|
-
background: $
|
|
54
|
-
color: $
|
|
53
|
+
background: $success;
|
|
54
|
+
color: $background;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
/* Footer styling */
|
xp/term/widgets/room_list.py
CHANGED
|
@@ -41,6 +41,9 @@ class RoomListWidget(Static):
|
|
|
41
41
|
self.service = service
|
|
42
42
|
self.table: Optional[DataTable] = None
|
|
43
43
|
self._row_keys: dict[str, Any] = {} # Map accessory_id to row key
|
|
44
|
+
self._row_to_accessory: dict[Any, str] = {} # Map row key to accessory_id
|
|
45
|
+
self._row_index_to_key: list[Any] = [] # Map row index to row key
|
|
46
|
+
self._action_to_row: dict[str, Any] = {} # Map action key to row key
|
|
44
47
|
self._current_room: str = ""
|
|
45
48
|
|
|
46
49
|
def compose(self) -> ComposeResult:
|
|
@@ -93,15 +96,23 @@ class RoomListWidget(Static):
|
|
|
93
96
|
|
|
94
97
|
self.table.clear()
|
|
95
98
|
self._row_keys.clear()
|
|
99
|
+
self._row_to_accessory.clear()
|
|
100
|
+
self._row_index_to_key.clear()
|
|
101
|
+
self._action_to_row.clear()
|
|
96
102
|
self._current_room = ""
|
|
97
103
|
|
|
98
104
|
for state in accessory_states:
|
|
99
105
|
# Add room header row if new room
|
|
100
106
|
if state.room_name != self._current_room:
|
|
101
107
|
self._current_room = state.room_name
|
|
102
|
-
|
|
103
|
-
self.
|
|
104
|
-
|
|
108
|
+
# Add layout rows (empty and header) - not selectable
|
|
109
|
+
self._row_index_to_key.extend(
|
|
110
|
+
[
|
|
111
|
+
self.table.add_row(),
|
|
112
|
+
self.table.add_row(Text(state.room_name, style="bold")),
|
|
113
|
+
self.table.add_row(),
|
|
114
|
+
]
|
|
115
|
+
)
|
|
105
116
|
|
|
106
117
|
self._add_accessory_row(state)
|
|
107
118
|
|
|
@@ -160,6 +171,10 @@ class RoomListWidget(Static):
|
|
|
160
171
|
Text(self._format_last_update(state.last_update), justify="center"),
|
|
161
172
|
)
|
|
162
173
|
self._row_keys[accessory_id] = row_key
|
|
174
|
+
self._row_to_accessory[row_key] = accessory_id
|
|
175
|
+
self._row_index_to_key.append(row_key)
|
|
176
|
+
if state.action:
|
|
177
|
+
self._action_to_row[state.action] = row_key
|
|
163
178
|
|
|
164
179
|
def _format_dim(self, state: AccessoryState) -> str:
|
|
165
180
|
"""
|
|
@@ -230,3 +245,46 @@ class RoomListWidget(Static):
|
|
|
230
245
|
justify="center",
|
|
231
246
|
),
|
|
232
247
|
)
|
|
248
|
+
|
|
249
|
+
def select_by_action_key(self, action_key: str) -> None:
|
|
250
|
+
"""
|
|
251
|
+
Select and highlight row by action key.
|
|
252
|
+
|
|
253
|
+
Moves the table cursor to the row corresponding to the action key.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
action_key: Action key (a-z0-9) to select.
|
|
257
|
+
"""
|
|
258
|
+
if not self.table:
|
|
259
|
+
return
|
|
260
|
+
|
|
261
|
+
row_key = self._action_to_row.get(action_key)
|
|
262
|
+
if row_key is not None:
|
|
263
|
+
row_index = self.table.get_row_index(row_key)
|
|
264
|
+
self.table.move_cursor(row=row_index)
|
|
265
|
+
|
|
266
|
+
def get_accessory_id_for_row(self, row_key: Any) -> Optional[str]:
|
|
267
|
+
"""
|
|
268
|
+
Get accessory ID for a row key.
|
|
269
|
+
|
|
270
|
+
Args:
|
|
271
|
+
row_key: DataTable row key.
|
|
272
|
+
|
|
273
|
+
Returns:
|
|
274
|
+
Accessory ID if found, None otherwise.
|
|
275
|
+
"""
|
|
276
|
+
return self._row_to_accessory.get(row_key)
|
|
277
|
+
|
|
278
|
+
def get_row_key_at_index(self, index: int) -> Optional[Any]:
|
|
279
|
+
"""
|
|
280
|
+
Get row key at a given index.
|
|
281
|
+
|
|
282
|
+
Args:
|
|
283
|
+
index: Row index.
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
Row key if valid index, None otherwise.
|
|
287
|
+
"""
|
|
288
|
+
if 0 <= index < len(self._row_index_to_key):
|
|
289
|
+
return self._row_index_to_key[index]
|
|
290
|
+
return None
|