conson-xp 1.18.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.18.0.dist-info/METADATA +412 -0
- conson_xp-1.18.0.dist-info/RECORD +176 -0
- conson_xp-1.18.0.dist-info/WHEEL +4 -0
- conson_xp-1.18.0.dist-info/entry_points.txt +5 -0
- conson_xp-1.18.0.dist-info/licenses/LICENSE +29 -0
- xp/__init__.py +9 -0
- xp/cli/__init__.py +5 -0
- xp/cli/__main__.py +6 -0
- xp/cli/commands/__init__.py +153 -0
- xp/cli/commands/conbus/__init__.py +25 -0
- xp/cli/commands/conbus/conbus.py +128 -0
- xp/cli/commands/conbus/conbus_actiontable_commands.py +233 -0
- xp/cli/commands/conbus/conbus_autoreport_commands.py +108 -0
- xp/cli/commands/conbus/conbus_blink_commands.py +163 -0
- xp/cli/commands/conbus/conbus_config_commands.py +29 -0
- xp/cli/commands/conbus/conbus_custom_commands.py +57 -0
- xp/cli/commands/conbus/conbus_datapoint_commands.py +113 -0
- xp/cli/commands/conbus/conbus_discover_commands.py +61 -0
- xp/cli/commands/conbus/conbus_event_commands.py +81 -0
- xp/cli/commands/conbus/conbus_lightlevel_commands.py +207 -0
- xp/cli/commands/conbus/conbus_linknumber_commands.py +102 -0
- xp/cli/commands/conbus/conbus_modulenumber_commands.py +104 -0
- xp/cli/commands/conbus/conbus_msactiontable_commands.py +94 -0
- xp/cli/commands/conbus/conbus_output_commands.py +163 -0
- xp/cli/commands/conbus/conbus_raw_commands.py +62 -0
- xp/cli/commands/conbus/conbus_receive_commands.py +59 -0
- xp/cli/commands/conbus/conbus_scan_commands.py +58 -0
- xp/cli/commands/file_commands.py +186 -0
- xp/cli/commands/homekit/__init__.py +3 -0
- xp/cli/commands/homekit/homekit.py +118 -0
- xp/cli/commands/homekit/homekit_start_commands.py +43 -0
- xp/cli/commands/module_commands.py +187 -0
- xp/cli/commands/reverse_proxy_commands.py +178 -0
- xp/cli/commands/server/__init__.py +3 -0
- xp/cli/commands/server/server_commands.py +135 -0
- xp/cli/commands/telegram/__init__.py +5 -0
- xp/cli/commands/telegram/telegram.py +41 -0
- xp/cli/commands/telegram/telegram_blink_commands.py +79 -0
- xp/cli/commands/telegram/telegram_checksum_commands.py +112 -0
- xp/cli/commands/telegram/telegram_discover_commands.py +41 -0
- xp/cli/commands/telegram/telegram_linknumber_commands.py +86 -0
- xp/cli/commands/telegram/telegram_parse_commands.py +75 -0
- xp/cli/commands/telegram/telegram_version_commands.py +52 -0
- xp/cli/main.py +87 -0
- xp/cli/utils/__init__.py +1 -0
- xp/cli/utils/click_tree.py +57 -0
- xp/cli/utils/datapoint_type_choice.py +57 -0
- xp/cli/utils/decorators.py +351 -0
- xp/cli/utils/error_handlers.py +201 -0
- xp/cli/utils/formatters.py +312 -0
- xp/cli/utils/module_type_choice.py +56 -0
- xp/cli/utils/serial_number_type.py +52 -0
- xp/cli/utils/system_function_choice.py +57 -0
- xp/cli/utils/xp_module_type.py +53 -0
- xp/connection/__init__.py +13 -0
- xp/connection/exceptions.py +22 -0
- xp/models/__init__.py +36 -0
- xp/models/actiontable/__init__.py +1 -0
- xp/models/actiontable/actiontable.py +43 -0
- xp/models/actiontable/msactiontable_xp20.py +53 -0
- xp/models/actiontable/msactiontable_xp24.py +58 -0
- xp/models/actiontable/msactiontable_xp33.py +65 -0
- xp/models/conbus/__init__.py +1 -0
- xp/models/conbus/conbus.py +87 -0
- xp/models/conbus/conbus_autoreport.py +67 -0
- xp/models/conbus/conbus_blink.py +80 -0
- xp/models/conbus/conbus_client_config.py +55 -0
- xp/models/conbus/conbus_connection_status.py +40 -0
- xp/models/conbus/conbus_custom.py +58 -0
- xp/models/conbus/conbus_datapoint.py +89 -0
- xp/models/conbus/conbus_discover.py +64 -0
- xp/models/conbus/conbus_event_raw.py +47 -0
- xp/models/conbus/conbus_lightlevel.py +52 -0
- xp/models/conbus/conbus_linknumber.py +54 -0
- xp/models/conbus/conbus_output.py +57 -0
- xp/models/conbus/conbus_raw.py +45 -0
- xp/models/conbus/conbus_receive.py +42 -0
- xp/models/conbus/conbus_writeconfig.py +60 -0
- xp/models/homekit/__init__.py +1 -0
- xp/models/homekit/homekit_accessory.py +35 -0
- xp/models/homekit/homekit_config.py +106 -0
- xp/models/homekit/homekit_conson_config.py +86 -0
- xp/models/log_entry.py +130 -0
- xp/models/protocol/__init__.py +1 -0
- xp/models/protocol/conbus_protocol.py +312 -0
- xp/models/response.py +42 -0
- xp/models/telegram/__init__.py +1 -0
- xp/models/telegram/action_type.py +31 -0
- xp/models/telegram/datapoint_type.py +82 -0
- xp/models/telegram/event_telegram.py +140 -0
- xp/models/telegram/event_type.py +15 -0
- xp/models/telegram/input_action_type.py +69 -0
- xp/models/telegram/input_type.py +17 -0
- xp/models/telegram/module_type.py +188 -0
- xp/models/telegram/module_type_code.py +205 -0
- xp/models/telegram/output_telegram.py +103 -0
- xp/models/telegram/reply_telegram.py +297 -0
- xp/models/telegram/system_function.py +116 -0
- xp/models/telegram/system_telegram.py +94 -0
- xp/models/telegram/telegram.py +28 -0
- xp/models/telegram/telegram_type.py +19 -0
- xp/models/telegram/timeparam_type.py +51 -0
- xp/models/write_config_type.py +33 -0
- xp/services/__init__.py +26 -0
- xp/services/actiontable/__init__.py +1 -0
- xp/services/actiontable/actiontable_serializer.py +273 -0
- xp/services/actiontable/msactiontable_serializer.py +7 -0
- xp/services/actiontable/msactiontable_xp20_serializer.py +169 -0
- xp/services/actiontable/msactiontable_xp24_serializer.py +120 -0
- xp/services/actiontable/msactiontable_xp33_serializer.py +239 -0
- xp/services/conbus/__init__.py +1 -0
- xp/services/conbus/actiontable/__init__.py +1 -0
- xp/services/conbus/actiontable/actiontable_download_service.py +158 -0
- xp/services/conbus/actiontable/actiontable_list_service.py +91 -0
- xp/services/conbus/actiontable/actiontable_show_service.py +89 -0
- xp/services/conbus/actiontable/actiontable_upload_service.py +211 -0
- xp/services/conbus/actiontable/msactiontable_service.py +232 -0
- xp/services/conbus/conbus_blink_all_service.py +181 -0
- xp/services/conbus/conbus_blink_service.py +158 -0
- xp/services/conbus/conbus_custom_service.py +156 -0
- xp/services/conbus/conbus_datapoint_queryall_service.py +182 -0
- xp/services/conbus/conbus_datapoint_service.py +170 -0
- xp/services/conbus/conbus_discover_service.py +312 -0
- xp/services/conbus/conbus_event_raw_service.py +181 -0
- xp/services/conbus/conbus_output_service.py +194 -0
- xp/services/conbus/conbus_raw_service.py +122 -0
- xp/services/conbus/conbus_receive_service.py +115 -0
- xp/services/conbus/conbus_scan_service.py +150 -0
- xp/services/conbus/write_config_service.py +194 -0
- xp/services/homekit/__init__.py +1 -0
- xp/services/homekit/homekit_cache_service.py +307 -0
- xp/services/homekit/homekit_conbus_service.py +93 -0
- xp/services/homekit/homekit_config_validator.py +310 -0
- xp/services/homekit/homekit_conson_validator.py +121 -0
- xp/services/homekit/homekit_dimminglight.py +182 -0
- xp/services/homekit/homekit_dimminglight_service.py +148 -0
- xp/services/homekit/homekit_hap_service.py +342 -0
- xp/services/homekit/homekit_lightbulb.py +120 -0
- xp/services/homekit/homekit_lightbulb_service.py +86 -0
- xp/services/homekit/homekit_module_service.py +56 -0
- xp/services/homekit/homekit_outlet.py +168 -0
- xp/services/homekit/homekit_outlet_service.py +121 -0
- xp/services/homekit/homekit_service.py +359 -0
- xp/services/log_file_service.py +309 -0
- xp/services/module_type_service.py +257 -0
- xp/services/protocol/__init__.py +21 -0
- xp/services/protocol/conbus_event_protocol.py +360 -0
- xp/services/protocol/conbus_protocol.py +318 -0
- xp/services/protocol/protocol_factory.py +78 -0
- xp/services/protocol/telegram_protocol.py +264 -0
- xp/services/reverse_proxy_service.py +435 -0
- xp/services/server/__init__.py +1 -0
- xp/services/server/base_server_service.py +366 -0
- xp/services/server/cp20_server_service.py +65 -0
- xp/services/server/device_service_factory.py +94 -0
- xp/services/server/server_service.py +428 -0
- xp/services/server/xp130_server_service.py +67 -0
- xp/services/server/xp20_server_service.py +92 -0
- xp/services/server/xp230_server_service.py +58 -0
- xp/services/server/xp24_server_service.py +245 -0
- xp/services/server/xp33_server_service.py +535 -0
- xp/services/telegram/__init__.py +1 -0
- xp/services/telegram/telegram_blink_service.py +138 -0
- xp/services/telegram/telegram_checksum_service.py +149 -0
- xp/services/telegram/telegram_datapoint_service.py +82 -0
- xp/services/telegram/telegram_discover_service.py +277 -0
- xp/services/telegram/telegram_link_number_service.py +216 -0
- xp/services/telegram/telegram_output_service.py +322 -0
- xp/services/telegram/telegram_service.py +380 -0
- xp/services/telegram/telegram_version_service.py +288 -0
- xp/utils/__init__.py +12 -0
- xp/utils/checksum.py +61 -0
- xp/utils/dependencies.py +531 -0
- xp/utils/event_helper.py +31 -0
- xp/utils/serialization.py +205 -0
- xp/utils/time_utils.py +134 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""HomeKit Module Service.
|
|
2
|
+
|
|
3
|
+
This module provides service implementation for HomeKit module management.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
from xp.models.homekit.homekit_conson_config import (
|
|
10
|
+
ConsonModuleConfig,
|
|
11
|
+
ConsonModuleListConfig,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class HomekitModuleService:
|
|
16
|
+
"""Service for managing HomeKit module configurations.
|
|
17
|
+
|
|
18
|
+
Attributes:
|
|
19
|
+
logger: Logger instance.
|
|
20
|
+
conson_modules_config: Conson module list configuration.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
conson_modules_config: ConsonModuleListConfig,
|
|
26
|
+
):
|
|
27
|
+
"""Initialize the HomeKit module service.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
conson_modules_config: Conson module list configuration.
|
|
31
|
+
"""
|
|
32
|
+
# Set up logging
|
|
33
|
+
self.logger = logging.getLogger(__name__)
|
|
34
|
+
self.conson_modules_config = conson_modules_config
|
|
35
|
+
|
|
36
|
+
def get_module_by_serial(self, serial_number: str) -> Optional[ConsonModuleConfig]:
|
|
37
|
+
"""Get a module by its serial number.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
serial_number: Serial number of the module to find.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
Module configuration if found, None otherwise.
|
|
44
|
+
"""
|
|
45
|
+
module = next(
|
|
46
|
+
(
|
|
47
|
+
module
|
|
48
|
+
for module in self.conson_modules_config.root
|
|
49
|
+
if module.serial_number == serial_number
|
|
50
|
+
),
|
|
51
|
+
None,
|
|
52
|
+
)
|
|
53
|
+
self.logger.debug(
|
|
54
|
+
f"Module search by serial '{serial_number}': {'found' if module else 'not found'}"
|
|
55
|
+
)
|
|
56
|
+
return module
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""HomeKit Outlet Accessory.
|
|
2
|
+
|
|
3
|
+
This module provides an outlet accessory for HomeKit integration.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
|
|
8
|
+
from bubus import EventBus
|
|
9
|
+
from pyhap.accessory import Accessory
|
|
10
|
+
from pyhap.accessory_driver import AccessoryDriver
|
|
11
|
+
from pyhap.const import CATEGORY_OUTLET
|
|
12
|
+
|
|
13
|
+
from xp.models.homekit.homekit_config import HomekitAccessoryConfig
|
|
14
|
+
from xp.models.homekit.homekit_conson_config import ConsonModuleConfig
|
|
15
|
+
from xp.models.protocol.conbus_protocol import (
|
|
16
|
+
OutletGetInUseEvent,
|
|
17
|
+
OutletGetOnEvent,
|
|
18
|
+
OutletSetInUseEvent,
|
|
19
|
+
OutletSetOnEvent,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Outlet(Accessory):
|
|
24
|
+
"""HomeKit outlet accessory.
|
|
25
|
+
|
|
26
|
+
Attributes:
|
|
27
|
+
category: HomeKit category (CATEGORY_OUTLET).
|
|
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
|
+
is_in_use: Current in-use state.
|
|
35
|
+
char_on: On characteristic.
|
|
36
|
+
char_outlet_in_use: Outlet in-use characteristic.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
category = CATEGORY_OUTLET
|
|
40
|
+
event_bus: EventBus
|
|
41
|
+
|
|
42
|
+
def __init__(
|
|
43
|
+
self,
|
|
44
|
+
driver: AccessoryDriver,
|
|
45
|
+
module: ConsonModuleConfig,
|
|
46
|
+
accessory: HomekitAccessoryConfig,
|
|
47
|
+
event_bus: EventBus,
|
|
48
|
+
):
|
|
49
|
+
"""Initialize the outlet accessory.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
driver: HAP accessory driver.
|
|
53
|
+
module: Module configuration.
|
|
54
|
+
accessory: Accessory configuration.
|
|
55
|
+
event_bus: Event bus for inter-service communication.
|
|
56
|
+
"""
|
|
57
|
+
super().__init__(driver=driver, display_name=accessory.description)
|
|
58
|
+
|
|
59
|
+
self.logger = logging.getLogger(__name__)
|
|
60
|
+
|
|
61
|
+
identifier = f"{module.serial_number}.{accessory.output_number:02d}"
|
|
62
|
+
version = accessory.id
|
|
63
|
+
manufacturer = "Conson"
|
|
64
|
+
model = ("XP24_outlet",)
|
|
65
|
+
|
|
66
|
+
self.identifier = identifier
|
|
67
|
+
self.accessory = accessory
|
|
68
|
+
self.module = module
|
|
69
|
+
|
|
70
|
+
self.event_bus = event_bus
|
|
71
|
+
self.logger.info(
|
|
72
|
+
"Creating Outlet { serial_number : %s, output_number: %s }",
|
|
73
|
+
module.serial_number,
|
|
74
|
+
accessory.output_number,
|
|
75
|
+
)
|
|
76
|
+
self.is_on = False
|
|
77
|
+
self.is_in_use = False
|
|
78
|
+
|
|
79
|
+
serv_outlet = self.add_preload_service("Outlet")
|
|
80
|
+
self.set_info_service(version, manufacturer, model, identifier)
|
|
81
|
+
self.char_on = serv_outlet.configure_char(
|
|
82
|
+
"On", setter_callback=self.set_on, getter_callback=self.get_on
|
|
83
|
+
)
|
|
84
|
+
self.char_outlet_in_use = serv_outlet.configure_char(
|
|
85
|
+
"OutletInUse",
|
|
86
|
+
setter_callback=self.set_outlet_in_use,
|
|
87
|
+
getter_callback=self.get_outlet_in_use,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
def set_outlet_in_use(self, value: bool) -> None:
|
|
91
|
+
"""Set the in-use state of the outlet.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
value: True if in use, False otherwise.
|
|
95
|
+
"""
|
|
96
|
+
self.logger.debug(f"set_outlet_in_use {value}")
|
|
97
|
+
|
|
98
|
+
self.is_in_use = value
|
|
99
|
+
self.event_bus.dispatch(
|
|
100
|
+
OutletSetInUseEvent(
|
|
101
|
+
serial_number=self.accessory.serial_number,
|
|
102
|
+
output_number=self.accessory.output_number,
|
|
103
|
+
module=self.module,
|
|
104
|
+
accessory=self.accessory,
|
|
105
|
+
value=value,
|
|
106
|
+
)
|
|
107
|
+
)
|
|
108
|
+
self.logger.debug(f"set_outlet_in_use {value} end")
|
|
109
|
+
|
|
110
|
+
def get_outlet_in_use(self) -> bool:
|
|
111
|
+
"""Get the in-use state of the outlet.
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
True if in use, False otherwise.
|
|
115
|
+
"""
|
|
116
|
+
# Emit event and get response
|
|
117
|
+
self.logger.debug("get_outlet_in_use")
|
|
118
|
+
|
|
119
|
+
# Dispatch event from HAP thread (thread-safe)
|
|
120
|
+
self.event_bus.dispatch(
|
|
121
|
+
OutletGetInUseEvent(
|
|
122
|
+
serial_number=self.accessory.serial_number,
|
|
123
|
+
output_number=self.accessory.output_number,
|
|
124
|
+
module=self.module,
|
|
125
|
+
accessory=self.accessory,
|
|
126
|
+
)
|
|
127
|
+
)
|
|
128
|
+
return self.is_in_use
|
|
129
|
+
|
|
130
|
+
def set_on(self, value: bool) -> None:
|
|
131
|
+
"""Set the on/off state of the outlet.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
value: True to turn on, False to turn off.
|
|
135
|
+
"""
|
|
136
|
+
# Emit set event
|
|
137
|
+
self.logger.debug(f"set_on {value} {self.is_on}")
|
|
138
|
+
|
|
139
|
+
self.is_on = value
|
|
140
|
+
self.event_bus.dispatch(
|
|
141
|
+
OutletSetOnEvent(
|
|
142
|
+
serial_number=self.accessory.serial_number,
|
|
143
|
+
output_number=self.accessory.output_number,
|
|
144
|
+
module=self.module,
|
|
145
|
+
accessory=self.accessory,
|
|
146
|
+
value=value,
|
|
147
|
+
)
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
def get_on(self) -> bool:
|
|
151
|
+
"""Get the on/off state of the outlet.
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
True if on, False if off.
|
|
155
|
+
"""
|
|
156
|
+
# Emit event and get response
|
|
157
|
+
self.logger.debug("get_on")
|
|
158
|
+
|
|
159
|
+
# Dispatch event from HAP thread (thread-safe)
|
|
160
|
+
self.event_bus.dispatch(
|
|
161
|
+
OutletGetOnEvent(
|
|
162
|
+
serial_number=self.accessory.serial_number,
|
|
163
|
+
output_number=self.accessory.output_number,
|
|
164
|
+
module=self.module,
|
|
165
|
+
accessory=self.accessory,
|
|
166
|
+
)
|
|
167
|
+
)
|
|
168
|
+
return self.is_on
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""HomeKit Outlet Service.
|
|
2
|
+
|
|
3
|
+
This module provides service implementation for outlet accessories.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
|
|
8
|
+
from bubus import EventBus
|
|
9
|
+
|
|
10
|
+
from xp.models.protocol.conbus_protocol import (
|
|
11
|
+
OutletGetInUseEvent,
|
|
12
|
+
OutletGetOnEvent,
|
|
13
|
+
OutletSetOnEvent,
|
|
14
|
+
ReadDatapointEvent,
|
|
15
|
+
SendActionEvent,
|
|
16
|
+
)
|
|
17
|
+
from xp.models.telegram.datapoint_type import DataPointType
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class HomeKitOutletService:
|
|
21
|
+
"""Outlet service for HomeKit.
|
|
22
|
+
|
|
23
|
+
Attributes:
|
|
24
|
+
event_bus: Event bus for inter-service communication.
|
|
25
|
+
logger: Logger instance.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
event_bus: EventBus
|
|
29
|
+
|
|
30
|
+
def __init__(self, event_bus: EventBus):
|
|
31
|
+
"""Initialize the outlet service.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
event_bus: Event bus instance.
|
|
35
|
+
"""
|
|
36
|
+
self.event_bus = event_bus
|
|
37
|
+
self.logger = logging.getLogger(__name__)
|
|
38
|
+
|
|
39
|
+
# Register event handlers
|
|
40
|
+
self.event_bus.on(OutletGetOnEvent, self.handle_outlet_get_on)
|
|
41
|
+
self.event_bus.on(OutletSetOnEvent, self.handle_outlet_set_on)
|
|
42
|
+
self.event_bus.on(OutletGetInUseEvent, self.handle_outlet_get_in_use)
|
|
43
|
+
|
|
44
|
+
def handle_outlet_get_on(self, event: OutletGetOnEvent) -> bool:
|
|
45
|
+
"""Handle outlet get on event.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
event: Outlet get on event.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
True if request was dispatched successfully.
|
|
52
|
+
"""
|
|
53
|
+
self.logger.debug(
|
|
54
|
+
f"Getting outlet state for serial {event.serial_number}, output {event.output_number}"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
datapoint_type = DataPointType.MODULE_OUTPUT_STATE
|
|
58
|
+
read_datapoint = ReadDatapointEvent(
|
|
59
|
+
serial_number=event.serial_number, datapoint_type=datapoint_type
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
self.logger.debug(f"Dispatching ReadDatapointEvent for {event.serial_number}")
|
|
63
|
+
self.event_bus.dispatch(read_datapoint)
|
|
64
|
+
self.logger.debug(f"Dispatched ReadDatapointEvent for {event.serial_number}")
|
|
65
|
+
return True
|
|
66
|
+
|
|
67
|
+
def handle_outlet_set_on(self, event: OutletSetOnEvent) -> bool:
|
|
68
|
+
"""Handle outlet set on event.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
event: Outlet set on event.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
True if command was sent successfully.
|
|
75
|
+
"""
|
|
76
|
+
self.logger.info(
|
|
77
|
+
f"Setting outlet "
|
|
78
|
+
f"for serial {event.serial_number}, "
|
|
79
|
+
f"output {event.output_number} "
|
|
80
|
+
f"to {'ON' if event.value else 'OFF'}"
|
|
81
|
+
)
|
|
82
|
+
self.logger.debug(f"outlet_set_on {event}")
|
|
83
|
+
|
|
84
|
+
send_action = SendActionEvent(
|
|
85
|
+
serial_number=event.serial_number,
|
|
86
|
+
output_number=event.output_number,
|
|
87
|
+
value=event.value,
|
|
88
|
+
on_action=event.accessory.on_action,
|
|
89
|
+
off_action=event.accessory.off_action,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
self.logger.debug(f"Dispatching SendActionEvent for {event.serial_number}")
|
|
93
|
+
self.event_bus.dispatch(send_action)
|
|
94
|
+
self.logger.info(
|
|
95
|
+
f"Outlet set command sent successfully for {event.serial_number}"
|
|
96
|
+
)
|
|
97
|
+
return True
|
|
98
|
+
|
|
99
|
+
def handle_outlet_get_in_use(self, event: OutletGetInUseEvent) -> bool:
|
|
100
|
+
"""Handle outlet get in-use event.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
event: Outlet get in-use event.
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
True if request was dispatched successfully.
|
|
107
|
+
"""
|
|
108
|
+
self.logger.info(
|
|
109
|
+
f"Getting outlet in-use status for serial {event.serial_number}"
|
|
110
|
+
)
|
|
111
|
+
self.logger.debug(f"outlet_get_in_use {event}")
|
|
112
|
+
|
|
113
|
+
datapoint_type = DataPointType.MODULE_STATE
|
|
114
|
+
read_datapoint = ReadDatapointEvent(
|
|
115
|
+
serial_number=event.serial_number, datapoint_type=datapoint_type
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
self.logger.debug(f"Dispatching ReadDatapointEvent for {event.serial_number}")
|
|
119
|
+
self.event_bus.dispatch(read_datapoint)
|
|
120
|
+
self.logger.debug("Dispatching ReadDatapointEvent (timeout: 2s)")
|
|
121
|
+
return True
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
"""HomeKit Service for Apple HomeKit integration.
|
|
2
|
+
|
|
3
|
+
This module provides the main service for HomeKit integration.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
# Install asyncio reactor before importing reactor
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import logging
|
|
10
|
+
import threading
|
|
11
|
+
|
|
12
|
+
from bubus import EventBus
|
|
13
|
+
from twisted.internet.posixbase import PosixReactorBase
|
|
14
|
+
|
|
15
|
+
from xp.models import ConbusClientConfig
|
|
16
|
+
from xp.models.protocol.conbus_protocol import (
|
|
17
|
+
ConnectionFailedEvent,
|
|
18
|
+
ConnectionLostEvent,
|
|
19
|
+
ConnectionMadeEvent,
|
|
20
|
+
LightLevelReceivedEvent,
|
|
21
|
+
ModuleDiscoveredEvent,
|
|
22
|
+
ModuleStateChangedEvent,
|
|
23
|
+
OutputStateReceivedEvent,
|
|
24
|
+
TelegramReceivedEvent,
|
|
25
|
+
)
|
|
26
|
+
from xp.services import TelegramService
|
|
27
|
+
from xp.services.homekit.homekit_cache_service import HomeKitCacheService
|
|
28
|
+
from xp.services.homekit.homekit_conbus_service import HomeKitConbusService
|
|
29
|
+
from xp.services.homekit.homekit_dimminglight_service import HomeKitDimmingLightService
|
|
30
|
+
from xp.services.homekit.homekit_hap_service import HomekitHapService
|
|
31
|
+
from xp.services.homekit.homekit_lightbulb_service import HomeKitLightbulbService
|
|
32
|
+
from xp.services.homekit.homekit_outlet_service import HomeKitOutletService
|
|
33
|
+
from xp.services.protocol.protocol_factory import TelegramFactory
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class HomeKitService:
|
|
37
|
+
"""Main HomeKit service for Apple HomeKit integration.
|
|
38
|
+
|
|
39
|
+
Attributes:
|
|
40
|
+
cli_config: Conbus client configuration.
|
|
41
|
+
reactor: Twisted reactor instance.
|
|
42
|
+
telegram_factory: Telegram factory for protocol.
|
|
43
|
+
protocol: Telegram protocol instance.
|
|
44
|
+
event_bus: Event bus for inter-service communication.
|
|
45
|
+
lightbulb_service: Lightbulb service instance.
|
|
46
|
+
dimminglight_service: Dimming light service instance.
|
|
47
|
+
outlet_service: Outlet service instance.
|
|
48
|
+
cache_service: Cache service instance.
|
|
49
|
+
conbus_service: Conbus service instance.
|
|
50
|
+
module_factory: HAP service instance.
|
|
51
|
+
telegram_service: Telegram service instance.
|
|
52
|
+
logger: Logger instance.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def __init__(
|
|
56
|
+
self,
|
|
57
|
+
cli_config: ConbusClientConfig,
|
|
58
|
+
event_bus: EventBus,
|
|
59
|
+
telegram_factory: TelegramFactory,
|
|
60
|
+
reactor: PosixReactorBase,
|
|
61
|
+
lightbulb_service: HomeKitLightbulbService,
|
|
62
|
+
outlet_service: HomeKitOutletService,
|
|
63
|
+
dimminglight_service: HomeKitDimmingLightService,
|
|
64
|
+
cache_service: HomeKitCacheService,
|
|
65
|
+
conbus_service: HomeKitConbusService,
|
|
66
|
+
module_factory: HomekitHapService,
|
|
67
|
+
telegram_service: TelegramService,
|
|
68
|
+
):
|
|
69
|
+
"""Initialize the HomeKit service.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
cli_config: Conbus client configuration.
|
|
73
|
+
event_bus: Event bus instance.
|
|
74
|
+
telegram_factory: Telegram factory instance.
|
|
75
|
+
reactor: Twisted reactor instance.
|
|
76
|
+
lightbulb_service: Lightbulb service instance.
|
|
77
|
+
outlet_service: Outlet service instance.
|
|
78
|
+
dimminglight_service: Dimming light service instance.
|
|
79
|
+
cache_service: Cache service instance.
|
|
80
|
+
conbus_service: Conbus service instance.
|
|
81
|
+
module_factory: HAP service instance.
|
|
82
|
+
telegram_service: Telegram service instance.
|
|
83
|
+
"""
|
|
84
|
+
self.cli_config = cli_config.conbus
|
|
85
|
+
self.reactor = reactor
|
|
86
|
+
self.telegram_factory = telegram_factory
|
|
87
|
+
self.protocol = telegram_factory.telegram_protocol
|
|
88
|
+
self.event_bus = event_bus
|
|
89
|
+
self.lightbulb_service = lightbulb_service
|
|
90
|
+
self.dimminglight_service = dimminglight_service
|
|
91
|
+
self.outlet_service = outlet_service
|
|
92
|
+
self.cache_service = cache_service
|
|
93
|
+
self.conbus_service = conbus_service
|
|
94
|
+
self.module_factory = module_factory
|
|
95
|
+
self.telegram_service = telegram_service
|
|
96
|
+
self.logger = logging.getLogger(__name__)
|
|
97
|
+
|
|
98
|
+
# Register event handlers
|
|
99
|
+
self.event_bus.on(ConnectionMadeEvent, self.handle_connection_made)
|
|
100
|
+
self.event_bus.on(ConnectionFailedEvent, self.handle_connection_failed)
|
|
101
|
+
self.event_bus.on(ConnectionLostEvent, self.handle_connection_lost)
|
|
102
|
+
self.event_bus.on(TelegramReceivedEvent, self.handle_telegram_received)
|
|
103
|
+
self.event_bus.on(ModuleDiscoveredEvent, self.handle_module_discovered)
|
|
104
|
+
|
|
105
|
+
def start(self) -> None:
|
|
106
|
+
"""Start the HomeKit service."""
|
|
107
|
+
self.logger.info("Starting HomeKit service.")
|
|
108
|
+
self.logger.debug("start")
|
|
109
|
+
|
|
110
|
+
# Run reactor in its own dedicated thread
|
|
111
|
+
self.logger.info("Starting reactor in dedicated thread.")
|
|
112
|
+
reactor_thread = threading.Thread(
|
|
113
|
+
target=self._run_reactor_in_thread, daemon=True, name="ReactorThread"
|
|
114
|
+
)
|
|
115
|
+
reactor_thread.start()
|
|
116
|
+
|
|
117
|
+
# Keep MainThread alive while reactor thread runs
|
|
118
|
+
self.logger.info("Reactor thread started, MainThread waiting.")
|
|
119
|
+
reactor_thread.join()
|
|
120
|
+
|
|
121
|
+
def _run_reactor_in_thread(self) -> None:
|
|
122
|
+
"""Run reactor in dedicated thread with its own event loop."""
|
|
123
|
+
self.logger.info("Reactor thread starting.")
|
|
124
|
+
|
|
125
|
+
# The asyncio reactor already has an event loop set up
|
|
126
|
+
# We just need to use it
|
|
127
|
+
|
|
128
|
+
# Connect to TCP server
|
|
129
|
+
self.logger.info(
|
|
130
|
+
f"Connecting to TCP server {self.cli_config.ip}:{self.cli_config.port}"
|
|
131
|
+
)
|
|
132
|
+
self.reactor.connectTCP(
|
|
133
|
+
self.cli_config.ip, self.cli_config.port, self.telegram_factory
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
# Schedule module factory to start after reactor is running
|
|
137
|
+
# Use callLater(0) to ensure event loop is actually running
|
|
138
|
+
self.reactor.callLater(0, self._start_module_factory)
|
|
139
|
+
|
|
140
|
+
# Run the reactor (which now uses asyncio underneath)
|
|
141
|
+
self.logger.info("Starting reactor event loop.")
|
|
142
|
+
self.reactor.run()
|
|
143
|
+
|
|
144
|
+
def _start_module_factory(self) -> None:
|
|
145
|
+
"""Start module factory after reactor starts.
|
|
146
|
+
|
|
147
|
+
Creates and schedules an async task to start the HAP service.
|
|
148
|
+
"""
|
|
149
|
+
self.logger.info("Starting module factory.")
|
|
150
|
+
self.logger.debug("callWhenRunning executed, scheduling async task")
|
|
151
|
+
|
|
152
|
+
# Run HAP-python driver asynchronously in the reactor's event loop
|
|
153
|
+
async def async_start() -> None:
|
|
154
|
+
"""Start the HAP service asynchronously."""
|
|
155
|
+
self.logger.info("async_start executing.")
|
|
156
|
+
try:
|
|
157
|
+
await self.module_factory.async_start()
|
|
158
|
+
self.logger.info("Module factory started successfully")
|
|
159
|
+
except Exception as e:
|
|
160
|
+
self.logger.error(f"Error starting module factory: {e}", exc_info=True)
|
|
161
|
+
|
|
162
|
+
# Schedule on reactor's event loop (which is asyncio)
|
|
163
|
+
try:
|
|
164
|
+
task = asyncio.create_task(async_start())
|
|
165
|
+
self.logger.debug(f"Created module factory task: {task}")
|
|
166
|
+
task.add_done_callback(
|
|
167
|
+
lambda t: self.logger.debug(f"Module factory task completed: {t}")
|
|
168
|
+
)
|
|
169
|
+
except Exception as e:
|
|
170
|
+
self.logger.error(f"Error creating async task: {e}", exc_info=True)
|
|
171
|
+
|
|
172
|
+
# Event handlers
|
|
173
|
+
def handle_connection_made(self, event: ConnectionMadeEvent) -> None:
|
|
174
|
+
"""Handle connection established - send initial telegram.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
event: Connection made event.
|
|
178
|
+
"""
|
|
179
|
+
self.logger.debug("Connection established successfully")
|
|
180
|
+
self.logger.debug("Sending initial discovery telegram: S0000000000F01D00")
|
|
181
|
+
event.protocol.sendFrame(b"S0000000000F01D00")
|
|
182
|
+
|
|
183
|
+
def handle_connection_failed(self, event: ConnectionFailedEvent) -> None:
|
|
184
|
+
"""Handle connection failed.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
event: Connection failed event.
|
|
188
|
+
"""
|
|
189
|
+
self.logger.error(f"Connection failed: {event.reason}")
|
|
190
|
+
|
|
191
|
+
def handle_connection_lost(self, event: ConnectionLostEvent) -> None:
|
|
192
|
+
"""Handle connection lost.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
event: Connection lost event.
|
|
196
|
+
"""
|
|
197
|
+
self.logger.warning(
|
|
198
|
+
f"Connection lost: {event.reason if hasattr(event, 'reason') else 'Unknown reason'}"
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
def handle_telegram_received(self, event: TelegramReceivedEvent) -> str:
|
|
202
|
+
"""Handle received telegram events.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
event: Telegram received event.
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
Frame data from the event.
|
|
209
|
+
"""
|
|
210
|
+
self.logger.debug(
|
|
211
|
+
f"handle_telegram_received ENTERED with telegram: {event.telegram}"
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
# Check if telegram is Reply (R) with Discover function (F01D)
|
|
215
|
+
if event.telegram_type in ("E"):
|
|
216
|
+
self.dispatch_event_telegram_received_event(event)
|
|
217
|
+
return event.frame
|
|
218
|
+
|
|
219
|
+
# Check if telegram is Reply (R) with Discover function (F01D)
|
|
220
|
+
if event.telegram_type in ("R") and "F01D" in event.telegram:
|
|
221
|
+
self.dispatch_module_discovered_event(event)
|
|
222
|
+
return event.frame
|
|
223
|
+
|
|
224
|
+
# Check if telegram is Reply (R) with Read Datapoint (F02) OUTPUT_STATE (D12)
|
|
225
|
+
if event.telegram_type in ("R") and "F02D12" in event.telegram:
|
|
226
|
+
self.dispatch_output_state_event(event)
|
|
227
|
+
return event.frame
|
|
228
|
+
|
|
229
|
+
# Check if telegram is Reply (R) with Read Datapoint (F02) LIGHT_LEVEL (D15)
|
|
230
|
+
if event.telegram_type in ("R") and "F02D15" in event.telegram:
|
|
231
|
+
self.dispatch_light_level_event(event)
|
|
232
|
+
return event.frame
|
|
233
|
+
|
|
234
|
+
self.logger.warning(f"Unhandled telegram received: {event.telegram}")
|
|
235
|
+
self.logger.info(f"telegram_received unhandled event {event}")
|
|
236
|
+
return event.frame
|
|
237
|
+
|
|
238
|
+
def dispatch_light_level_event(self, event: TelegramReceivedEvent) -> None:
|
|
239
|
+
"""Dispatch light level received event.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
event: Telegram received event.
|
|
243
|
+
"""
|
|
244
|
+
self.logger.debug("Light level Datapoint, parsing telegram.")
|
|
245
|
+
reply_telegram = self.telegram_service.parse_reply_telegram(event.frame)
|
|
246
|
+
self.logger.debug(
|
|
247
|
+
f"Parsed telegram: "
|
|
248
|
+
f"serial={reply_telegram.serial_number}, "
|
|
249
|
+
f"type={reply_telegram.datapoint_type}, "
|
|
250
|
+
f"value={reply_telegram.data_value}"
|
|
251
|
+
)
|
|
252
|
+
self.logger.debug("About to dispatch LightLevelReceivedEvent")
|
|
253
|
+
self.event_bus.dispatch(
|
|
254
|
+
LightLevelReceivedEvent(
|
|
255
|
+
serial_number=reply_telegram.serial_number,
|
|
256
|
+
datapoint_type=reply_telegram.datapoint_type,
|
|
257
|
+
data_value=reply_telegram.data_value,
|
|
258
|
+
)
|
|
259
|
+
)
|
|
260
|
+
self.logger.debug("LightLevelReceivedEvent dispatched successfully")
|
|
261
|
+
|
|
262
|
+
def dispatch_output_state_event(self, event: TelegramReceivedEvent) -> None:
|
|
263
|
+
"""Dispatch output state received event.
|
|
264
|
+
|
|
265
|
+
Args:
|
|
266
|
+
event: Telegram received event.
|
|
267
|
+
"""
|
|
268
|
+
self.logger.debug("Module Read Datapoint, parsing telegram.")
|
|
269
|
+
reply_telegram = self.telegram_service.parse_reply_telegram(event.frame)
|
|
270
|
+
self.logger.debug(
|
|
271
|
+
f"Parsed telegram: "
|
|
272
|
+
f"serial={reply_telegram.serial_number}, "
|
|
273
|
+
f"type={reply_telegram.datapoint_type}, "
|
|
274
|
+
f"value={reply_telegram.data_value}"
|
|
275
|
+
)
|
|
276
|
+
self.logger.debug("About to dispatch OutputStateReceivedEvent")
|
|
277
|
+
self.event_bus.dispatch(
|
|
278
|
+
OutputStateReceivedEvent(
|
|
279
|
+
serial_number=reply_telegram.serial_number,
|
|
280
|
+
datapoint_type=reply_telegram.datapoint_type,
|
|
281
|
+
data_value=reply_telegram.data_value,
|
|
282
|
+
)
|
|
283
|
+
)
|
|
284
|
+
self.logger.debug("OutputStateReceivedEvent dispatched successfully")
|
|
285
|
+
|
|
286
|
+
def dispatch_event_telegram_received_event(
|
|
287
|
+
self, event: TelegramReceivedEvent
|
|
288
|
+
) -> None:
|
|
289
|
+
"""Dispatch event telegram received event.
|
|
290
|
+
|
|
291
|
+
Args:
|
|
292
|
+
event: Telegram received event.
|
|
293
|
+
"""
|
|
294
|
+
self.logger.debug("Event telegram received, parsing.")
|
|
295
|
+
|
|
296
|
+
# Parse event telegram to extract module information
|
|
297
|
+
event_telegram = self.telegram_service.parse_event_telegram(event.frame)
|
|
298
|
+
|
|
299
|
+
self.logger.debug(
|
|
300
|
+
f"Parsed event: "
|
|
301
|
+
f"module_type={event_telegram.module_type}, "
|
|
302
|
+
f"link={event_telegram.link_number}, "
|
|
303
|
+
f"input={event_telegram.input_number}"
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
# Dispatch ModuleStateChangedEvent for cache refresh
|
|
307
|
+
self.event_bus.dispatch(
|
|
308
|
+
ModuleStateChangedEvent(
|
|
309
|
+
module_type_code=event_telegram.module_type,
|
|
310
|
+
link_number=event_telegram.link_number,
|
|
311
|
+
input_number=event_telegram.input_number,
|
|
312
|
+
telegram_event_type=(
|
|
313
|
+
event_telegram.event_type.value
|
|
314
|
+
if event_telegram.event_type
|
|
315
|
+
else "M"
|
|
316
|
+
),
|
|
317
|
+
)
|
|
318
|
+
)
|
|
319
|
+
self.logger.debug("ModuleStateChangedEvent dispatched successfully")
|
|
320
|
+
|
|
321
|
+
def dispatch_module_discovered_event(self, event: TelegramReceivedEvent) -> None:
|
|
322
|
+
"""Dispatch module discovered event.
|
|
323
|
+
|
|
324
|
+
Args:
|
|
325
|
+
event: Telegram received event.
|
|
326
|
+
"""
|
|
327
|
+
self.logger.debug("Module discovered, dispatching ModuleDiscoveredEvent")
|
|
328
|
+
self.event_bus.dispatch(
|
|
329
|
+
ModuleDiscoveredEvent(
|
|
330
|
+
frame=event.frame,
|
|
331
|
+
telegram=event.telegram,
|
|
332
|
+
payload=event.payload,
|
|
333
|
+
telegram_type=event.telegram_type,
|
|
334
|
+
serial_number=event.serial_number,
|
|
335
|
+
checksum=event.checksum,
|
|
336
|
+
protocol=event.protocol,
|
|
337
|
+
)
|
|
338
|
+
)
|
|
339
|
+
self.logger.debug("ModuleDiscoveredEvent dispatched successfully")
|
|
340
|
+
|
|
341
|
+
def handle_module_discovered(self, event: ModuleDiscoveredEvent) -> str:
|
|
342
|
+
"""Handle module discovered event.
|
|
343
|
+
|
|
344
|
+
Args:
|
|
345
|
+
event: Module discovered event.
|
|
346
|
+
|
|
347
|
+
Returns:
|
|
348
|
+
Serial number of the discovered module.
|
|
349
|
+
"""
|
|
350
|
+
self.logger.debug("Handling module discovered event")
|
|
351
|
+
|
|
352
|
+
# Replace R with S and F01D with F02D00
|
|
353
|
+
new_telegram = event.telegram.replace("R", "S", 1).replace(
|
|
354
|
+
"F01D", "F02D00", 1
|
|
355
|
+
) # module type
|
|
356
|
+
|
|
357
|
+
self.logger.debug(f"Sending module type request: {new_telegram}")
|
|
358
|
+
event.protocol.sendFrame(new_telegram.encode())
|
|
359
|
+
return event.serial_number
|