conson-xp 1.52.0__py3-none-any.whl → 2.0.0__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.
Files changed (38) hide show
  1. {conson_xp-1.52.0.dist-info → conson_xp-2.0.0.dist-info}/METADATA +1 -11
  2. {conson_xp-1.52.0.dist-info → conson_xp-2.0.0.dist-info}/RECORD +19 -38
  3. xp/__init__.py +1 -1
  4. xp/cli/commands/__init__.py +0 -4
  5. xp/cli/commands/term/term_commands.py +1 -1
  6. xp/cli/main.py +0 -3
  7. xp/models/protocol/conbus_protocol.py +30 -25
  8. xp/models/term/accessory_state.py +1 -1
  9. xp/services/protocol/__init__.py +2 -3
  10. xp/services/protocol/conbus_event_protocol.py +5 -5
  11. xp/services/term/homekit_accessory_driver.py +5 -2
  12. xp/services/term/homekit_service.py +118 -11
  13. xp/term/homekit.py +140 -8
  14. xp/term/homekit.tcss +4 -4
  15. xp/term/widgets/room_list.py +61 -3
  16. xp/utils/dependencies.py +24 -154
  17. xp/cli/commands/homekit/__init__.py +0 -3
  18. xp/cli/commands/homekit/homekit.py +0 -120
  19. xp/cli/commands/homekit/homekit_start_commands.py +0 -44
  20. xp/services/homekit/__init__.py +0 -1
  21. xp/services/homekit/homekit_cache_service.py +0 -313
  22. xp/services/homekit/homekit_conbus_service.py +0 -99
  23. xp/services/homekit/homekit_config_validator.py +0 -327
  24. xp/services/homekit/homekit_conson_validator.py +0 -130
  25. xp/services/homekit/homekit_dimminglight.py +0 -189
  26. xp/services/homekit/homekit_dimminglight_service.py +0 -155
  27. xp/services/homekit/homekit_hap_service.py +0 -351
  28. xp/services/homekit/homekit_lightbulb.py +0 -125
  29. xp/services/homekit/homekit_lightbulb_service.py +0 -91
  30. xp/services/homekit/homekit_module_service.py +0 -60
  31. xp/services/homekit/homekit_outlet.py +0 -175
  32. xp/services/homekit/homekit_outlet_service.py +0 -127
  33. xp/services/homekit/homekit_service.py +0 -371
  34. xp/services/protocol/protocol_factory.py +0 -84
  35. xp/services/protocol/telegram_protocol.py +0 -270
  36. {conson_xp-1.52.0.dist-info → conson_xp-2.0.0.dist-info}/WHEEL +0 -0
  37. {conson_xp-1.52.0.dist-info → conson_xp-2.0.0.dist-info}/entry_points.txt +0 -0
  38. {conson_xp-1.52.0.dist-info → conson_xp-2.0.0.dist-info}/licenses/LICENSE +0 -0
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
- and toggle control via action keys.
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
- ("r", "refresh_all", "Refresh"),
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
- Intercepts a-z keys to toggle accessories.
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.lower()
98
- if len(key) == 1 and "a" <= key <= "z":
99
- if self.homekit_service.toggle_accessory(key):
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: $background;
49
- color: $success;
48
+ background: $success;
49
+ color: $background;
50
50
  }
51
51
 
52
52
  DataTable:focus > .datatable--cursor {
53
- background: $background;
54
- color: $success;
53
+ background: $success;
54
+ color: $background;
55
55
  }
56
56
 
57
57
  /* Footer styling */
@@ -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
- self.table.add_row()
103
- self.table.add_row(Text(state.room_name, style="bold"))
104
- self.table.add_row()
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
xp/utils/dependencies.py CHANGED
@@ -1,9 +1,7 @@
1
1
  """Dependency injection container for XP services."""
2
2
 
3
3
  import punq
4
- from bubus import EventBus
5
4
  from twisted.internet import asyncioreactor
6
- from twisted.internet.interfaces import IConnector
7
5
  from twisted.internet.posixbase import PosixReactorBase
8
6
 
9
7
  from xp.models import ConbusClientConfig
@@ -55,19 +53,9 @@ from xp.services.conbus.conbus_raw_service import ConbusRawService
55
53
  from xp.services.conbus.conbus_receive_service import ConbusReceiveService
56
54
  from xp.services.conbus.conbus_scan_service import ConbusScanService
57
55
  from xp.services.conbus.write_config_service import WriteConfigService
58
- from xp.services.homekit.homekit_cache_service import HomeKitCacheService
59
- from xp.services.homekit.homekit_conbus_service import HomeKitConbusService
60
- from xp.services.homekit.homekit_dimminglight_service import HomeKitDimmingLightService
61
- from xp.services.homekit.homekit_hap_service import HomekitHapService
62
- from xp.services.homekit.homekit_lightbulb_service import HomeKitLightbulbService
63
- from xp.services.homekit.homekit_module_service import HomekitModuleService
64
- from xp.services.homekit.homekit_outlet_service import HomeKitOutletService
65
- from xp.services.homekit.homekit_service import HomeKitService
66
56
  from xp.services.log_file_service import LogFileService
67
57
  from xp.services.module_type_service import ModuleTypeService
68
58
  from xp.services.protocol import ConbusEventProtocol
69
- from xp.services.protocol.protocol_factory import TelegramFactory
70
- from xp.services.protocol.telegram_protocol import TelegramProtocol
71
59
  from xp.services.reverse_proxy_service import ReverseProxyService
72
60
  from xp.services.server.device_service_factory import DeviceServiceFactory
73
61
  from xp.services.server.server_service import ServerService
@@ -169,7 +157,24 @@ class ServiceContainer:
169
157
  self.container.register(TelegramDatapointService, scope=punq.Scope.singleton)
170
158
  self.container.register(LinkNumberService, scope=punq.Scope.singleton)
171
159
 
160
+ # Reactor
161
+ self.container.register(
162
+ PosixReactorBase,
163
+ factory=lambda: reactor,
164
+ scope=punq.Scope.singleton,
165
+ )
166
+
172
167
  # Conbus services layer
168
+ self.container.register(
169
+ ConbusEventProtocol,
170
+ factory=lambda: ConbusEventProtocol(
171
+ cli_config=self.container.resolve(ConbusClientConfig),
172
+ reactor=self.container.resolve(PosixReactorBase),
173
+ telegram_service=self.container.resolve(TelegramService),
174
+ ),
175
+ scope=punq.Scope.singleton,
176
+ )
177
+
173
178
  self.container.register(
174
179
  ConbusDatapointService,
175
180
  factory=lambda: ConbusDatapointService(
@@ -196,16 +201,6 @@ class ServiceContainer:
196
201
  scope=punq.Scope.singleton,
197
202
  )
198
203
 
199
- self.container.register(
200
- ConbusEventProtocol,
201
- factory=lambda: ConbusEventProtocol(
202
- cli_config=self.container.resolve(ConbusClientConfig),
203
- reactor=self.container.resolve(PosixReactorBase),
204
- telegram_service=self.container.resolve(TelegramService),
205
- ),
206
- scope=punq.Scope.singleton,
207
- )
208
-
209
204
  self.container.register(
210
205
  ConbusDiscoverService,
211
206
  factory=lambda: ConbusDiscoverService(
@@ -268,6 +263,13 @@ class ServiceContainer:
268
263
  scope=punq.Scope.singleton,
269
264
  )
270
265
 
266
+ # HomeKit config
267
+ self.container.register(
268
+ HomekitConfig,
269
+ factory=lambda: HomekitConfig.from_yaml(self._homekit_config_path),
270
+ scope=punq.Scope.singleton,
271
+ )
272
+
271
273
  self.container.register(
272
274
  HomekitAccessoryDriver,
273
275
  factory=lambda: HomekitAccessoryDriver(
@@ -441,39 +443,6 @@ class ServiceContainer:
441
443
  scope=punq.Scope.singleton,
442
444
  )
443
445
 
444
- # HomeKit services layer
445
- self.container.register(
446
- HomekitModuleService,
447
- factory=lambda: HomekitModuleService(
448
- conson_modules_config=self.container.resolve(ConsonModuleListConfig),
449
- ),
450
- scope=punq.Scope.singleton,
451
- )
452
-
453
- # Create event bus
454
- self.container.register(
455
- EventBus,
456
- factory=lambda: EventBus(max_history_size=500),
457
- scope=punq.Scope.singleton,
458
- )
459
-
460
- # HomeKit conson config
461
- self.container.register(
462
- HomekitConfig,
463
- factory=lambda: HomekitConfig.from_yaml(self._homekit_config_path),
464
- scope=punq.Scope.singleton,
465
- )
466
-
467
- self.container.register(
468
- HomekitHapService,
469
- factory=lambda: HomekitHapService(
470
- homekit_config=self.container.resolve(HomekitConfig),
471
- module_service=self.container.resolve(HomekitModuleService),
472
- event_bus=self.container.resolve(EventBus),
473
- ),
474
- scope=punq.Scope.singleton,
475
- )
476
-
477
446
  # Log file services layer
478
447
  self.container.register(
479
448
  LogFileService,
@@ -536,105 +505,6 @@ class ServiceContainer:
536
505
  scope=punq.Scope.singleton,
537
506
  )
538
507
 
539
- # Create protocol with built-in debouncing
540
- self.container.register(
541
- TelegramProtocol,
542
- factory=lambda: TelegramProtocol(
543
- event_bus=self.container.resolve(EventBus),
544
- debounce_ms=50,
545
- ),
546
- scope=punq.Scope.singleton,
547
- )
548
-
549
- self.container.register(
550
- IConnector,
551
- factory=lambda: reactor,
552
- scope=punq.Scope.singleton,
553
- )
554
-
555
- self.container.register(
556
- TelegramFactory,
557
- factory=lambda: TelegramFactory(
558
- event_bus=self.container.resolve(EventBus),
559
- telegram_protocol=self.container.resolve(TelegramProtocol),
560
- connector=self.container.resolve(IConnector),
561
- ),
562
- scope=punq.Scope.singleton,
563
- )
564
-
565
- self.container.register(
566
- PosixReactorBase,
567
- factory=lambda: reactor,
568
- scope=punq.Scope.singleton,
569
- )
570
-
571
- self.container.register(
572
- HomeKitLightbulbService,
573
- factory=lambda: HomeKitLightbulbService(
574
- event_bus=self.container.resolve(EventBus),
575
- ),
576
- scope=punq.Scope.singleton,
577
- )
578
-
579
- self.container.register(
580
- HomeKitOutletService,
581
- factory=lambda: HomeKitOutletService(
582
- event_bus=self.container.resolve(EventBus),
583
- ),
584
- scope=punq.Scope.singleton,
585
- )
586
-
587
- self.container.register(
588
- HomeKitDimmingLightService,
589
- factory=lambda: HomeKitDimmingLightService(
590
- event_bus=self.container.resolve(EventBus),
591
- ),
592
- scope=punq.Scope.singleton,
593
- )
594
-
595
- # Cache service must be registered BEFORE HomeKitConbusService
596
- # so it intercepts ReadDatapointEvent first
597
- self.container.register(
598
- HomeKitCacheService,
599
- factory=lambda: HomeKitCacheService(
600
- event_bus=self.container.resolve(EventBus),
601
- ),
602
- scope=punq.Scope.singleton,
603
- )
604
-
605
- self.container.register(
606
- HomeKitConbusService,
607
- factory=lambda: HomeKitConbusService(
608
- event_bus=self.container.resolve(EventBus),
609
- telegram_protocol=self.container.resolve(TelegramProtocol),
610
- ),
611
- scope=punq.Scope.singleton,
612
- )
613
-
614
- self.container.register(
615
- TelegramService,
616
- factory=TelegramService,
617
- scope=punq.Scope.singleton,
618
- )
619
-
620
- self.container.register(
621
- HomeKitService,
622
- factory=lambda: HomeKitService(
623
- cli_config=self.container.resolve(ConbusClientConfig),
624
- event_bus=self.container.resolve(EventBus),
625
- telegram_factory=self.container.resolve(TelegramFactory),
626
- reactor=self.container.resolve(PosixReactorBase),
627
- lightbulb_service=self.container.resolve(HomeKitLightbulbService),
628
- outlet_service=self.container.resolve(HomeKitOutletService),
629
- dimminglight_service=self.container.resolve(HomeKitDimmingLightService),
630
- cache_service=self.container.resolve(HomeKitCacheService),
631
- conbus_service=self.container.resolve(HomeKitConbusService),
632
- module_factory=self.container.resolve(HomekitHapService),
633
- telegram_service=self.container.resolve(TelegramService),
634
- ),
635
- scope=punq.Scope.singleton,
636
- )
637
-
638
508
  def _load_protocol_keys(self) -> "ProtocolKeysConfig":
639
509
  """
640
510
  Load protocol keys from YAML config file.
@@ -1,3 +0,0 @@
1
- """HomeKit CLI commands package."""
2
-
3
- __all__ = []
@@ -1,120 +0,0 @@
1
- """HomeKit management CLI commands."""
2
-
3
- import click
4
- from click_help_colors import HelpColorsGroup
5
-
6
- from xp.cli.utils.decorators import service_command
7
-
8
-
9
- @click.group(
10
- cls=HelpColorsGroup, help_headers_color="yellow", help_options_color="green"
11
- )
12
- def homekit() -> None:
13
- """Manage the HomeKit server for XP Protocol operations."""
14
- pass
15
-
16
-
17
- @homekit.group(
18
- cls=HelpColorsGroup, help_headers_color="yellow", help_options_color="green"
19
- )
20
- def config() -> None:
21
- """Manage HomeKit configuration."""
22
- pass
23
-
24
-
25
- @config.command()
26
- @click.option(
27
- "--conson-config",
28
- default="conson.yml",
29
- help="Path to conson.yml configuration file",
30
- )
31
- @click.option(
32
- "--homekit-config",
33
- default="homekit.yml",
34
- help="Path to homekit.yml configuration file",
35
- )
36
- @service_command()
37
- def validate(conson_config: str, homekit_config: str) -> None:
38
- """
39
- Validate homekit.yml and conson.yml coherence.
40
-
41
- Args:
42
- conson_config: Path to conson.yml configuration file.
43
- homekit_config: Path to homekit.yml configuration file.
44
- """
45
- from xp.services.homekit.homekit_config_validator import ConfigValidationService
46
-
47
- try:
48
- validator = ConfigValidationService(conson_config, homekit_config)
49
- results = validator.validate_all()
50
-
51
- if results["is_valid"]:
52
- click.echo(click.style("✓ Configuration validation passed", fg="green"))
53
- else:
54
- click.echo(
55
- click.style(
56
- f"✗ Configuration validation failed with {results['total_errors']} errors",
57
- fg="red",
58
- )
59
- )
60
-
61
- if results["conson_errors"]:
62
- click.echo(
63
- click.style("\nConson Configuration Errors:", fg="red", bold=True)
64
- )
65
- for error in results["conson_errors"]:
66
- click.echo(f" - {error}")
67
-
68
- if results["homekit_errors"]:
69
- click.echo(
70
- click.style("\nHomeKit Configuration Errors:", fg="red", bold=True)
71
- )
72
- for error in results["homekit_errors"]:
73
- click.echo(f" - {error}")
74
-
75
- if results["cross_reference_errors"]:
76
- click.echo(
77
- click.style("\nCross-Reference Errors:", fg="red", bold=True)
78
- )
79
- for error in results["cross_reference_errors"]:
80
- click.echo(f" - {error}")
81
-
82
- exit(1)
83
-
84
- except Exception as e:
85
- click.echo(click.style(f"✗ Validation failed: {e}", fg="red"))
86
- exit(1)
87
-
88
-
89
- @config.command("show")
90
- @click.option(
91
- "--conson-config",
92
- default="conson.yml",
93
- help="Path to conson.yml configuration file",
94
- )
95
- @click.option(
96
- "--homekit-config",
97
- default="homekit.yml",
98
- help="Path to homekit.yml configuration file",
99
- )
100
- @service_command()
101
- def show_config(conson_config: str, homekit_config: str) -> None:
102
- """
103
- Display parsed configuration summary.
104
-
105
- Args:
106
- conson_config: Path to conson.yml configuration file.
107
- homekit_config: Path to homekit.yml configuration file.
108
- """
109
- from xp.services.homekit.homekit_config_validator import ConfigValidationService
110
-
111
- try:
112
- validator = ConfigValidationService(conson_config, homekit_config)
113
- summary = validator.print_config_summary()
114
-
115
- click.echo(click.style("Configuration Summary:", fg="blue", bold=True))
116
- click.echo(summary)
117
-
118
- except Exception as e:
119
- click.echo(click.style(f"✗ Failed to load configuration: {e}", fg="red"))
120
- exit(1)