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.
- {conson_xp-1.52.0.dist-info → conson_xp-2.0.0.dist-info}/METADATA +1 -11
- {conson_xp-1.52.0.dist-info → conson_xp-2.0.0.dist-info}/RECORD +19 -38
- 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 +0 -3
- 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 +5 -5
- 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.0.dist-info}/WHEEL +0 -0
- {conson_xp-1.52.0.dist-info → conson_xp-2.0.0.dist-info}/entry_points.txt +0 -0
- {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
|
-
|
|
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
|
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,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)
|