lifx-emulator-core 3.1.1__py3-none-any.whl → 3.3.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/device.py +44 -12
- lifx_emulator/handlers/multizone_handlers.py +19 -15
- lifx_emulator/handlers/tile_handlers.py +59 -52
- lifx_emulator/scenarios/models.py +12 -0
- lifx_emulator/server.py +55 -4
- {lifx_emulator_core-3.1.1.dist-info → lifx_emulator_core-3.3.0.dist-info}/METADATA +1 -1
- {lifx_emulator_core-3.1.1.dist-info → lifx_emulator_core-3.3.0.dist-info}/RECORD +8 -8
- {lifx_emulator_core-3.1.1.dist-info → lifx_emulator_core-3.3.0.dist-info}/WHEEL +0 -0
lifx_emulator/devices/device.py
CHANGED
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
import asyncio
|
|
6
6
|
import copy
|
|
7
7
|
import logging
|
|
8
|
+
import random
|
|
8
9
|
import time
|
|
9
10
|
from typing import Any
|
|
10
11
|
|
|
@@ -269,10 +270,8 @@ class EmulatedLifxDevice:
|
|
|
269
270
|
state_unhandled.PKT_TYPE,
|
|
270
271
|
len(unhandled_payload),
|
|
271
272
|
)
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
# Still send acknowledgment if requested
|
|
275
|
-
if header.ack_required:
|
|
273
|
+
# Send ack before StateUnhandled when scenario controls ack behavior
|
|
274
|
+
if header.ack_required and scenario.affects_acks:
|
|
276
275
|
ack_packet = Device.Acknowledgement()
|
|
277
276
|
ack_payload = ack_packet.pack()
|
|
278
277
|
ack_header = self._create_response_header(
|
|
@@ -281,15 +280,18 @@ class EmulatedLifxDevice:
|
|
|
281
280
|
ack_packet.PKT_TYPE,
|
|
282
281
|
len(ack_payload),
|
|
283
282
|
)
|
|
284
|
-
responses.append((ack_header, ack_packet))
|
|
283
|
+
responses.append((ack_header, ack_packet, ack_payload))
|
|
285
284
|
|
|
286
|
-
|
|
285
|
+
responses.append((unhandled_header, state_unhandled, unhandled_payload))
|
|
286
|
+
return self._apply_error_scenarios(responses, scenario)
|
|
287
287
|
|
|
288
288
|
# Update uptime
|
|
289
289
|
self.state.uptime_ns = self.get_uptime_ns()
|
|
290
290
|
|
|
291
291
|
# Handle acknowledgment (packet type 45, no payload)
|
|
292
|
-
|
|
292
|
+
# Only generate ack here when a scenario targets ack behavior;
|
|
293
|
+
# otherwise the server sends the ack immediately before calling us.
|
|
294
|
+
if header.ack_required and scenario.affects_acks:
|
|
293
295
|
ack_packet = Device.Acknowledgement()
|
|
294
296
|
ack_payload = ack_packet.pack()
|
|
295
297
|
ack_header = self._create_response_header(
|
|
@@ -304,6 +306,24 @@ class EmulatedLifxDevice:
|
|
|
304
306
|
|
|
305
307
|
# Handle specific packet types - handlers always return list
|
|
306
308
|
response_packets = self._handle_packet_type(header, packet)
|
|
309
|
+
|
|
310
|
+
# Apply partial_responses: truncate multi-packet responses to random subset
|
|
311
|
+
if len(response_packets) > 1:
|
|
312
|
+
first_pkt = response_packets[0]
|
|
313
|
+
if (
|
|
314
|
+
hasattr(first_pkt, "PKT_TYPE")
|
|
315
|
+
and first_pkt.PKT_TYPE in scenario.partial_responses
|
|
316
|
+
):
|
|
317
|
+
original_count = len(response_packets)
|
|
318
|
+
partial_count = random.randint(1, original_count - 1) # nosec
|
|
319
|
+
response_packets = response_packets[:partial_count]
|
|
320
|
+
logger.info(
|
|
321
|
+
"Sending partial response for packet type %s (%d of %d packets)",
|
|
322
|
+
first_pkt.PKT_TYPE,
|
|
323
|
+
partial_count,
|
|
324
|
+
original_count,
|
|
325
|
+
)
|
|
326
|
+
|
|
307
327
|
# Handlers now always return list (empty if no response)
|
|
308
328
|
for resp_packet in response_packets:
|
|
309
329
|
# Cache packed payload to avoid double packing (performance optimization)
|
|
@@ -317,16 +337,29 @@ class EmulatedLifxDevice:
|
|
|
317
337
|
# Store both header and pre-packed payload for error scenario processing
|
|
318
338
|
responses.append((resp_header, resp_packet, resp_payload))
|
|
319
339
|
|
|
320
|
-
|
|
321
|
-
|
|
340
|
+
return self._apply_error_scenarios(responses, scenario)
|
|
341
|
+
|
|
342
|
+
def _apply_error_scenarios(
|
|
343
|
+
self,
|
|
344
|
+
responses: list[tuple],
|
|
345
|
+
scenario: ScenarioConfig,
|
|
346
|
+
) -> list[tuple[LifxHeader, Any]]:
|
|
347
|
+
"""Apply malformed/invalid-field error scenarios to response packets.
|
|
348
|
+
|
|
349
|
+
Args:
|
|
350
|
+
responses: List of (header, packet, payload) tuples
|
|
351
|
+
scenario: Resolved scenario config
|
|
352
|
+
|
|
353
|
+
Returns:
|
|
354
|
+
List of (header, packet) tuples with error scenarios applied
|
|
355
|
+
"""
|
|
356
|
+
modified_responses: list[tuple[LifxHeader, Any]] = []
|
|
322
357
|
for resp_header, resp_packet, resp_payload in responses:
|
|
323
358
|
# Check if we should send malformed packet (truncate payload)
|
|
324
359
|
if resp_header.pkt_type in scenario.malformed_packets:
|
|
325
|
-
# For malformed packets, truncate the pre-packed payload
|
|
326
360
|
truncated_len = len(resp_payload) // 2
|
|
327
361
|
resp_payload_modified = resp_payload[:truncated_len]
|
|
328
362
|
resp_header.size = LIFX_HEADER_SIZE + truncated_len + 10 # Wrong size
|
|
329
|
-
# Convert back to bytes for malformed case
|
|
330
363
|
modified_responses.append((resp_header, resp_payload_modified))
|
|
331
364
|
logger.info(
|
|
332
365
|
"Sending malformed packet type %s (truncated)", resp_header.pkt_type
|
|
@@ -335,7 +368,6 @@ class EmulatedLifxDevice:
|
|
|
335
368
|
|
|
336
369
|
# Check if we should send invalid field values
|
|
337
370
|
if resp_header.pkt_type in scenario.invalid_field_values:
|
|
338
|
-
# Corrupt the pre-packed payload
|
|
339
371
|
resp_payload_modified = b"\xff" * len(resp_payload)
|
|
340
372
|
modified_responses.append((resp_header, resp_payload_modified))
|
|
341
373
|
pkt_type = resp_header.pkt_type
|
|
@@ -124,22 +124,26 @@ class ExtendedGetColorZonesHandler(PacketHandler):
|
|
|
124
124
|
if not device_state.has_multizone:
|
|
125
125
|
return []
|
|
126
126
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
127
|
+
responses = []
|
|
128
|
+
index = 0
|
|
129
|
+
while index < device_state.zone_count:
|
|
130
|
+
end = min(index + 82, device_state.zone_count)
|
|
131
|
+
colors_count = end - index
|
|
132
|
+
colors = list(device_state.zone_colors[index:end])
|
|
133
|
+
# Pad to 82 colors
|
|
134
|
+
while len(colors) < 82:
|
|
135
|
+
colors.append(LightHsbk(hue=0, saturation=0, brightness=0, kelvin=3500))
|
|
136
|
+
responses.append(
|
|
137
|
+
MultiZone.ExtendedStateMultiZone(
|
|
138
|
+
count=device_state.zone_count,
|
|
139
|
+
index=index,
|
|
140
|
+
colors_count=colors_count,
|
|
141
|
+
colors=colors,
|
|
142
|
+
)
|
|
141
143
|
)
|
|
142
|
-
|
|
144
|
+
index += 82
|
|
145
|
+
|
|
146
|
+
return responses
|
|
143
147
|
|
|
144
148
|
|
|
145
149
|
class ExtendedSetColorZonesHandler(PacketHandler):
|
|
@@ -128,64 +128,71 @@ class Get64Handler(PacketHandler):
|
|
|
128
128
|
if not device_state.has_matrix or not packet:
|
|
129
129
|
return []
|
|
130
130
|
|
|
131
|
-
tile_index = packet.tile_index
|
|
132
131
|
rect = packet.rect
|
|
132
|
+
length = max(1, packet.length)
|
|
133
133
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
tile_width = tile["width"]
|
|
139
|
-
tile_height = tile["height"]
|
|
140
|
-
|
|
141
|
-
# Get64 always returns framebuffer 0 (the visible buffer)
|
|
142
|
-
# regardless of which fb_index is in the request
|
|
143
|
-
tile_colors = tile["colors"]
|
|
144
|
-
|
|
145
|
-
# Calculate how many rows fit in 64 zones
|
|
146
|
-
rows_to_return = 64 // rect.width if rect.width > 0 else 1
|
|
147
|
-
rows_to_return = min(rows_to_return, tile_height - rect.y)
|
|
148
|
-
|
|
149
|
-
# Extract colors from the requested rectangle
|
|
150
|
-
colors = []
|
|
151
|
-
zones_extracted = 0
|
|
152
|
-
|
|
153
|
-
for row in range(rows_to_return):
|
|
154
|
-
y = rect.y + row
|
|
155
|
-
if y >= tile_height:
|
|
134
|
+
responses = []
|
|
135
|
+
for i in range(length):
|
|
136
|
+
idx = packet.tile_index + i
|
|
137
|
+
if idx >= len(device_state.tile_devices):
|
|
156
138
|
break
|
|
157
139
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
140
|
+
tile = device_state.tile_devices[idx]
|
|
141
|
+
tile_width = tile["width"]
|
|
142
|
+
tile_height = tile["height"]
|
|
143
|
+
|
|
144
|
+
# Get64 always returns framebuffer 0 (the visible buffer)
|
|
145
|
+
# regardless of which fb_index is in the request
|
|
146
|
+
tile_colors = tile["colors"]
|
|
147
|
+
|
|
148
|
+
# Calculate how many rows fit in 64 zones
|
|
149
|
+
rows_to_return = 64 // rect.width if rect.width > 0 else 1
|
|
150
|
+
rows_to_return = min(rows_to_return, tile_height - rect.y)
|
|
151
|
+
|
|
152
|
+
# Extract colors from the requested rectangle
|
|
153
|
+
colors = []
|
|
154
|
+
zones_extracted = 0
|
|
155
|
+
|
|
156
|
+
for row in range(rows_to_return):
|
|
157
|
+
y = rect.y + row
|
|
158
|
+
if y >= tile_height:
|
|
159
|
+
break
|
|
160
|
+
|
|
161
|
+
for col in range(rect.width):
|
|
162
|
+
x = rect.x + col
|
|
163
|
+
if x >= tile_width or zones_extracted >= 64:
|
|
164
|
+
colors.append(
|
|
165
|
+
LightHsbk(hue=0, saturation=0, brightness=0, kelvin=3500)
|
|
166
|
+
)
|
|
167
|
+
zones_extracted += 1
|
|
168
|
+
continue
|
|
169
|
+
|
|
170
|
+
# Calculate zone index in flat color array
|
|
171
|
+
zone_idx = y * tile_width + x
|
|
172
|
+
if zone_idx < len(tile_colors):
|
|
173
|
+
colors.append(tile_colors[zone_idx])
|
|
174
|
+
else:
|
|
175
|
+
colors.append(
|
|
176
|
+
LightHsbk(hue=0, saturation=0, brightness=0, kelvin=3500)
|
|
177
|
+
)
|
|
164
178
|
zones_extracted += 1
|
|
165
|
-
continue
|
|
166
179
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
fb_index=0, # Always return FB0
|
|
184
|
-
x=rect.x,
|
|
185
|
-
y=rect.y,
|
|
186
|
-
width=rect.width,
|
|
187
|
-
)
|
|
188
|
-
return [Tile.State64(tile_index=tile_index, rect=return_rect, colors=colors)]
|
|
180
|
+
# Pad to exactly 64 colors
|
|
181
|
+
while len(colors) < 64:
|
|
182
|
+
colors.append(LightHsbk(hue=0, saturation=0, brightness=0, kelvin=3500))
|
|
183
|
+
|
|
184
|
+
# Return with fb_index forced to 0 (visible buffer)
|
|
185
|
+
return_rect = TileBufferRect(
|
|
186
|
+
fb_index=0, # Always return FB0
|
|
187
|
+
x=rect.x,
|
|
188
|
+
y=rect.y,
|
|
189
|
+
width=rect.width,
|
|
190
|
+
)
|
|
191
|
+
responses.append(
|
|
192
|
+
Tile.State64(tile_index=idx, rect=return_rect, colors=colors)
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
return responses
|
|
189
196
|
|
|
190
197
|
|
|
191
198
|
class Set64Handler(PacketHandler):
|
|
@@ -6,6 +6,8 @@ of the application (domain, API, persistence, etc.).
|
|
|
6
6
|
|
|
7
7
|
from pydantic import BaseModel, Field, field_validator
|
|
8
8
|
|
|
9
|
+
ACK_PACKET_TYPE = 45
|
|
10
|
+
|
|
9
11
|
|
|
10
12
|
class ScenarioConfig(BaseModel):
|
|
11
13
|
"""Scenario configuration for testing LIFX protocol behavior.
|
|
@@ -52,6 +54,16 @@ class ScenarioConfig(BaseModel):
|
|
|
52
54
|
False, description="Send unhandled message responses for unknown packet types"
|
|
53
55
|
)
|
|
54
56
|
|
|
57
|
+
@property
|
|
58
|
+
def affects_acks(self) -> bool:
|
|
59
|
+
"""Whether this scenario configuration modifies acknowledgment behavior."""
|
|
60
|
+
return (
|
|
61
|
+
ACK_PACKET_TYPE in self.drop_packets
|
|
62
|
+
or ACK_PACKET_TYPE in self.response_delays
|
|
63
|
+
or ACK_PACKET_TYPE in self.malformed_packets
|
|
64
|
+
or ACK_PACKET_TYPE in self.invalid_field_values
|
|
65
|
+
)
|
|
66
|
+
|
|
55
67
|
@field_validator("drop_packets", mode="before")
|
|
56
68
|
@classmethod
|
|
57
69
|
def convert_drop_packets_keys(cls, v):
|
lifx_emulator/server.py
CHANGED
|
@@ -18,7 +18,7 @@ from lifx_emulator.devices import (
|
|
|
18
18
|
PacketEvent,
|
|
19
19
|
)
|
|
20
20
|
from lifx_emulator.protocol.header import LifxHeader
|
|
21
|
-
from lifx_emulator.protocol.packets import get_packet_class
|
|
21
|
+
from lifx_emulator.protocol.packets import Device, get_packet_class
|
|
22
22
|
from lifx_emulator.repositories import IScenarioStorageBackend
|
|
23
23
|
from lifx_emulator.scenarios import HierarchicalScenarioManager
|
|
24
24
|
|
|
@@ -179,6 +179,54 @@ class EmulatedLifxServer:
|
|
|
179
179
|
# Fallback for edge case where loop not yet cached
|
|
180
180
|
asyncio.create_task(self.server.handle_packet(data, addr))
|
|
181
181
|
|
|
182
|
+
def _send_ack(
|
|
183
|
+
self,
|
|
184
|
+
device: EmulatedLifxDevice,
|
|
185
|
+
header: LifxHeader,
|
|
186
|
+
addr: tuple[str, int],
|
|
187
|
+
) -> None:
|
|
188
|
+
"""Send an acknowledgment packet immediately via UDP.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
device: The device acknowledging the packet
|
|
192
|
+
header: Request header (source/sequence are copied)
|
|
193
|
+
addr: Client address (host, port)
|
|
194
|
+
"""
|
|
195
|
+
ack_packet = Device.Acknowledgement()
|
|
196
|
+
ack_payload = ack_packet.pack()
|
|
197
|
+
ack_header = device._create_response_header(
|
|
198
|
+
header.source,
|
|
199
|
+
header.sequence,
|
|
200
|
+
ack_packet.PKT_TYPE,
|
|
201
|
+
len(ack_payload),
|
|
202
|
+
)
|
|
203
|
+
response_data = ack_header.pack() + ack_payload
|
|
204
|
+
if self.transport:
|
|
205
|
+
self.transport.sendto(response_data, addr)
|
|
206
|
+
|
|
207
|
+
self.packets_sent += 1
|
|
208
|
+
self.packets_sent_by_type[ack_header.pkt_type] += 1
|
|
209
|
+
|
|
210
|
+
logger.debug(
|
|
211
|
+
"→ TX %s to %s:%s (target=%s, seq=%s) [no fields]",
|
|
212
|
+
_get_packet_type_name(ack_header.pkt_type),
|
|
213
|
+
addr[0],
|
|
214
|
+
addr[1],
|
|
215
|
+
device.state.serial,
|
|
216
|
+
ack_header.sequence,
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
self.activity_observer.on_packet_sent(
|
|
220
|
+
PacketEvent(
|
|
221
|
+
timestamp=time.time(),
|
|
222
|
+
direction="tx",
|
|
223
|
+
packet_type=ack_header.pkt_type,
|
|
224
|
+
packet_name=_get_packet_type_name(ack_header.pkt_type),
|
|
225
|
+
addr=f"{addr[0]}:{addr[1]}",
|
|
226
|
+
device=device.state.serial,
|
|
227
|
+
)
|
|
228
|
+
)
|
|
229
|
+
|
|
182
230
|
async def _process_device_packet(
|
|
183
231
|
self,
|
|
184
232
|
device: EmulatedLifxDevice,
|
|
@@ -194,10 +242,13 @@ class EmulatedLifxServer:
|
|
|
194
242
|
packet: Parsed packet payload (or None)
|
|
195
243
|
addr: Client address (host, port)
|
|
196
244
|
"""
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
# Get resolved scenario for response delays
|
|
245
|
+
# Send ack immediately before device processing when no scenario
|
|
246
|
+
# targets ack behavior (fast path for the common case)
|
|
200
247
|
scenario = device._get_resolved_scenario()
|
|
248
|
+
if header.ack_required and not scenario.affects_acks:
|
|
249
|
+
self._send_ack(device, header, addr)
|
|
250
|
+
|
|
251
|
+
responses = device.process_packet(header, packet)
|
|
201
252
|
|
|
202
253
|
# Send responses with delay if configured
|
|
203
254
|
for resp_header, resp_packet in responses:
|
|
@@ -1,8 +1,8 @@
|
|
|
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=
|
|
3
|
+
lifx_emulator/server.py,sha256=x-RDfxwmzUfSSROfBzVm2LcGK68sVIM4o7IkcgzkSfI,18363
|
|
4
4
|
lifx_emulator/devices/__init__.py,sha256=QlBTPnFErJcSKLvGyeDwemh7xcpjYvB_L5siKsjr3s8,1089
|
|
5
|
-
lifx_emulator/devices/device.py,sha256=
|
|
5
|
+
lifx_emulator/devices/device.py,sha256=w7Y9VYux3cyGus9mFmruFn6WYtoD7zwhzCL2xrICY-Q,17180
|
|
6
6
|
lifx_emulator/devices/manager.py,sha256=XDrT82um5sgNpNihLj5RsNvHqdVI1bK9YY2eBzWIcf0,8162
|
|
7
7
|
lifx_emulator/devices/observers.py,sha256=-KnUgFcKdhlNo7CNVstP-u0wU2W0JAGg055ZPV15Sj0,3874
|
|
8
8
|
lifx_emulator/devices/persistence.py,sha256=9Mhj46-xrweOmyzjORCi2jKIwa8XJWpQ5CgaKcw6U98,10513
|
|
@@ -19,9 +19,9 @@ lifx_emulator/handlers/__init__.py,sha256=3Hj1hRo3yL3E7GKwG9TaYh33ymk_N3bRiQ8nvq
|
|
|
19
19
|
lifx_emulator/handlers/base.py,sha256=0avCLXY_rNlw16PpJ5JrRCwXNE4uMpBqF3PfSfNJ0b8,1654
|
|
20
20
|
lifx_emulator/handlers/device_handlers.py,sha256=1AmslA4Ut6L7b3SfduDdvnQizTpzUB3KKWBXmp4WYLQ,9462
|
|
21
21
|
lifx_emulator/handlers/light_handlers.py,sha256=255aoiIjSIL63kbHQa6wqUpEwFzFFx7SG6P1nWM9jgU,17769
|
|
22
|
-
lifx_emulator/handlers/multizone_handlers.py,sha256=
|
|
22
|
+
lifx_emulator/handlers/multizone_handlers.py,sha256=ypv9G7od2bdQc7plRb38hyPyd02ugiMsinUHKCB2cdM,8094
|
|
23
23
|
lifx_emulator/handlers/registry.py,sha256=s1ht4PmPhXhAcwu1hoY4yW39wy3SPJBMY-9Uxd0FWuE,3292
|
|
24
|
-
lifx_emulator/handlers/tile_handlers.py,sha256=
|
|
24
|
+
lifx_emulator/handlers/tile_handlers.py,sha256=TRrXfq1L-1WR35dhYMv3_GE-8pDX9yrYDHZUGbBNlSE,17495
|
|
25
25
|
lifx_emulator/products/__init__.py,sha256=qcNop_kRYFF3zSjNemzQEgu3jPrIxfyQyLv9GsnaLEI,627
|
|
26
26
|
lifx_emulator/products/generator.py,sha256=fvrhw_b7shLCtEtUFxWF5VBEQAeSrsaiXxoGIP5Vn4g,34675
|
|
27
27
|
lifx_emulator/products/registry.py,sha256=1SZ3fXVFFL8jhKYIZBqwtIQDN3qL1Lvf86P3N1_Kdx8,47323
|
|
@@ -40,8 +40,8 @@ lifx_emulator/repositories/device_repository.py,sha256=KsXVg2sg7PGSTsK_PvDYeHHwE
|
|
|
40
40
|
lifx_emulator/repositories/storage_backend.py,sha256=wEgjhnBvAxl6aO1ZGL3ou0dW9P2hBPnK8jEE03sOlL4,3264
|
|
41
41
|
lifx_emulator/scenarios/__init__.py,sha256=CGjudoWvyysvFj2xej11N2cr3mYROGtRb9zVHcOHGrQ,665
|
|
42
42
|
lifx_emulator/scenarios/manager.py,sha256=1esxRdz74UynNk1wb86MGZ2ZFAuMzByuu74nRe3D-Og,11163
|
|
43
|
-
lifx_emulator/scenarios/models.py,sha256=
|
|
43
|
+
lifx_emulator/scenarios/models.py,sha256=1cX399JcTYVo29-8Rc4BwYPRty7sMR4fcn0njGfspZg,4504
|
|
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.3.0.dist-info/METADATA,sha256=JtFPPtM9n0gsUawt_SHBvR_d67384-wheXW8viWc5Vc,3217
|
|
46
|
+
lifx_emulator_core-3.3.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
47
|
+
lifx_emulator_core-3.3.0.dist-info/RECORD,,
|
|
File without changes
|