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
|
@@ -1,155 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
HomeKit Dimming Light Service.
|
|
3
|
-
|
|
4
|
-
This module provides service implementation for dimming light accessories.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
import logging
|
|
8
|
-
|
|
9
|
-
from bubus import EventBus
|
|
10
|
-
|
|
11
|
-
from xp.models.protocol.conbus_protocol import (
|
|
12
|
-
DimmingLightGetBrightnessEvent,
|
|
13
|
-
DimmingLightGetOnEvent,
|
|
14
|
-
DimmingLightSetBrightnessEvent,
|
|
15
|
-
DimmingLightSetOnEvent,
|
|
16
|
-
ReadDatapointEvent,
|
|
17
|
-
SendWriteConfigEvent,
|
|
18
|
-
)
|
|
19
|
-
from xp.models.telegram.datapoint_type import DataPointType
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
class HomeKitDimmingLightService:
|
|
23
|
-
"""
|
|
24
|
-
Dimming light service for HomeKit.
|
|
25
|
-
|
|
26
|
-
Attributes:
|
|
27
|
-
event_bus: Event bus for inter-service communication.
|
|
28
|
-
logger: Logger instance.
|
|
29
|
-
"""
|
|
30
|
-
|
|
31
|
-
event_bus: EventBus
|
|
32
|
-
|
|
33
|
-
def __init__(self, event_bus: EventBus) -> None:
|
|
34
|
-
"""
|
|
35
|
-
Initialize the dimming light service.
|
|
36
|
-
|
|
37
|
-
Args:
|
|
38
|
-
event_bus: Event bus instance.
|
|
39
|
-
"""
|
|
40
|
-
self.logger = logging.getLogger(__name__)
|
|
41
|
-
self.event_bus = event_bus
|
|
42
|
-
|
|
43
|
-
# Register event handlers
|
|
44
|
-
self.event_bus.on(DimmingLightGetOnEvent, self.handle_dimminglight_get_on)
|
|
45
|
-
self.event_bus.on(DimmingLightSetOnEvent, self.handle_dimminglight_set_on)
|
|
46
|
-
self.event_bus.on(
|
|
47
|
-
DimmingLightSetBrightnessEvent, self.handle_dimminglight_set_brightness
|
|
48
|
-
)
|
|
49
|
-
self.event_bus.on(
|
|
50
|
-
DimmingLightGetBrightnessEvent, self.handle_dimminglight_get_brightness
|
|
51
|
-
)
|
|
52
|
-
|
|
53
|
-
def handle_dimminglight_get_on(self, event: DimmingLightGetOnEvent) -> None:
|
|
54
|
-
"""
|
|
55
|
-
Handle dimming light get on event.
|
|
56
|
-
|
|
57
|
-
Args:
|
|
58
|
-
event: Dimming light get on event.
|
|
59
|
-
"""
|
|
60
|
-
self.logger.info(
|
|
61
|
-
f"Getting dimming light state for serial {event.serial_number}, output {event.output_number}"
|
|
62
|
-
)
|
|
63
|
-
self.logger.debug(f"dimminglight_get_on {event}")
|
|
64
|
-
|
|
65
|
-
read_datapoint = ReadDatapointEvent(
|
|
66
|
-
serial_number=event.serial_number,
|
|
67
|
-
datapoint_type=DataPointType.MODULE_OUTPUT_STATE,
|
|
68
|
-
)
|
|
69
|
-
self.logger.debug(f"Dispatching ReadDatapointEvent for {event.serial_number}")
|
|
70
|
-
self.event_bus.dispatch(read_datapoint)
|
|
71
|
-
self.logger.debug(f"Dispatched ReadDatapointEvent for {event.serial_number}")
|
|
72
|
-
|
|
73
|
-
def handle_dimminglight_set_on(self, event: DimmingLightSetOnEvent) -> None:
|
|
74
|
-
"""
|
|
75
|
-
Handle dimming light set on event.
|
|
76
|
-
|
|
77
|
-
Args:
|
|
78
|
-
event: Dimming light set on event.
|
|
79
|
-
"""
|
|
80
|
-
brightness = event.brightness if event.value else 0
|
|
81
|
-
self.logger.debug(
|
|
82
|
-
f"Setting on light for "
|
|
83
|
-
f"serial {event.serial_number}, "
|
|
84
|
-
f"output {event.output_number}, "
|
|
85
|
-
f"event_value: {event.value}, "
|
|
86
|
-
f"state: {'ON' if event.value else 'OFF'}, "
|
|
87
|
-
f"brightness: {brightness}"
|
|
88
|
-
)
|
|
89
|
-
self.logger.debug(f"dimminglight_set_on {event}")
|
|
90
|
-
|
|
91
|
-
datapoint_type = DataPointType.MODULE_LIGHT_LEVEL
|
|
92
|
-
send_action = SendWriteConfigEvent(
|
|
93
|
-
serial_number=event.serial_number,
|
|
94
|
-
output_number=event.output_number,
|
|
95
|
-
datapoint_type=datapoint_type,
|
|
96
|
-
value=brightness,
|
|
97
|
-
)
|
|
98
|
-
|
|
99
|
-
self.logger.debug(f"Dispatching SendWriteConfigEvent for {event.serial_number}")
|
|
100
|
-
self.event_bus.dispatch(send_action)
|
|
101
|
-
self.logger.debug(f"Dispatched SendWriteConfigEvent for {event.serial_number}")
|
|
102
|
-
|
|
103
|
-
def handle_dimminglight_set_brightness(
|
|
104
|
-
self, event: DimmingLightSetBrightnessEvent
|
|
105
|
-
) -> None:
|
|
106
|
-
"""
|
|
107
|
-
Handle dimming light set brightness event.
|
|
108
|
-
|
|
109
|
-
Args:
|
|
110
|
-
event: Dimming light set brightness event.
|
|
111
|
-
"""
|
|
112
|
-
self.logger.info(
|
|
113
|
-
f"Setting dimming light brightness"
|
|
114
|
-
f"serial {event.serial_number}, "
|
|
115
|
-
f"output {event.output_number} "
|
|
116
|
-
f"to {event.brightness}"
|
|
117
|
-
)
|
|
118
|
-
self.logger.debug(f"dimminglight_set_brightness {event}")
|
|
119
|
-
|
|
120
|
-
datapoint_type = DataPointType.MODULE_LIGHT_LEVEL
|
|
121
|
-
send_action = SendWriteConfigEvent(
|
|
122
|
-
serial_number=event.serial_number,
|
|
123
|
-
output_number=event.output_number,
|
|
124
|
-
datapoint_type=datapoint_type,
|
|
125
|
-
value=event.brightness,
|
|
126
|
-
)
|
|
127
|
-
|
|
128
|
-
self.logger.debug(f"Dispatching SendWriteConfigEvent for {event.serial_number}")
|
|
129
|
-
self.event_bus.dispatch(send_action)
|
|
130
|
-
self.logger.debug(f"Dispatched SendWriteConfigEvent for {event.serial_number}")
|
|
131
|
-
|
|
132
|
-
def handle_dimminglight_get_brightness(
|
|
133
|
-
self, event: DimmingLightGetBrightnessEvent
|
|
134
|
-
) -> None:
|
|
135
|
-
"""
|
|
136
|
-
Handle dimming light get brightness event.
|
|
137
|
-
|
|
138
|
-
Args:
|
|
139
|
-
event: Dimming light get brightness event.
|
|
140
|
-
"""
|
|
141
|
-
self.logger.info(
|
|
142
|
-
f"Getting dimming light brightness "
|
|
143
|
-
f"for serial {event.serial_number}, "
|
|
144
|
-
f"output {event.output_number}"
|
|
145
|
-
)
|
|
146
|
-
self.logger.debug(f"dimminglight_get_brightness {event}")
|
|
147
|
-
|
|
148
|
-
datapoint_type = DataPointType.MODULE_LIGHT_LEVEL
|
|
149
|
-
read_datapoint = ReadDatapointEvent(
|
|
150
|
-
serial_number=event.serial_number, datapoint_type=datapoint_type
|
|
151
|
-
)
|
|
152
|
-
|
|
153
|
-
self.logger.debug(f"Dispatching ReadDatapointEvent for {event.serial_number}")
|
|
154
|
-
self.event_bus.dispatch(read_datapoint)
|
|
155
|
-
self.logger.debug(f"Dispatched ReadDatapointEvent for {event.serial_number}")
|
|
@@ -1,351 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
HomeKit HAP Service for Apple HomeKit integration.
|
|
3
|
-
|
|
4
|
-
This module provides the main HAP (HomeKit Accessory Protocol) service.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
import logging
|
|
8
|
-
import signal
|
|
9
|
-
import threading
|
|
10
|
-
from datetime import datetime
|
|
11
|
-
from typing import Dict, List, Optional
|
|
12
|
-
|
|
13
|
-
from bubus import EventBus
|
|
14
|
-
from pyhap.accessory import Bridge
|
|
15
|
-
from pyhap.accessory_driver import AccessoryDriver
|
|
16
|
-
from typing_extensions import Union
|
|
17
|
-
|
|
18
|
-
import xp
|
|
19
|
-
from xp.models.homekit.homekit_accessory import TemperatureSensor
|
|
20
|
-
from xp.models.homekit.homekit_config import (
|
|
21
|
-
HomekitAccessoryConfig,
|
|
22
|
-
HomekitConfig,
|
|
23
|
-
RoomConfig,
|
|
24
|
-
)
|
|
25
|
-
from xp.models.protocol.conbus_protocol import (
|
|
26
|
-
LightLevelReceivedEvent,
|
|
27
|
-
ModuleStateChangedEvent,
|
|
28
|
-
OutputStateReceivedEvent,
|
|
29
|
-
ReadDatapointEvent,
|
|
30
|
-
)
|
|
31
|
-
from xp.models.telegram.datapoint_type import DataPointType
|
|
32
|
-
from xp.services.homekit.homekit_dimminglight import DimmingLight
|
|
33
|
-
from xp.services.homekit.homekit_lightbulb import LightBulb
|
|
34
|
-
from xp.services.homekit.homekit_module_service import HomekitModuleService
|
|
35
|
-
from xp.services.homekit.homekit_outlet import Outlet
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
class HomekitHapService:
|
|
39
|
-
"""
|
|
40
|
-
HomeKit HAP service.
|
|
41
|
-
|
|
42
|
-
Manages HAP accessory protocol, handles bridge and accessory setup,
|
|
43
|
-
and processes HomeKit events for device state synchronization.
|
|
44
|
-
|
|
45
|
-
Attributes:
|
|
46
|
-
event_bus: Event bus for inter-service communication.
|
|
47
|
-
last_activity: Timestamp of last service activity.
|
|
48
|
-
logger: Logger instance.
|
|
49
|
-
config: HomeKit configuration.
|
|
50
|
-
accessory_registry: Registry of accessories by identifier.
|
|
51
|
-
module_registry: Registry of accessories by module key.
|
|
52
|
-
modules: Module service for module lookup.
|
|
53
|
-
driver: HAP accessory driver.
|
|
54
|
-
"""
|
|
55
|
-
|
|
56
|
-
event_bus: EventBus
|
|
57
|
-
|
|
58
|
-
def __init__(
|
|
59
|
-
self,
|
|
60
|
-
homekit_config: HomekitConfig,
|
|
61
|
-
module_service: HomekitModuleService,
|
|
62
|
-
event_bus: EventBus,
|
|
63
|
-
):
|
|
64
|
-
"""
|
|
65
|
-
Initialize the HomeKit HAP service.
|
|
66
|
-
|
|
67
|
-
Args:
|
|
68
|
-
homekit_config: HomeKit configuration.
|
|
69
|
-
module_service: Module service for dependency injection.
|
|
70
|
-
event_bus: Event bus for dependency injection.
|
|
71
|
-
"""
|
|
72
|
-
self.last_activity: Optional[datetime] = None
|
|
73
|
-
|
|
74
|
-
# Set up logging
|
|
75
|
-
self.logger = logging.getLogger(__name__)
|
|
76
|
-
|
|
77
|
-
# Load configuration
|
|
78
|
-
self.config = homekit_config
|
|
79
|
-
self.accessory_registry: Dict[str, Union[LightBulb, Outlet, DimmingLight]] = {}
|
|
80
|
-
self.module_registry: Dict[
|
|
81
|
-
tuple[int, int], List[Union[LightBulb, Outlet, DimmingLight]]
|
|
82
|
-
] = {}
|
|
83
|
-
|
|
84
|
-
# Service dependencies
|
|
85
|
-
self.modules = module_service
|
|
86
|
-
self.event_bus = event_bus
|
|
87
|
-
|
|
88
|
-
# Subscribe to events
|
|
89
|
-
self.event_bus.on(ModuleStateChangedEvent, self.handle_module_state_changed)
|
|
90
|
-
self.event_bus.on(OutputStateReceivedEvent, self.handle_output_state_received)
|
|
91
|
-
self.event_bus.on(LightLevelReceivedEvent, self.handle_light_level_received)
|
|
92
|
-
|
|
93
|
-
# We want SIGTERM (terminate) to be handled by the driver itself,
|
|
94
|
-
# so that it can gracefully stop the accessory, server and advertising.
|
|
95
|
-
driver = AccessoryDriver(
|
|
96
|
-
port=self.config.homekit.port,
|
|
97
|
-
)
|
|
98
|
-
signal.signal(signal.SIGTERM, driver.signal_handler)
|
|
99
|
-
self.driver: AccessoryDriver = driver
|
|
100
|
-
|
|
101
|
-
async def async_start(self) -> None:
|
|
102
|
-
"""Start the HAP service asynchronously."""
|
|
103
|
-
self.logger.info("Loading accessories.")
|
|
104
|
-
self.build_bridge()
|
|
105
|
-
self.logger.info("Accessories loaded successfully")
|
|
106
|
-
|
|
107
|
-
# Start HAP-python in a separate thread to avoid event loop conflicts
|
|
108
|
-
self.logger.info("Starting HAP-python driver in separate thread.")
|
|
109
|
-
hap_thread = threading.Thread(
|
|
110
|
-
target=self._run_driver_in_thread, daemon=True, name="HAP-Python"
|
|
111
|
-
)
|
|
112
|
-
hap_thread.start()
|
|
113
|
-
self.logger.info("HAP-python driver thread started")
|
|
114
|
-
|
|
115
|
-
def _run_driver_in_thread(self) -> None:
|
|
116
|
-
"""Run the HAP-python driver in a separate thread with its own event loop."""
|
|
117
|
-
try:
|
|
118
|
-
self.logger.info("HAP-python thread starting, creating new event loop.")
|
|
119
|
-
# Create a new event loop for this thread
|
|
120
|
-
|
|
121
|
-
self.logger.info("Starting HAP-python driver.")
|
|
122
|
-
self.driver.start()
|
|
123
|
-
self.logger.info("HAP-python driver started successfully")
|
|
124
|
-
except Exception as e:
|
|
125
|
-
self.logger.error(f"HAP-python driver error: {e}", exc_info=True)
|
|
126
|
-
|
|
127
|
-
def handle_output_state_received(self, event: OutputStateReceivedEvent) -> str:
|
|
128
|
-
"""
|
|
129
|
-
Handle output state received event.
|
|
130
|
-
|
|
131
|
-
Args:
|
|
132
|
-
event: Output state received event.
|
|
133
|
-
|
|
134
|
-
Returns:
|
|
135
|
-
Data value from the event.
|
|
136
|
-
"""
|
|
137
|
-
self.logger.debug(f"Received OutputStateReceivedEvent {event}")
|
|
138
|
-
output_number = 0
|
|
139
|
-
for output in event.data_value[::-1]:
|
|
140
|
-
if output == "x":
|
|
141
|
-
break
|
|
142
|
-
identifier = f"{event.serial_number}.{output_number:02X}"
|
|
143
|
-
accessory = self.accessory_registry.get(identifier)
|
|
144
|
-
|
|
145
|
-
if not accessory:
|
|
146
|
-
self.logger.warning(f"Invalid accessory: {identifier} (not found)")
|
|
147
|
-
else:
|
|
148
|
-
accessory.is_on = True if output == "1" else False
|
|
149
|
-
output_number += 1
|
|
150
|
-
|
|
151
|
-
self.logger.debug(
|
|
152
|
-
f"handle_output_state_received "
|
|
153
|
-
f"serial_number: {event.serial_number}, "
|
|
154
|
-
f"data_vale: {event.data_value}"
|
|
155
|
-
)
|
|
156
|
-
return event.data_value
|
|
157
|
-
|
|
158
|
-
def handle_light_level_received(self, event: LightLevelReceivedEvent) -> str:
|
|
159
|
-
"""
|
|
160
|
-
Handle light level received event.
|
|
161
|
-
|
|
162
|
-
Args:
|
|
163
|
-
event: Light level received event.
|
|
164
|
-
|
|
165
|
-
Returns:
|
|
166
|
-
Data value from the event.
|
|
167
|
-
"""
|
|
168
|
-
# Parse response format like "00:050,01:025,02:100"
|
|
169
|
-
self.logger.debug("Received LightLevelReceivedEvent", extra={"event": event})
|
|
170
|
-
output_number = 0
|
|
171
|
-
for output_data in event.data_value.split(","):
|
|
172
|
-
if ":" in output_data:
|
|
173
|
-
output_str, level_str = output_data.split(":")
|
|
174
|
-
level_str = level_str.replace("[%]", "")
|
|
175
|
-
output_number = int(output_str)
|
|
176
|
-
brightness = int(level_str)
|
|
177
|
-
identifier = f"{event.serial_number}.{output_number:02X}"
|
|
178
|
-
accessory = self.accessory_registry.get(identifier)
|
|
179
|
-
|
|
180
|
-
if not accessory:
|
|
181
|
-
self.logger.warning(
|
|
182
|
-
f"Invalid accessory: {event.serial_number} (not found)"
|
|
183
|
-
)
|
|
184
|
-
elif not isinstance(accessory, DimmingLight):
|
|
185
|
-
self.logger.warning(
|
|
186
|
-
f"Invalid accessory: {event.serial_number} (not dimming light)"
|
|
187
|
-
)
|
|
188
|
-
else:
|
|
189
|
-
accessory.brightness = brightness
|
|
190
|
-
|
|
191
|
-
output_number += 1
|
|
192
|
-
|
|
193
|
-
self.logger.debug(
|
|
194
|
-
f"handle_light_level_received "
|
|
195
|
-
f"serial_number: {event.serial_number}, "
|
|
196
|
-
f"data_vale: {event.data_value}"
|
|
197
|
-
)
|
|
198
|
-
return event.data_value
|
|
199
|
-
|
|
200
|
-
def build_bridge(self) -> None:
|
|
201
|
-
"""Build the HomeKit bridge with all configured accessories."""
|
|
202
|
-
bridge_config = self.config.bridge
|
|
203
|
-
bridge = Bridge(self.driver, bridge_config.name)
|
|
204
|
-
bridge.set_info_service(
|
|
205
|
-
xp.__version__, xp.__manufacturer__, xp.__model__, xp.__serial__
|
|
206
|
-
)
|
|
207
|
-
|
|
208
|
-
for room in bridge_config.rooms:
|
|
209
|
-
self.add_room(bridge, room)
|
|
210
|
-
|
|
211
|
-
self.driver.add_accessory(accessory=bridge)
|
|
212
|
-
|
|
213
|
-
def add_room(self, bridge: Bridge, room: RoomConfig) -> None:
|
|
214
|
-
"""
|
|
215
|
-
Add a room with its accessories to the bridge.
|
|
216
|
-
|
|
217
|
-
Args:
|
|
218
|
-
bridge: HAP bridge instance.
|
|
219
|
-
room: Room configuration.
|
|
220
|
-
"""
|
|
221
|
-
temperature = TemperatureSensor(self.driver, room.name)
|
|
222
|
-
bridge.add_accessory(temperature)
|
|
223
|
-
|
|
224
|
-
for accessory_name in room.accessories:
|
|
225
|
-
homekit_accessory = self.get_accessory_by_name(accessory_name)
|
|
226
|
-
if homekit_accessory is None:
|
|
227
|
-
self.logger.warning("Accessory '{}' not found".format(accessory_name))
|
|
228
|
-
continue
|
|
229
|
-
|
|
230
|
-
accessory = self.get_accessory(homekit_accessory)
|
|
231
|
-
if accessory:
|
|
232
|
-
bridge.add_accessory(accessory)
|
|
233
|
-
# Add to accessory_registry
|
|
234
|
-
self.accessory_registry[accessory.identifier] = accessory
|
|
235
|
-
|
|
236
|
-
# Add to module_registry for event-driven lookup
|
|
237
|
-
module_key = (
|
|
238
|
-
accessory.module.module_type_code,
|
|
239
|
-
accessory.module.link_number,
|
|
240
|
-
)
|
|
241
|
-
if module_key not in self.module_registry:
|
|
242
|
-
self.module_registry[module_key] = []
|
|
243
|
-
self.module_registry[module_key].append(accessory)
|
|
244
|
-
|
|
245
|
-
def get_accessory(
|
|
246
|
-
self, homekit_accessory: HomekitAccessoryConfig
|
|
247
|
-
) -> Union[LightBulb, Outlet, DimmingLight, None]:
|
|
248
|
-
"""
|
|
249
|
-
Get an accessory instance from configuration.
|
|
250
|
-
|
|
251
|
-
Args:
|
|
252
|
-
homekit_accessory: HomeKit accessory configuration.
|
|
253
|
-
|
|
254
|
-
Returns:
|
|
255
|
-
Accessory instance or None if not found or invalid service type.
|
|
256
|
-
"""
|
|
257
|
-
module_config = self.modules.get_module_by_serial(
|
|
258
|
-
homekit_accessory.serial_number
|
|
259
|
-
)
|
|
260
|
-
if module_config is None:
|
|
261
|
-
self.logger.warning(f"Accessory '{homekit_accessory.name}' not found")
|
|
262
|
-
return None
|
|
263
|
-
|
|
264
|
-
if homekit_accessory.service == "lightbulb":
|
|
265
|
-
return LightBulb(
|
|
266
|
-
driver=self.driver,
|
|
267
|
-
module=module_config,
|
|
268
|
-
accessory=homekit_accessory,
|
|
269
|
-
event_bus=self.event_bus,
|
|
270
|
-
)
|
|
271
|
-
|
|
272
|
-
if homekit_accessory.service == "outlet":
|
|
273
|
-
return Outlet(
|
|
274
|
-
driver=self.driver,
|
|
275
|
-
module=module_config,
|
|
276
|
-
accessory=homekit_accessory,
|
|
277
|
-
event_bus=self.event_bus,
|
|
278
|
-
)
|
|
279
|
-
|
|
280
|
-
if homekit_accessory.service == "dimminglight":
|
|
281
|
-
return DimmingLight(
|
|
282
|
-
driver=self.driver,
|
|
283
|
-
module=module_config,
|
|
284
|
-
accessory=homekit_accessory,
|
|
285
|
-
event_bus=self.event_bus,
|
|
286
|
-
)
|
|
287
|
-
|
|
288
|
-
self.logger.warning(f"Accessory '{homekit_accessory.name}' not found")
|
|
289
|
-
return None
|
|
290
|
-
|
|
291
|
-
def get_accessory_by_name(self, name: str) -> Optional[HomekitAccessoryConfig]:
|
|
292
|
-
"""
|
|
293
|
-
Get an accessory configuration by name.
|
|
294
|
-
|
|
295
|
-
Args:
|
|
296
|
-
name: Name of the accessory to find.
|
|
297
|
-
|
|
298
|
-
Returns:
|
|
299
|
-
Accessory configuration if found, None otherwise.
|
|
300
|
-
"""
|
|
301
|
-
return next(
|
|
302
|
-
(module for module in self.config.accessories if module.name == name), None
|
|
303
|
-
)
|
|
304
|
-
|
|
305
|
-
def handle_module_state_changed(self, event: ModuleStateChangedEvent) -> None:
|
|
306
|
-
"""
|
|
307
|
-
Handle module state change by refreshing affected accessories.
|
|
308
|
-
|
|
309
|
-
Args:
|
|
310
|
-
event: Module state changed event.
|
|
311
|
-
"""
|
|
312
|
-
self.logger.debug(
|
|
313
|
-
f"Module state changed: module_type={event.module_type_code}, "
|
|
314
|
-
f"link={event.link_number}, input={event.input_number}"
|
|
315
|
-
)
|
|
316
|
-
|
|
317
|
-
# O(1) lookup using module_registry
|
|
318
|
-
module_key = (event.module_type_code, event.link_number)
|
|
319
|
-
affected_accessories = self.module_registry.get(module_key, [])
|
|
320
|
-
|
|
321
|
-
if not affected_accessories:
|
|
322
|
-
self.logger.debug(
|
|
323
|
-
f"No accessories found for module_type={event.module_type_code}, "
|
|
324
|
-
f"link={event.link_number}"
|
|
325
|
-
)
|
|
326
|
-
return
|
|
327
|
-
|
|
328
|
-
# Request cache refresh for each affected accessory
|
|
329
|
-
for accessory in affected_accessories:
|
|
330
|
-
self.logger.info(
|
|
331
|
-
f"Requesting cache refresh for accessory: {accessory.identifier}"
|
|
332
|
-
)
|
|
333
|
-
|
|
334
|
-
# Request OUTPUT_STATE refresh
|
|
335
|
-
self.event_bus.dispatch(
|
|
336
|
-
ReadDatapointEvent(
|
|
337
|
-
serial_number=accessory.module.serial_number,
|
|
338
|
-
datapoint_type=DataPointType.MODULE_OUTPUT_STATE,
|
|
339
|
-
refresh_cache=True,
|
|
340
|
-
)
|
|
341
|
-
)
|
|
342
|
-
|
|
343
|
-
# If dimming light, also refresh LIGHT_LEVEL
|
|
344
|
-
if isinstance(accessory, DimmingLight):
|
|
345
|
-
self.event_bus.dispatch(
|
|
346
|
-
ReadDatapointEvent(
|
|
347
|
-
serial_number=accessory.module.serial_number,
|
|
348
|
-
datapoint_type=DataPointType.MODULE_LIGHT_LEVEL,
|
|
349
|
-
refresh_cache=True,
|
|
350
|
-
)
|
|
351
|
-
)
|
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
HomeKit Light Bulb Accessory.
|
|
3
|
-
|
|
4
|
-
This module provides a light bulb accessory for HomeKit integration.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
import logging
|
|
8
|
-
|
|
9
|
-
from bubus import EventBus
|
|
10
|
-
from pyhap.accessory import Accessory
|
|
11
|
-
from pyhap.accessory_driver import AccessoryDriver
|
|
12
|
-
from pyhap.const import CATEGORY_LIGHTBULB
|
|
13
|
-
|
|
14
|
-
from xp.models.config.conson_module_config import ConsonModuleConfig
|
|
15
|
-
from xp.models.homekit.homekit_config import HomekitAccessoryConfig
|
|
16
|
-
from xp.models.protocol.conbus_protocol import (
|
|
17
|
-
LightBulbGetOnEvent,
|
|
18
|
-
LightBulbSetOnEvent,
|
|
19
|
-
)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
class LightBulb(Accessory):
|
|
23
|
-
"""
|
|
24
|
-
HomeKit light bulb accessory.
|
|
25
|
-
|
|
26
|
-
Attributes:
|
|
27
|
-
category: HomeKit category (CATEGORY_LIGHTBULB).
|
|
28
|
-
event_bus: Event bus for inter-service communication.
|
|
29
|
-
logger: Logger instance.
|
|
30
|
-
identifier: Unique identifier for the accessory.
|
|
31
|
-
accessory: Accessory configuration.
|
|
32
|
-
module: Module configuration.
|
|
33
|
-
is_on: Current on/off state.
|
|
34
|
-
char_on: On characteristic.
|
|
35
|
-
"""
|
|
36
|
-
|
|
37
|
-
category = CATEGORY_LIGHTBULB
|
|
38
|
-
event_bus: EventBus
|
|
39
|
-
|
|
40
|
-
def __init__(
|
|
41
|
-
self,
|
|
42
|
-
driver: AccessoryDriver,
|
|
43
|
-
module: ConsonModuleConfig,
|
|
44
|
-
accessory: HomekitAccessoryConfig,
|
|
45
|
-
event_bus: EventBus,
|
|
46
|
-
):
|
|
47
|
-
"""
|
|
48
|
-
Initialize the light bulb accessory.
|
|
49
|
-
|
|
50
|
-
Args:
|
|
51
|
-
driver: HAP accessory driver.
|
|
52
|
-
module: Module configuration.
|
|
53
|
-
accessory: Accessory configuration.
|
|
54
|
-
event_bus: Event bus for inter-service communication.
|
|
55
|
-
"""
|
|
56
|
-
super().__init__(driver, accessory.description)
|
|
57
|
-
|
|
58
|
-
self.logger = logging.getLogger(__name__)
|
|
59
|
-
|
|
60
|
-
identifier = f"{module.serial_number}.{accessory.output_number:02d}"
|
|
61
|
-
version = accessory.id
|
|
62
|
-
manufacturer = "Conson"
|
|
63
|
-
model = ("XP24_lightbulb",)
|
|
64
|
-
|
|
65
|
-
self.identifier = identifier
|
|
66
|
-
self.accessory = accessory
|
|
67
|
-
self.module = module
|
|
68
|
-
self.event_bus = event_bus
|
|
69
|
-
self.is_on = False
|
|
70
|
-
|
|
71
|
-
self.logger.info(
|
|
72
|
-
"Creating Lightbulb { serial_number : %s, output_number: %s }",
|
|
73
|
-
module.serial_number,
|
|
74
|
-
accessory.output_number,
|
|
75
|
-
)
|
|
76
|
-
|
|
77
|
-
serv_light = self.add_preload_service("Lightbulb")
|
|
78
|
-
|
|
79
|
-
self.set_info_service(version, manufacturer, model, identifier)
|
|
80
|
-
|
|
81
|
-
self.char_on = serv_light.configure_char(
|
|
82
|
-
"On", getter_callback=self.get_on, setter_callback=self.set_on
|
|
83
|
-
)
|
|
84
|
-
|
|
85
|
-
def set_on(self, value: bool) -> None:
|
|
86
|
-
"""
|
|
87
|
-
Set the on/off state of the light bulb.
|
|
88
|
-
|
|
89
|
-
Args:
|
|
90
|
-
value: True to turn on, False to turn off.
|
|
91
|
-
"""
|
|
92
|
-
# Emit set event
|
|
93
|
-
self.logger.debug(f"set_on {value}")
|
|
94
|
-
if self.is_on != value:
|
|
95
|
-
self.is_on = value
|
|
96
|
-
self.event_bus.dispatch(
|
|
97
|
-
LightBulbSetOnEvent(
|
|
98
|
-
serial_number=self.accessory.serial_number,
|
|
99
|
-
output_number=self.accessory.output_number,
|
|
100
|
-
module=self.module,
|
|
101
|
-
accessory=self.accessory,
|
|
102
|
-
value=value,
|
|
103
|
-
)
|
|
104
|
-
)
|
|
105
|
-
|
|
106
|
-
def get_on(self) -> bool:
|
|
107
|
-
"""
|
|
108
|
-
Get the on/off state of the light bulb.
|
|
109
|
-
|
|
110
|
-
Returns:
|
|
111
|
-
True if on, False if off.
|
|
112
|
-
"""
|
|
113
|
-
# Emit event and get response
|
|
114
|
-
self.logger.debug("get_on")
|
|
115
|
-
self.event_bus.dispatch(
|
|
116
|
-
LightBulbGetOnEvent(
|
|
117
|
-
serial_number=self.accessory.serial_number,
|
|
118
|
-
output_number=self.accessory.output_number,
|
|
119
|
-
module=self.module,
|
|
120
|
-
accessory=self.accessory,
|
|
121
|
-
)
|
|
122
|
-
)
|
|
123
|
-
self.logger.debug(f"get_on from dispatch: {self.is_on}")
|
|
124
|
-
|
|
125
|
-
return self.is_on
|