lifx-emulator-core 3.3.0__py3-none-any.whl → 3.5.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.
- lifx_emulator/devices/__init__.py +8 -1
- lifx_emulator/devices/manager.py +44 -1
- lifx_emulator/devices/states.py +2 -0
- lifx_emulator/factories/builder.py +1 -0
- lifx_emulator/scenarios/manager.py +8 -7
- lifx_emulator/scenarios/models.py +1 -1
- lifx_emulator/server.py +4 -3
- {lifx_emulator_core-3.3.0.dist-info → lifx_emulator_core-3.5.0.dist-info}/METADATA +1 -1
- {lifx_emulator_core-3.3.0.dist-info → lifx_emulator_core-3.5.0.dist-info}/RECORD +10 -10
- {lifx_emulator_core-3.3.0.dist-info → lifx_emulator_core-3.5.0.dist-info}/WHEEL +0 -0
|
@@ -10,7 +10,12 @@ This module contains all device-related functionality including:
|
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
12
|
from lifx_emulator.devices.device import EmulatedLifxDevice
|
|
13
|
-
from lifx_emulator.devices.manager import
|
|
13
|
+
from lifx_emulator.devices.manager import (
|
|
14
|
+
DeviceAddedCallback,
|
|
15
|
+
DeviceManager,
|
|
16
|
+
DeviceRemovedCallback,
|
|
17
|
+
IDeviceManager,
|
|
18
|
+
)
|
|
14
19
|
from lifx_emulator.devices.observers import (
|
|
15
20
|
ActivityLogger,
|
|
16
21
|
ActivityObserver,
|
|
@@ -26,6 +31,8 @@ from lifx_emulator.devices.states import DeviceState
|
|
|
26
31
|
__all__ = [
|
|
27
32
|
"EmulatedLifxDevice",
|
|
28
33
|
"DeviceManager",
|
|
34
|
+
"DeviceAddedCallback",
|
|
35
|
+
"DeviceRemovedCallback",
|
|
29
36
|
"IDeviceManager",
|
|
30
37
|
"DeviceState",
|
|
31
38
|
"DevicePersistenceAsyncFile",
|
lifx_emulator/devices/manager.py
CHANGED
|
@@ -8,6 +8,7 @@ concerns principle by extracting domain logic from the network layer.
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
10
|
import logging
|
|
11
|
+
from collections.abc import Callable
|
|
11
12
|
from typing import TYPE_CHECKING, Protocol, runtime_checkable
|
|
12
13
|
|
|
13
14
|
if TYPE_CHECKING:
|
|
@@ -18,6 +19,10 @@ if TYPE_CHECKING:
|
|
|
18
19
|
|
|
19
20
|
logger = logging.getLogger(__name__)
|
|
20
21
|
|
|
22
|
+
# Type aliases for device lifecycle callbacks
|
|
23
|
+
DeviceAddedCallback = Callable[["EmulatedLifxDevice"], None]
|
|
24
|
+
DeviceRemovedCallback = Callable[[str], None]
|
|
25
|
+
|
|
21
26
|
|
|
22
27
|
@runtime_checkable
|
|
23
28
|
class IDeviceManager(Protocol):
|
|
@@ -116,15 +121,27 @@ class DeviceManager:
|
|
|
116
121
|
This class extracts device management logic from EmulatedLifxServer,
|
|
117
122
|
providing a clean separation between domain logic and network I/O.
|
|
118
123
|
It mirrors the architecture of HierarchicalScenarioManager.
|
|
124
|
+
|
|
125
|
+
Supports optional callbacks for device lifecycle events, allowing
|
|
126
|
+
external systems (like WebSocket managers) to be notified of changes.
|
|
119
127
|
"""
|
|
120
128
|
|
|
121
|
-
def __init__(
|
|
129
|
+
def __init__(
|
|
130
|
+
self,
|
|
131
|
+
device_repository: IDeviceRepository,
|
|
132
|
+
on_device_added: DeviceAddedCallback | None = None,
|
|
133
|
+
on_device_removed: DeviceRemovedCallback | None = None,
|
|
134
|
+
):
|
|
122
135
|
"""Initialize the device manager.
|
|
123
136
|
|
|
124
137
|
Args:
|
|
125
138
|
device_repository: Repository for device storage and retrieval
|
|
139
|
+
on_device_added: Optional callback invoked when a device is added
|
|
140
|
+
on_device_removed: Optional callback invoked when a device is removed
|
|
126
141
|
"""
|
|
127
142
|
self._device_repository = device_repository
|
|
143
|
+
self.on_device_added = on_device_added
|
|
144
|
+
self.on_device_removed = on_device_removed
|
|
128
145
|
|
|
129
146
|
def add_device(
|
|
130
147
|
self,
|
|
@@ -152,6 +169,11 @@ class DeviceManager:
|
|
|
152
169
|
if success:
|
|
153
170
|
serial = device.state.serial
|
|
154
171
|
logger.info("Added device: %s (product=%s)", serial, device.state.product)
|
|
172
|
+
if self.on_device_added is not None:
|
|
173
|
+
try:
|
|
174
|
+
self.on_device_added(device)
|
|
175
|
+
except Exception:
|
|
176
|
+
logger.exception("Error in on_device_added callback for %s", serial)
|
|
155
177
|
return success
|
|
156
178
|
|
|
157
179
|
def remove_device(self, serial: str, storage=None) -> bool:
|
|
@@ -172,6 +194,14 @@ class DeviceManager:
|
|
|
172
194
|
if storage:
|
|
173
195
|
storage.delete_device_state(serial)
|
|
174
196
|
|
|
197
|
+
if self.on_device_removed is not None:
|
|
198
|
+
try:
|
|
199
|
+
self.on_device_removed(serial)
|
|
200
|
+
except Exception:
|
|
201
|
+
logger.exception(
|
|
202
|
+
"Error in on_device_removed callback for %s", serial
|
|
203
|
+
)
|
|
204
|
+
|
|
175
205
|
return success
|
|
176
206
|
|
|
177
207
|
def remove_all_devices(self, delete_storage: bool = False, storage=None) -> int:
|
|
@@ -184,6 +214,9 @@ class DeviceManager:
|
|
|
184
214
|
Returns:
|
|
185
215
|
Number of devices removed
|
|
186
216
|
"""
|
|
217
|
+
# Get all device serials before clearing (for callbacks)
|
|
218
|
+
serials = [d.state.serial for d in self._device_repository.get_all()]
|
|
219
|
+
|
|
187
220
|
# Clear all devices from repository
|
|
188
221
|
device_count = self._device_repository.clear()
|
|
189
222
|
logger.info("Removed all %s device(s)", device_count)
|
|
@@ -193,6 +226,16 @@ class DeviceManager:
|
|
|
193
226
|
deleted = storage.delete_all_device_states()
|
|
194
227
|
logger.info("Deleted %s device state(s) from persistent storage", deleted)
|
|
195
228
|
|
|
229
|
+
# Notify callbacks for each removed device
|
|
230
|
+
if self.on_device_removed is not None:
|
|
231
|
+
for serial in serials:
|
|
232
|
+
try:
|
|
233
|
+
self.on_device_removed(serial)
|
|
234
|
+
except Exception:
|
|
235
|
+
logger.exception(
|
|
236
|
+
"Error in on_device_removed callback for %s", serial
|
|
237
|
+
)
|
|
238
|
+
|
|
196
239
|
return device_count
|
|
197
240
|
|
|
198
241
|
def get_device(self, serial: str) -> EmulatedLifxDevice | None:
|
lifx_emulator/devices/states.py
CHANGED
|
@@ -182,6 +182,7 @@ class DeviceState:
|
|
|
182
182
|
has_multizone: bool = False
|
|
183
183
|
has_extended_multizone: bool = False
|
|
184
184
|
has_matrix: bool = False
|
|
185
|
+
has_chain: bool = False
|
|
185
186
|
has_hev: bool = False
|
|
186
187
|
has_relays: bool = False
|
|
187
188
|
has_buttons: bool = False
|
|
@@ -348,6 +349,7 @@ class DeviceState:
|
|
|
348
349
|
"has_multizone",
|
|
349
350
|
"has_extended_multizone",
|
|
350
351
|
"has_matrix",
|
|
352
|
+
"has_chain",
|
|
351
353
|
"has_hev",
|
|
352
354
|
"has_relays",
|
|
353
355
|
"has_buttons",
|
|
@@ -256,6 +256,7 @@ class DeviceBuilder:
|
|
|
256
256
|
has_multizone=self._product_info.has_multizone,
|
|
257
257
|
has_extended_multizone=has_extended_multizone,
|
|
258
258
|
has_matrix=self._product_info.has_matrix,
|
|
259
|
+
has_chain=self._product_info.has_chain,
|
|
259
260
|
has_hev=self._product_info.has_hev,
|
|
260
261
|
has_relays=self._product_info.has_relays,
|
|
261
262
|
has_buttons=self._product_info.has_buttons,
|
|
@@ -60,8 +60,9 @@ class HierarchicalScenarioManager:
|
|
|
60
60
|
self.type_scenarios: dict[str, ScenarioConfig] = {} # type → config
|
|
61
61
|
self.location_scenarios: dict[str, ScenarioConfig] = {} # location → config
|
|
62
62
|
self.group_scenarios: dict[str, ScenarioConfig] = {} # group → config
|
|
63
|
+
# Default global scenario: send_unhandled=True matches real LIFX behavior
|
|
63
64
|
self.global_scenario: ScenarioConfig = ScenarioConfig(
|
|
64
|
-
firmware_version=None, send_unhandled=
|
|
65
|
+
firmware_version=None, send_unhandled=True
|
|
65
66
|
)
|
|
66
67
|
|
|
67
68
|
def set_device_scenario(self, serial: str, config: ScenarioConfig):
|
|
@@ -101,9 +102,9 @@ class HierarchicalScenarioManager:
|
|
|
101
102
|
return self.group_scenarios.pop(group, None) is not None
|
|
102
103
|
|
|
103
104
|
def clear_global_scenario(self):
|
|
104
|
-
"""Clear global scenario (reset to
|
|
105
|
+
"""Clear global scenario (reset to default with send_unhandled=True)."""
|
|
105
106
|
self.global_scenario = ScenarioConfig(
|
|
106
|
-
firmware_version=None, send_unhandled=
|
|
107
|
+
firmware_version=None, send_unhandled=True
|
|
107
108
|
)
|
|
108
109
|
|
|
109
110
|
def get_global_scenario(self) -> ScenarioConfig:
|
|
@@ -184,8 +185,8 @@ class HierarchicalScenarioManager:
|
|
|
184
185
|
Returns:
|
|
185
186
|
Merged ScenarioConfig
|
|
186
187
|
"""
|
|
187
|
-
# Start with
|
|
188
|
-
merged = ScenarioConfig(firmware_version=None, send_unhandled=
|
|
188
|
+
# Start with default config (send_unhandled=True matches real LIFX behavior)
|
|
189
|
+
merged = ScenarioConfig(firmware_version=None, send_unhandled=True)
|
|
189
190
|
|
|
190
191
|
# Layer in each scope (general to specific)
|
|
191
192
|
# Later scopes override or merge with earlier ones
|
|
@@ -219,8 +220,8 @@ class HierarchicalScenarioManager:
|
|
|
219
220
|
# Scalars: use most specific non-default value
|
|
220
221
|
if config.firmware_version is not None:
|
|
221
222
|
merged.firmware_version = config.firmware_version
|
|
222
|
-
|
|
223
|
-
|
|
223
|
+
# send_unhandled: most specific scope wins (explicit False overrides)
|
|
224
|
+
merged.send_unhandled = config.send_unhandled
|
|
224
225
|
|
|
225
226
|
return merged
|
|
226
227
|
|
|
@@ -51,7 +51,7 @@ class ScenarioConfig(BaseModel):
|
|
|
51
51
|
description="List of packet types to send with incomplete data",
|
|
52
52
|
)
|
|
53
53
|
send_unhandled: bool = Field(
|
|
54
|
-
|
|
54
|
+
True, description="Send unhandled message responses for unknown packet types"
|
|
55
55
|
)
|
|
56
56
|
|
|
57
57
|
@property
|
lifx_emulator/server.py
CHANGED
|
@@ -482,7 +482,7 @@ class EmulatedLifxServer:
|
|
|
482
482
|
"packets_received_by_type": dict(self.packets_received_by_type),
|
|
483
483
|
"packets_sent_by_type": dict(self.packets_sent_by_type),
|
|
484
484
|
"error_count": self.error_count,
|
|
485
|
-
"activity_enabled":
|
|
485
|
+
"activity_enabled": hasattr(self.activity_observer, "get_recent_activity"),
|
|
486
486
|
}
|
|
487
487
|
|
|
488
488
|
def get_recent_activity(self) -> list[dict[str, Any]]:
|
|
@@ -492,8 +492,9 @@ class EmulatedLifxServer:
|
|
|
492
492
|
List of activity event dictionaries, or empty list if observer
|
|
493
493
|
doesn't support activity tracking
|
|
494
494
|
"""
|
|
495
|
-
|
|
496
|
-
|
|
495
|
+
get_activity = getattr(self.activity_observer, "get_recent_activity", None)
|
|
496
|
+
if get_activity is not None:
|
|
497
|
+
return get_activity()
|
|
497
498
|
return []
|
|
498
499
|
|
|
499
500
|
async def start(self):
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
lifx_emulator/__init__.py,sha256=SSnQg0RiCaID7DhHOGZoVIDQ3_8Lyt0J9cA_2StF63s,824
|
|
2
2
|
lifx_emulator/constants.py,sha256=DFZkUsdewE-x_3MgO28tMGkjUCWPeYc3xLj_EXViGOw,1032
|
|
3
|
-
lifx_emulator/server.py,sha256=
|
|
4
|
-
lifx_emulator/devices/__init__.py,sha256=
|
|
3
|
+
lifx_emulator/server.py,sha256=UsecEjdxTvJrM3KXc1S8_YIkQIBX9Yi8WnBnja67Og8,18395
|
|
4
|
+
lifx_emulator/devices/__init__.py,sha256=Cfr2L1XqtZcxSTaSFcq0TdQnAjjT4PoGiiMxge3eMI0,1210
|
|
5
5
|
lifx_emulator/devices/device.py,sha256=w7Y9VYux3cyGus9mFmruFn6WYtoD7zwhzCL2xrICY-Q,17180
|
|
6
|
-
lifx_emulator/devices/manager.py,sha256=
|
|
6
|
+
lifx_emulator/devices/manager.py,sha256=5mc1f8sB_vVYvxwmixFfpbQHVK7VPtBJIE7ChjVCEGk,9961
|
|
7
7
|
lifx_emulator/devices/observers.py,sha256=-KnUgFcKdhlNo7CNVstP-u0wU2W0JAGg055ZPV15Sj0,3874
|
|
8
8
|
lifx_emulator/devices/persistence.py,sha256=9Mhj46-xrweOmyzjORCi2jKIwa8XJWpQ5CgaKcw6U98,10513
|
|
9
9
|
lifx_emulator/devices/state_restorer.py,sha256=eDsRSW-2RviP_0Qlk2DHqMaB-zhV0X1cNQECv2lD1qc,9809
|
|
10
10
|
lifx_emulator/devices/state_serializer.py,sha256=aws4LUmXBJS8oBrQziJtlV0XMvCTm5X4dGkGlO_QHcM,6281
|
|
11
|
-
lifx_emulator/devices/states.py,sha256=
|
|
11
|
+
lifx_emulator/devices/states.py,sha256=7UfGoFbgV5TZNGNm4PIthFw9s7-tG1tF_5AfQu5q6d0,12257
|
|
12
12
|
lifx_emulator/factories/__init__.py,sha256=CsryMcf_80hTjOAgrukA6vRZaZow_2VQkSewrpP9gEI,1210
|
|
13
|
-
lifx_emulator/factories/builder.py,sha256=
|
|
13
|
+
lifx_emulator/factories/builder.py,sha256=f70iH1MnO_UrsyHVXgE2BlWCYFZvZ6df6rwvxutby6g,12216
|
|
14
14
|
lifx_emulator/factories/default_config.py,sha256=FTcxKDfeTmO49GTSki8nxnEIZQzR0Lg0hL_PwHUrkVQ,4828
|
|
15
15
|
lifx_emulator/factories/factory.py,sha256=MyGG-pW7EV2BFP5ZzgMuFF5TfNFvfyFDoE5dmd3LC8w,8623
|
|
16
16
|
lifx_emulator/factories/firmware_config.py,sha256=tPN5Hq-uNb1xzW9Q0A9jD-G0-NaGfINcD0i1XZRUMoE,2711
|
|
@@ -39,9 +39,9 @@ lifx_emulator/repositories/__init__.py,sha256=x-ncM6T_Q7jNrwhK4a1uAyMrTGHHGeUzPS
|
|
|
39
39
|
lifx_emulator/repositories/device_repository.py,sha256=KsXVg2sg7PGSTsK_PvDYeHHwEPM9Qx2ZZF_ORncBrYQ,3929
|
|
40
40
|
lifx_emulator/repositories/storage_backend.py,sha256=wEgjhnBvAxl6aO1ZGL3ou0dW9P2hBPnK8jEE03sOlL4,3264
|
|
41
41
|
lifx_emulator/scenarios/__init__.py,sha256=CGjudoWvyysvFj2xej11N2cr3mYROGtRb9zVHcOHGrQ,665
|
|
42
|
-
lifx_emulator/scenarios/manager.py,sha256=
|
|
43
|
-
lifx_emulator/scenarios/models.py,sha256=
|
|
42
|
+
lifx_emulator/scenarios/manager.py,sha256=pp_XHkEbVofwgng5pYAV8lL5eQM6tubte5Dq4hAlOCA,11377
|
|
43
|
+
lifx_emulator/scenarios/models.py,sha256=1OfQWIr3yEwI5IUTBPewxCvagNNhoLaoqfEp3fZJTtQ,4503
|
|
44
44
|
lifx_emulator/scenarios/persistence.py,sha256=3vjtPNFYfag38tUxuqxkGpWhQ7uBitc1rLroSAuw9N8,8881
|
|
45
|
-
lifx_emulator_core-3.
|
|
46
|
-
lifx_emulator_core-3.
|
|
47
|
-
lifx_emulator_core-3.
|
|
45
|
+
lifx_emulator_core-3.5.0.dist-info/METADATA,sha256=BEWJae_4f4ETk31sHpK_dEruIRohKUs5iD9pqJD6VsE,3217
|
|
46
|
+
lifx_emulator_core-3.5.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
47
|
+
lifx_emulator_core-3.5.0.dist-info/RECORD,,
|
|
File without changes
|