lifx-emulator 2.4.0__py3-none-any.whl → 3.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.
- lifx_emulator-3.0.1.dist-info/METADATA +102 -0
- lifx_emulator-3.0.1.dist-info/RECORD +18 -0
- lifx_emulator-3.0.1.dist-info/entry_points.txt +2 -0
- lifx_emulator_app/__init__.py +10 -0
- {lifx_emulator → lifx_emulator_app}/__main__.py +2 -3
- {lifx_emulator → lifx_emulator_app}/api/__init__.py +1 -1
- {lifx_emulator → lifx_emulator_app}/api/app.py +3 -3
- {lifx_emulator → lifx_emulator_app}/api/mappers/__init__.py +1 -1
- {lifx_emulator → lifx_emulator_app}/api/mappers/device_mapper.py +1 -1
- {lifx_emulator → lifx_emulator_app}/api/models.py +1 -2
- lifx_emulator_app/api/routers/__init__.py +11 -0
- {lifx_emulator → lifx_emulator_app}/api/routers/devices.py +2 -2
- {lifx_emulator → lifx_emulator_app}/api/routers/monitoring.py +1 -1
- {lifx_emulator → lifx_emulator_app}/api/routers/scenarios.py +1 -1
- lifx_emulator_app/api/services/__init__.py +8 -0
- {lifx_emulator → lifx_emulator_app}/api/services/device_service.py +3 -2
- lifx_emulator/__init__.py +0 -31
- lifx_emulator/api/routers/__init__.py +0 -11
- lifx_emulator/api/services/__init__.py +0 -8
- lifx_emulator/constants.py +0 -33
- lifx_emulator/devices/__init__.py +0 -37
- lifx_emulator/devices/device.py +0 -395
- lifx_emulator/devices/manager.py +0 -256
- lifx_emulator/devices/observers.py +0 -139
- lifx_emulator/devices/persistence.py +0 -308
- lifx_emulator/devices/state_restorer.py +0 -259
- lifx_emulator/devices/state_serializer.py +0 -157
- lifx_emulator/devices/states.py +0 -381
- lifx_emulator/factories/__init__.py +0 -39
- lifx_emulator/factories/builder.py +0 -375
- lifx_emulator/factories/default_config.py +0 -158
- lifx_emulator/factories/factory.py +0 -252
- lifx_emulator/factories/firmware_config.py +0 -77
- lifx_emulator/factories/serial_generator.py +0 -82
- lifx_emulator/handlers/__init__.py +0 -39
- lifx_emulator/handlers/base.py +0 -49
- lifx_emulator/handlers/device_handlers.py +0 -322
- lifx_emulator/handlers/light_handlers.py +0 -503
- lifx_emulator/handlers/multizone_handlers.py +0 -249
- lifx_emulator/handlers/registry.py +0 -110
- lifx_emulator/handlers/tile_handlers.py +0 -488
- lifx_emulator/products/__init__.py +0 -28
- lifx_emulator/products/generator.py +0 -1079
- lifx_emulator/products/registry.py +0 -1530
- lifx_emulator/products/specs.py +0 -284
- lifx_emulator/products/specs.yml +0 -386
- lifx_emulator/protocol/__init__.py +0 -1
- lifx_emulator/protocol/base.py +0 -446
- lifx_emulator/protocol/const.py +0 -8
- lifx_emulator/protocol/generator.py +0 -1384
- lifx_emulator/protocol/header.py +0 -159
- lifx_emulator/protocol/packets.py +0 -1351
- lifx_emulator/protocol/protocol_types.py +0 -817
- lifx_emulator/protocol/serializer.py +0 -379
- lifx_emulator/repositories/__init__.py +0 -22
- lifx_emulator/repositories/device_repository.py +0 -155
- lifx_emulator/repositories/storage_backend.py +0 -107
- lifx_emulator/scenarios/__init__.py +0 -22
- lifx_emulator/scenarios/manager.py +0 -322
- lifx_emulator/scenarios/models.py +0 -112
- lifx_emulator/scenarios/persistence.py +0 -241
- lifx_emulator/server.py +0 -464
- lifx_emulator-2.4.0.dist-info/METADATA +0 -107
- lifx_emulator-2.4.0.dist-info/RECORD +0 -62
- lifx_emulator-2.4.0.dist-info/entry_points.txt +0 -2
- lifx_emulator-2.4.0.dist-info/licenses/LICENSE +0 -35
- {lifx_emulator-2.4.0.dist-info → lifx_emulator-3.0.1.dist-info}/WHEEL +0 -0
- {lifx_emulator → lifx_emulator_app}/api/templates/dashboard.html +0 -0
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
"""Shared serialization logic for device state."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
from typing import Any
|
|
6
|
-
|
|
7
|
-
from lifx_emulator.protocol.protocol_types import LightHsbk
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def serialize_hsbk(hsbk: LightHsbk) -> dict[str, int]:
|
|
11
|
-
"""Serialize LightHsbk to dict."""
|
|
12
|
-
return {
|
|
13
|
-
"hue": hsbk.hue,
|
|
14
|
-
"saturation": hsbk.saturation,
|
|
15
|
-
"brightness": hsbk.brightness,
|
|
16
|
-
"kelvin": hsbk.kelvin,
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def deserialize_hsbk(data: dict[str, int]) -> LightHsbk:
|
|
21
|
-
"""Deserialize dict to LightHsbk."""
|
|
22
|
-
return LightHsbk(
|
|
23
|
-
hue=data["hue"],
|
|
24
|
-
saturation=data["saturation"],
|
|
25
|
-
brightness=data["brightness"],
|
|
26
|
-
kelvin=data["kelvin"],
|
|
27
|
-
)
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def serialize_device_state(device_state: Any) -> dict[str, Any]:
|
|
31
|
-
"""Serialize DeviceState to dict.
|
|
32
|
-
|
|
33
|
-
Note: Accesses state via properties for backward compatibility with composed state.
|
|
34
|
-
"""
|
|
35
|
-
state_dict = {
|
|
36
|
-
"serial": device_state.serial,
|
|
37
|
-
"label": device_state.label,
|
|
38
|
-
"product": device_state.product,
|
|
39
|
-
"power_level": device_state.power_level,
|
|
40
|
-
"color": serialize_hsbk(device_state.color),
|
|
41
|
-
"location_id": device_state.location_id.hex(),
|
|
42
|
-
"location_label": device_state.location_label,
|
|
43
|
-
"location_updated_at": device_state.location_updated_at,
|
|
44
|
-
"group_id": device_state.group_id.hex(),
|
|
45
|
-
"group_label": device_state.group_label,
|
|
46
|
-
"group_updated_at": device_state.group_updated_at,
|
|
47
|
-
"has_color": device_state.has_color,
|
|
48
|
-
"has_infrared": device_state.has_infrared,
|
|
49
|
-
"has_multizone": device_state.has_multizone,
|
|
50
|
-
"has_matrix": device_state.has_matrix,
|
|
51
|
-
"has_hev": device_state.has_hev,
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
if device_state.has_infrared:
|
|
55
|
-
state_dict["infrared_brightness"] = device_state.infrared_brightness
|
|
56
|
-
|
|
57
|
-
if device_state.has_hev:
|
|
58
|
-
state_dict["hev_cycle_duration_s"] = device_state.hev_cycle_duration_s
|
|
59
|
-
state_dict["hev_cycle_remaining_s"] = device_state.hev_cycle_remaining_s
|
|
60
|
-
state_dict["hev_cycle_last_power"] = device_state.hev_cycle_last_power
|
|
61
|
-
state_dict["hev_indication"] = device_state.hev_indication
|
|
62
|
-
state_dict["hev_last_result"] = device_state.hev_last_result
|
|
63
|
-
|
|
64
|
-
if device_state.has_multizone:
|
|
65
|
-
state_dict["zone_count"] = device_state.zone_count
|
|
66
|
-
state_dict["zone_colors"] = [
|
|
67
|
-
serialize_hsbk(c) for c in device_state.zone_colors
|
|
68
|
-
]
|
|
69
|
-
state_dict["multizone_effect_type"] = device_state.multizone_effect_type
|
|
70
|
-
state_dict["multizone_effect_speed"] = device_state.multizone_effect_speed
|
|
71
|
-
|
|
72
|
-
if device_state.has_matrix:
|
|
73
|
-
state_dict["tile_count"] = device_state.tile_count
|
|
74
|
-
state_dict["tile_width"] = device_state.tile_width
|
|
75
|
-
state_dict["tile_height"] = device_state.tile_height
|
|
76
|
-
state_dict["tile_effect_type"] = device_state.tile_effect_type
|
|
77
|
-
state_dict["tile_effect_speed"] = device_state.tile_effect_speed
|
|
78
|
-
state_dict["tile_effect_palette_count"] = device_state.tile_effect_palette_count
|
|
79
|
-
state_dict["tile_effect_palette"] = [
|
|
80
|
-
serialize_hsbk(c) for c in device_state.tile_effect_palette
|
|
81
|
-
]
|
|
82
|
-
state_dict["tile_devices"] = [
|
|
83
|
-
{
|
|
84
|
-
"accel_meas_x": t["accel_meas_x"],
|
|
85
|
-
"accel_meas_y": t["accel_meas_y"],
|
|
86
|
-
"accel_meas_z": t["accel_meas_z"],
|
|
87
|
-
"user_x": t["user_x"],
|
|
88
|
-
"user_y": t["user_y"],
|
|
89
|
-
"width": t["width"],
|
|
90
|
-
"height": t["height"],
|
|
91
|
-
"device_version_vendor": t["device_version_vendor"],
|
|
92
|
-
"device_version_product": t["device_version_product"],
|
|
93
|
-
"firmware_build": t["firmware_build"],
|
|
94
|
-
"firmware_version_minor": t["firmware_version_minor"],
|
|
95
|
-
"firmware_version_major": t["firmware_version_major"],
|
|
96
|
-
"colors": [serialize_hsbk(c) for c in t["colors"]],
|
|
97
|
-
}
|
|
98
|
-
for t in device_state.tile_devices
|
|
99
|
-
]
|
|
100
|
-
# Serialize tile framebuffers (non-visible framebuffers 1-7)
|
|
101
|
-
state_dict["tile_framebuffers"] = [
|
|
102
|
-
{
|
|
103
|
-
"tile_index": fb.tile_index,
|
|
104
|
-
"framebuffers": {
|
|
105
|
-
str(fb_idx): [serialize_hsbk(c) for c in colors]
|
|
106
|
-
for fb_idx, colors in fb.framebuffers.items()
|
|
107
|
-
},
|
|
108
|
-
}
|
|
109
|
-
for fb in device_state.tile_framebuffers
|
|
110
|
-
]
|
|
111
|
-
|
|
112
|
-
return state_dict
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
def deserialize_device_state(state_dict: dict[str, Any]) -> dict[str, Any]:
|
|
116
|
-
"""Deserialize device state dict (convert hex strings and nested objects)."""
|
|
117
|
-
# Deserialize bytes fields
|
|
118
|
-
state_dict["location_id"] = bytes.fromhex(state_dict["location_id"])
|
|
119
|
-
state_dict["group_id"] = bytes.fromhex(state_dict["group_id"])
|
|
120
|
-
|
|
121
|
-
# Deserialize color
|
|
122
|
-
state_dict["color"] = deserialize_hsbk(state_dict["color"])
|
|
123
|
-
|
|
124
|
-
# Deserialize zone colors if present
|
|
125
|
-
if "zone_colors" in state_dict:
|
|
126
|
-
state_dict["zone_colors"] = [
|
|
127
|
-
deserialize_hsbk(c) for c in state_dict["zone_colors"]
|
|
128
|
-
]
|
|
129
|
-
|
|
130
|
-
# Deserialize tile effect palette if present
|
|
131
|
-
if "tile_effect_palette" in state_dict:
|
|
132
|
-
state_dict["tile_effect_palette"] = [
|
|
133
|
-
deserialize_hsbk(c) for c in state_dict["tile_effect_palette"]
|
|
134
|
-
]
|
|
135
|
-
|
|
136
|
-
# Deserialize tile devices if present
|
|
137
|
-
if "tile_devices" in state_dict:
|
|
138
|
-
for tile_dict in state_dict["tile_devices"]:
|
|
139
|
-
tile_dict["colors"] = [deserialize_hsbk(c) for c in tile_dict["colors"]]
|
|
140
|
-
|
|
141
|
-
# Deserialize tile framebuffers if present (for backwards compatibility)
|
|
142
|
-
if "tile_framebuffers" in state_dict:
|
|
143
|
-
from lifx_emulator.devices.states import TileFramebuffers
|
|
144
|
-
|
|
145
|
-
deserialized_fbs = []
|
|
146
|
-
for fb_dict in state_dict["tile_framebuffers"]:
|
|
147
|
-
tile_fb = TileFramebuffers(tile_index=fb_dict["tile_index"])
|
|
148
|
-
# Deserialize each framebuffer's colors
|
|
149
|
-
for fb_idx_str, colors_list in fb_dict["framebuffers"].items():
|
|
150
|
-
fb_idx = int(fb_idx_str)
|
|
151
|
-
tile_fb.framebuffers[fb_idx] = [
|
|
152
|
-
deserialize_hsbk(c) for c in colors_list
|
|
153
|
-
]
|
|
154
|
-
deserialized_fbs.append(tile_fb)
|
|
155
|
-
state_dict["tile_framebuffers"] = deserialized_fbs
|
|
156
|
-
|
|
157
|
-
return state_dict
|
lifx_emulator/devices/states.py
DELETED
|
@@ -1,381 +0,0 @@
|
|
|
1
|
-
"""Focused state dataclasses following Single Responsibility Principle."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import time
|
|
6
|
-
import uuid
|
|
7
|
-
from dataclasses import dataclass, field
|
|
8
|
-
from typing import Any
|
|
9
|
-
|
|
10
|
-
from lifx_emulator.constants import LIFX_UDP_PORT
|
|
11
|
-
from lifx_emulator.protocol.protocol_types import LightHsbk
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
@dataclass
|
|
15
|
-
class CoreDeviceState:
|
|
16
|
-
"""Core device identification and basic state."""
|
|
17
|
-
|
|
18
|
-
serial: str
|
|
19
|
-
label: str
|
|
20
|
-
power_level: int
|
|
21
|
-
color: LightHsbk
|
|
22
|
-
vendor: int
|
|
23
|
-
product: int
|
|
24
|
-
version_major: int
|
|
25
|
-
version_minor: int
|
|
26
|
-
build_timestamp: int
|
|
27
|
-
uptime_ns: int = 0
|
|
28
|
-
mac_address: bytes = field(default_factory=lambda: bytes.fromhex("d073d5123456"))
|
|
29
|
-
port: int = LIFX_UDP_PORT
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
@dataclass
|
|
33
|
-
class NetworkState:
|
|
34
|
-
"""Network and connectivity state."""
|
|
35
|
-
|
|
36
|
-
wifi_signal: float = -45.0
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
@dataclass
|
|
40
|
-
class LocationState:
|
|
41
|
-
"""Device location metadata."""
|
|
42
|
-
|
|
43
|
-
location_id: bytes = field(default_factory=lambda: uuid.uuid4().bytes)
|
|
44
|
-
location_label: str = "Test Location"
|
|
45
|
-
location_updated_at: int = field(default_factory=lambda: int(time.time() * 1e9))
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
@dataclass
|
|
49
|
-
class GroupState:
|
|
50
|
-
"""Device group metadata."""
|
|
51
|
-
|
|
52
|
-
group_id: bytes = field(default_factory=lambda: uuid.uuid4().bytes)
|
|
53
|
-
group_label: str = "Test Group"
|
|
54
|
-
group_updated_at: int = field(default_factory=lambda: int(time.time() * 1e9))
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
@dataclass
|
|
58
|
-
class InfraredState:
|
|
59
|
-
"""Infrared capability state."""
|
|
60
|
-
|
|
61
|
-
infrared_brightness: int = 0 # 0-65535
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
@dataclass
|
|
65
|
-
class HevState:
|
|
66
|
-
"""HEV (germicidal UV) capability state."""
|
|
67
|
-
|
|
68
|
-
hev_cycle_duration_s: int = 7200 # 2 hours default
|
|
69
|
-
hev_cycle_remaining_s: int = 0
|
|
70
|
-
hev_cycle_last_power: bool = False
|
|
71
|
-
hev_indication: bool = True
|
|
72
|
-
hev_last_result: int = 0 # 0=success
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
@dataclass
|
|
76
|
-
class MultiZoneState:
|
|
77
|
-
"""Multizone (strip/beam) capability state."""
|
|
78
|
-
|
|
79
|
-
zone_count: int
|
|
80
|
-
zone_colors: list[LightHsbk]
|
|
81
|
-
effect_type: int = 0 # 0=OFF, 1=MOVE, 2=RESERVED
|
|
82
|
-
effect_speed: int = 5 # Duration of one cycle in seconds
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
@dataclass
|
|
86
|
-
class TileFramebuffers:
|
|
87
|
-
"""Internal storage for non-visible tile framebuffers (1-7).
|
|
88
|
-
|
|
89
|
-
Framebuffer 0 is stored in tile_devices[i]["colors"] (the visible buffer).
|
|
90
|
-
Framebuffers 1-7 are stored here for Set64/CopyFrameBuffer operations.
|
|
91
|
-
Each framebuffer is a list of LightHsbk colors with length = width * height.
|
|
92
|
-
"""
|
|
93
|
-
|
|
94
|
-
tile_index: int # Which tile this belongs to
|
|
95
|
-
framebuffers: dict[int, list[LightHsbk]] = field(default_factory=dict)
|
|
96
|
-
|
|
97
|
-
def get_framebuffer(
|
|
98
|
-
self, fb_index: int, width: int, height: int
|
|
99
|
-
) -> list[LightHsbk]:
|
|
100
|
-
"""Get framebuffer by index, creating it if needed."""
|
|
101
|
-
if fb_index not in self.framebuffers:
|
|
102
|
-
# Initialize with default black color
|
|
103
|
-
zones = width * height
|
|
104
|
-
self.framebuffers[fb_index] = [
|
|
105
|
-
LightHsbk(hue=0, saturation=0, brightness=0, kelvin=3500)
|
|
106
|
-
for _ in range(zones)
|
|
107
|
-
]
|
|
108
|
-
return self.framebuffers[fb_index]
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
@dataclass
|
|
112
|
-
class MatrixState:
|
|
113
|
-
"""Matrix (tile/candle) capability state."""
|
|
114
|
-
|
|
115
|
-
tile_count: int
|
|
116
|
-
tile_devices: list[dict[str, Any]]
|
|
117
|
-
tile_width: int
|
|
118
|
-
tile_height: int
|
|
119
|
-
effect_type: int = 0 # 0=OFF, 2=MORPH, 3=FLAME, 5=SKY
|
|
120
|
-
effect_speed: int = 5 # Duration of one cycle in seconds
|
|
121
|
-
effect_palette_count: int = 0
|
|
122
|
-
effect_palette: list[LightHsbk] = field(default_factory=list)
|
|
123
|
-
effect_sky_type: int = 0 # 0=SUNRISE, 1=SUNSET, 2=CLOUDS (only when effect_type=5)
|
|
124
|
-
effect_cloud_sat_min: int = (
|
|
125
|
-
0 # Min cloud saturation 0-200 (only when effect_type=5)
|
|
126
|
-
)
|
|
127
|
-
effect_cloud_sat_max: int = (
|
|
128
|
-
0 # Max cloud saturation 0-200 (only when effect_type=5)
|
|
129
|
-
)
|
|
130
|
-
# Internal storage for non-visible framebuffers (1-7) per tile
|
|
131
|
-
# Framebuffer 0 remains in tile_devices[i]["colors"]
|
|
132
|
-
tile_framebuffers: list[TileFramebuffers] = field(default_factory=list)
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
@dataclass
|
|
136
|
-
class WaveformState:
|
|
137
|
-
"""Waveform effect state."""
|
|
138
|
-
|
|
139
|
-
waveform_active: bool = False
|
|
140
|
-
waveform_type: int = 0
|
|
141
|
-
waveform_transient: bool = False
|
|
142
|
-
waveform_color: LightHsbk = field(
|
|
143
|
-
default_factory=lambda: LightHsbk(
|
|
144
|
-
hue=0, saturation=0, brightness=0, kelvin=3500
|
|
145
|
-
)
|
|
146
|
-
)
|
|
147
|
-
waveform_period_ms: int = 0
|
|
148
|
-
waveform_cycles: float = 0
|
|
149
|
-
waveform_duty_cycle: int = 0
|
|
150
|
-
waveform_skew_ratio: int = 0
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
@dataclass
|
|
154
|
-
class DeviceState:
|
|
155
|
-
"""Composed device state following Single Responsibility Principle.
|
|
156
|
-
|
|
157
|
-
Each aspect of device state is managed by a focused sub-state object.
|
|
158
|
-
Properties are automatically delegated to the appropriate state object
|
|
159
|
-
using __getattr__ and __setattr__ magic methods.
|
|
160
|
-
|
|
161
|
-
Examples:
|
|
162
|
-
>>> state.label # Delegates to state.core.label
|
|
163
|
-
>>> state.location_label # Delegates to state.location.location_label
|
|
164
|
-
>>> state.zone_count # Delegates to state.multizone.zone_count (if present)
|
|
165
|
-
"""
|
|
166
|
-
|
|
167
|
-
core: CoreDeviceState
|
|
168
|
-
network: NetworkState
|
|
169
|
-
location: LocationState
|
|
170
|
-
group: GroupState
|
|
171
|
-
waveform: WaveformState
|
|
172
|
-
|
|
173
|
-
# Optional capability-specific state
|
|
174
|
-
infrared: InfraredState | None = None
|
|
175
|
-
hev: HevState | None = None
|
|
176
|
-
multizone: MultiZoneState | None = None
|
|
177
|
-
matrix: MatrixState | None = None
|
|
178
|
-
|
|
179
|
-
# Capability flags (kept for convenience)
|
|
180
|
-
has_color: bool = True
|
|
181
|
-
has_infrared: bool = False
|
|
182
|
-
has_multizone: bool = False
|
|
183
|
-
has_extended_multizone: bool = False
|
|
184
|
-
has_matrix: bool = False
|
|
185
|
-
has_hev: bool = False
|
|
186
|
-
has_relays: bool = False
|
|
187
|
-
has_buttons: bool = False
|
|
188
|
-
|
|
189
|
-
# Attribute routing map: maps attribute prefixes to state objects
|
|
190
|
-
# This eliminates ~360 lines of property boilerplate
|
|
191
|
-
_ATTRIBUTE_ROUTES = {
|
|
192
|
-
# Core properties (no prefix)
|
|
193
|
-
"serial": "core",
|
|
194
|
-
"label": "core",
|
|
195
|
-
"power_level": "core",
|
|
196
|
-
"color": "core",
|
|
197
|
-
"vendor": "core",
|
|
198
|
-
"product": "core",
|
|
199
|
-
"version_major": "core",
|
|
200
|
-
"version_minor": "core",
|
|
201
|
-
"build_timestamp": "core",
|
|
202
|
-
"uptime_ns": "core",
|
|
203
|
-
"mac_address": "core",
|
|
204
|
-
"port": "core",
|
|
205
|
-
# Network properties
|
|
206
|
-
"wifi_signal": "network",
|
|
207
|
-
# Location properties
|
|
208
|
-
"location_id": "location",
|
|
209
|
-
"location_label": "location",
|
|
210
|
-
"location_updated_at": "location",
|
|
211
|
-
# Group properties
|
|
212
|
-
"group_id": "group",
|
|
213
|
-
"group_label": "group",
|
|
214
|
-
"group_updated_at": "group",
|
|
215
|
-
# Waveform properties
|
|
216
|
-
"waveform_active": "waveform",
|
|
217
|
-
"waveform_type": "waveform",
|
|
218
|
-
"waveform_transient": "waveform",
|
|
219
|
-
"waveform_color": "waveform",
|
|
220
|
-
"waveform_period_ms": "waveform",
|
|
221
|
-
"waveform_cycles": "waveform",
|
|
222
|
-
"waveform_duty_cycle": "waveform",
|
|
223
|
-
"waveform_skew_ratio": "waveform",
|
|
224
|
-
# Infrared properties
|
|
225
|
-
"infrared_brightness": "infrared",
|
|
226
|
-
# HEV properties
|
|
227
|
-
"hev_cycle_duration_s": "hev",
|
|
228
|
-
"hev_cycle_remaining_s": "hev",
|
|
229
|
-
"hev_cycle_last_power": "hev",
|
|
230
|
-
"hev_indication": "hev",
|
|
231
|
-
"hev_last_result": "hev",
|
|
232
|
-
# Multizone properties
|
|
233
|
-
"zone_count": "multizone",
|
|
234
|
-
"zone_colors": "multizone",
|
|
235
|
-
"multizone_effect_type": ("multizone", "effect_type"),
|
|
236
|
-
"multizone_effect_speed": ("multizone", "effect_speed"),
|
|
237
|
-
# Matrix/Tile properties
|
|
238
|
-
"tile_count": "matrix",
|
|
239
|
-
"tile_devices": "matrix",
|
|
240
|
-
"tile_width": "matrix",
|
|
241
|
-
"tile_height": "matrix",
|
|
242
|
-
"tile_effect_type": ("matrix", "effect_type"),
|
|
243
|
-
"tile_effect_speed": ("matrix", "effect_speed"),
|
|
244
|
-
"tile_effect_palette_count": ("matrix", "effect_palette_count"),
|
|
245
|
-
"tile_effect_palette": ("matrix", "effect_palette"),
|
|
246
|
-
"tile_effect_sky_type": ("matrix", "effect_sky_type"),
|
|
247
|
-
"tile_effect_cloud_sat_min": ("matrix", "effect_cloud_sat_min"),
|
|
248
|
-
"tile_effect_cloud_sat_max": ("matrix", "effect_cloud_sat_max"),
|
|
249
|
-
"tile_framebuffers": "matrix",
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
# Default values for optional state attributes when state object is None
|
|
253
|
-
_OPTIONAL_DEFAULTS = {
|
|
254
|
-
"infrared_brightness": 0,
|
|
255
|
-
"hev_cycle_duration_s": 0,
|
|
256
|
-
"hev_cycle_remaining_s": 0,
|
|
257
|
-
"hev_cycle_last_power": False,
|
|
258
|
-
"hev_indication": False,
|
|
259
|
-
"hev_last_result": 0,
|
|
260
|
-
"zone_count": 0,
|
|
261
|
-
"zone_colors": [],
|
|
262
|
-
"multizone_effect_type": 0,
|
|
263
|
-
"multizone_effect_speed": 0,
|
|
264
|
-
"tile_count": 0,
|
|
265
|
-
"tile_devices": [],
|
|
266
|
-
"tile_width": 8,
|
|
267
|
-
"tile_height": 8,
|
|
268
|
-
"tile_effect_type": 0,
|
|
269
|
-
"tile_effect_speed": 0,
|
|
270
|
-
"tile_effect_palette_count": 0,
|
|
271
|
-
"tile_effect_palette": [],
|
|
272
|
-
"tile_effect_sky_type": 0,
|
|
273
|
-
"tile_effect_cloud_sat_min": 0,
|
|
274
|
-
"tile_effect_cloud_sat_max": 0,
|
|
275
|
-
"tile_framebuffers": [],
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
def get_target_bytes(self) -> bytes:
|
|
279
|
-
"""Get target bytes for this device."""
|
|
280
|
-
return bytes.fromhex(self.core.serial) + b"\x00\x00"
|
|
281
|
-
|
|
282
|
-
def __getattr__(self, name: str) -> Any:
|
|
283
|
-
"""Dynamically delegate attribute access to appropriate state object.
|
|
284
|
-
|
|
285
|
-
This eliminates ~180 lines of @property boilerplate.
|
|
286
|
-
|
|
287
|
-
Args:
|
|
288
|
-
name: Attribute name being accessed
|
|
289
|
-
|
|
290
|
-
Returns:
|
|
291
|
-
Attribute value from the appropriate state object
|
|
292
|
-
|
|
293
|
-
Raises:
|
|
294
|
-
AttributeError: If attribute is not found
|
|
295
|
-
"""
|
|
296
|
-
# Check if this attribute has a routing rule
|
|
297
|
-
if name in self._ATTRIBUTE_ROUTES:
|
|
298
|
-
route = self._ATTRIBUTE_ROUTES[name]
|
|
299
|
-
|
|
300
|
-
# Route can be either 'state_name' or ('state_name', 'attr_name')
|
|
301
|
-
if isinstance(route, tuple):
|
|
302
|
-
state_name, attr_name = route
|
|
303
|
-
else:
|
|
304
|
-
state_name = route
|
|
305
|
-
attr_name = name
|
|
306
|
-
|
|
307
|
-
# Get the state object
|
|
308
|
-
state_obj = object.__getattribute__(self, state_name)
|
|
309
|
-
|
|
310
|
-
# Handle optional state objects (infrared, hev, multizone, matrix)
|
|
311
|
-
if state_obj is None:
|
|
312
|
-
# Return default value for optional attributes
|
|
313
|
-
return self._OPTIONAL_DEFAULTS.get(name)
|
|
314
|
-
|
|
315
|
-
# Delegate to the state object
|
|
316
|
-
return getattr(state_obj, attr_name)
|
|
317
|
-
|
|
318
|
-
# If not in routing map, raise AttributeError
|
|
319
|
-
raise AttributeError(
|
|
320
|
-
f"'{type(self).__name__}' object has no attribute '{name}'"
|
|
321
|
-
)
|
|
322
|
-
|
|
323
|
-
def __setattr__(self, name: str, value: Any) -> None:
|
|
324
|
-
"""Dynamically delegate attribute writes to appropriate state object.
|
|
325
|
-
|
|
326
|
-
This eliminates ~180 lines of @property.setter boilerplate.
|
|
327
|
-
|
|
328
|
-
Args:
|
|
329
|
-
name: Attribute name being set
|
|
330
|
-
value: Value to set
|
|
331
|
-
|
|
332
|
-
Note:
|
|
333
|
-
Dataclass fields and private attributes bypass delegation.
|
|
334
|
-
"""
|
|
335
|
-
# Dataclass fields and private attributes use normal assignment
|
|
336
|
-
if name in {
|
|
337
|
-
"core",
|
|
338
|
-
"network",
|
|
339
|
-
"location",
|
|
340
|
-
"group",
|
|
341
|
-
"waveform",
|
|
342
|
-
"infrared",
|
|
343
|
-
"hev",
|
|
344
|
-
"multizone",
|
|
345
|
-
"matrix",
|
|
346
|
-
"has_color",
|
|
347
|
-
"has_infrared",
|
|
348
|
-
"has_multizone",
|
|
349
|
-
"has_extended_multizone",
|
|
350
|
-
"has_matrix",
|
|
351
|
-
"has_hev",
|
|
352
|
-
"has_relays",
|
|
353
|
-
"has_buttons",
|
|
354
|
-
} or name.startswith("_"):
|
|
355
|
-
object.__setattr__(self, name, value)
|
|
356
|
-
return
|
|
357
|
-
|
|
358
|
-
# Check if this attribute has a routing rule
|
|
359
|
-
if name in self._ATTRIBUTE_ROUTES:
|
|
360
|
-
route = self._ATTRIBUTE_ROUTES[name]
|
|
361
|
-
|
|
362
|
-
# Route can be either 'state_name' or ('state_name', 'attr_name')
|
|
363
|
-
if isinstance(route, tuple):
|
|
364
|
-
state_name, attr_name = route
|
|
365
|
-
else:
|
|
366
|
-
state_name = route
|
|
367
|
-
attr_name = name
|
|
368
|
-
|
|
369
|
-
# Get the state object
|
|
370
|
-
state_obj = object.__getattribute__(self, state_name)
|
|
371
|
-
|
|
372
|
-
# Handle optional state objects - silently ignore writes if None
|
|
373
|
-
if state_obj is None:
|
|
374
|
-
return
|
|
375
|
-
|
|
376
|
-
# Delegate to the state object
|
|
377
|
-
setattr(state_obj, attr_name, value)
|
|
378
|
-
return
|
|
379
|
-
|
|
380
|
-
# For unknown attributes, use normal assignment (allows adding new attributes)
|
|
381
|
-
object.__setattr__(self, name, value)
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
"""Device factory for creating emulated LIFX devices.
|
|
2
|
-
|
|
3
|
-
This package provides a clean, testable API for creating LIFX devices using:
|
|
4
|
-
- Builder pattern for flexible device construction
|
|
5
|
-
- Separate services for serial generation, color config, firmware config
|
|
6
|
-
- Product registry integration for accurate device specifications
|
|
7
|
-
"""
|
|
8
|
-
|
|
9
|
-
from lifx_emulator.factories.builder import DeviceBuilder
|
|
10
|
-
from lifx_emulator.factories.default_config import DefaultColorConfig
|
|
11
|
-
from lifx_emulator.factories.factory import (
|
|
12
|
-
create_color_light,
|
|
13
|
-
create_color_temperature_light,
|
|
14
|
-
create_device,
|
|
15
|
-
create_hev_light,
|
|
16
|
-
create_infrared_light,
|
|
17
|
-
create_multizone_light,
|
|
18
|
-
create_switch,
|
|
19
|
-
create_tile_device,
|
|
20
|
-
)
|
|
21
|
-
from lifx_emulator.factories.firmware_config import FirmwareConfig
|
|
22
|
-
from lifx_emulator.factories.serial_generator import SerialGenerator
|
|
23
|
-
|
|
24
|
-
__all__ = [
|
|
25
|
-
# Builder and helpers
|
|
26
|
-
"DeviceBuilder",
|
|
27
|
-
"SerialGenerator",
|
|
28
|
-
"DefaultColorConfig",
|
|
29
|
-
"FirmwareConfig",
|
|
30
|
-
# Factory functions
|
|
31
|
-
"create_device",
|
|
32
|
-
"create_color_light",
|
|
33
|
-
"create_infrared_light",
|
|
34
|
-
"create_hev_light",
|
|
35
|
-
"create_multizone_light",
|
|
36
|
-
"create_switch",
|
|
37
|
-
"create_tile_device",
|
|
38
|
-
"create_color_temperature_light",
|
|
39
|
-
]
|