lifx-emulator 1.0.2__py3-none-any.whl → 2.1.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.
Files changed (58) hide show
  1. lifx_emulator/__init__.py +1 -1
  2. lifx_emulator/__main__.py +26 -51
  3. lifx_emulator/api/__init__.py +18 -0
  4. lifx_emulator/api/app.py +154 -0
  5. lifx_emulator/api/mappers/__init__.py +5 -0
  6. lifx_emulator/api/mappers/device_mapper.py +114 -0
  7. lifx_emulator/api/models.py +133 -0
  8. lifx_emulator/api/routers/__init__.py +11 -0
  9. lifx_emulator/api/routers/devices.py +130 -0
  10. lifx_emulator/api/routers/monitoring.py +52 -0
  11. lifx_emulator/api/routers/scenarios.py +247 -0
  12. lifx_emulator/api/services/__init__.py +8 -0
  13. lifx_emulator/api/services/device_service.py +198 -0
  14. lifx_emulator/{api.py → api/templates/dashboard.html} +0 -942
  15. lifx_emulator/devices/__init__.py +37 -0
  16. lifx_emulator/devices/device.py +333 -0
  17. lifx_emulator/devices/manager.py +256 -0
  18. lifx_emulator/{async_storage.py → devices/persistence.py} +3 -3
  19. lifx_emulator/{state_restorer.py → devices/state_restorer.py} +2 -2
  20. lifx_emulator/devices/states.py +346 -0
  21. lifx_emulator/factories/__init__.py +37 -0
  22. lifx_emulator/factories/builder.py +371 -0
  23. lifx_emulator/factories/default_config.py +158 -0
  24. lifx_emulator/factories/factory.py +221 -0
  25. lifx_emulator/factories/firmware_config.py +59 -0
  26. lifx_emulator/factories/serial_generator.py +82 -0
  27. lifx_emulator/handlers/base.py +1 -1
  28. lifx_emulator/handlers/device_handlers.py +10 -28
  29. lifx_emulator/handlers/light_handlers.py +5 -9
  30. lifx_emulator/handlers/multizone_handlers.py +1 -1
  31. lifx_emulator/handlers/tile_handlers.py +31 -11
  32. lifx_emulator/products/generator.py +389 -170
  33. lifx_emulator/products/registry.py +52 -40
  34. lifx_emulator/products/specs.py +12 -13
  35. lifx_emulator/protocol/base.py +175 -63
  36. lifx_emulator/protocol/generator.py +18 -5
  37. lifx_emulator/protocol/packets.py +7 -7
  38. lifx_emulator/protocol/protocol_types.py +35 -62
  39. lifx_emulator/repositories/__init__.py +22 -0
  40. lifx_emulator/repositories/device_repository.py +155 -0
  41. lifx_emulator/repositories/storage_backend.py +107 -0
  42. lifx_emulator/scenarios/__init__.py +22 -0
  43. lifx_emulator/{scenario_manager.py → scenarios/manager.py} +11 -91
  44. lifx_emulator/scenarios/models.py +112 -0
  45. lifx_emulator/{scenario_persistence.py → scenarios/persistence.py} +82 -47
  46. lifx_emulator/server.py +42 -66
  47. {lifx_emulator-1.0.2.dist-info → lifx_emulator-2.1.0.dist-info}/METADATA +1 -1
  48. lifx_emulator-2.1.0.dist-info/RECORD +62 -0
  49. lifx_emulator/device.py +0 -750
  50. lifx_emulator/device_states.py +0 -114
  51. lifx_emulator/factories.py +0 -380
  52. lifx_emulator/storage_protocol.py +0 -100
  53. lifx_emulator-1.0.2.dist-info/RECORD +0 -40
  54. /lifx_emulator/{observers.py → devices/observers.py} +0 -0
  55. /lifx_emulator/{state_serializer.py → devices/state_serializer.py} +0 -0
  56. {lifx_emulator-1.0.2.dist-info → lifx_emulator-2.1.0.dist-info}/WHEEL +0 -0
  57. {lifx_emulator-1.0.2.dist-info → lifx_emulator-2.1.0.dist-info}/entry_points.txt +0 -0
  58. {lifx_emulator-1.0.2.dist-info → lifx_emulator-2.1.0.dist-info}/licenses/LICENSE +0 -0
lifx_emulator/__init__.py CHANGED
@@ -6,7 +6,7 @@ Implements the binary UDP protocol documented at https://lan.developer.lifx.com
6
6
 
7
7
  from importlib.metadata import version as get_version
8
8
 
9
- from lifx_emulator.device import EmulatedLifxDevice
9
+ from lifx_emulator.devices import EmulatedLifxDevice
10
10
  from lifx_emulator.factories import (
11
11
  create_color_light,
12
12
  create_color_temperature_light,
lifx_emulator/__main__.py CHANGED
@@ -8,8 +8,12 @@ from typing import Annotated
8
8
  import cyclopts
9
9
  from rich.logging import RichHandler
10
10
 
11
- from lifx_emulator.async_storage import AsyncDeviceStorage
12
11
  from lifx_emulator.constants import LIFX_UDP_PORT
12
+ from lifx_emulator.devices import (
13
+ DEFAULT_STORAGE_DIR,
14
+ DeviceManager,
15
+ DevicePersistenceAsyncFile,
16
+ )
13
17
  from lifx_emulator.factories import (
14
18
  create_color_light,
15
19
  create_color_temperature_light,
@@ -19,7 +23,9 @@ from lifx_emulator.factories import (
19
23
  create_multizone_light,
20
24
  create_tile_device,
21
25
  )
22
- from lifx_emulator.products.registry import ProductInfo, get_registry
26
+ from lifx_emulator.products.registry import get_registry
27
+ from lifx_emulator.repositories import DeviceRepository
28
+ from lifx_emulator.scenarios import ScenarioPersistenceAsyncFile
23
29
  from lifx_emulator.server import EmulatedLifxServer
24
30
 
25
31
  app = cyclopts.App(
@@ -78,49 +84,6 @@ def _format_capabilities(device) -> str:
78
84
  return ", ".join(capabilities)
79
85
 
80
86
 
81
- def _format_product_capabilities(product: ProductInfo) -> str:
82
- """Format product capabilities as a human-readable string."""
83
- caps = []
84
-
85
- # Determine base light type
86
- if product.has_relays:
87
- # Devices with relays are switches, not lights
88
- caps.append("switch")
89
- elif product.has_color:
90
- caps.append("full color")
91
- else:
92
- # Check temperature range to determine white light type
93
- if product.temperature_range:
94
- if product.temperature_range.min != product.temperature_range.max:
95
- caps.append("color temperature")
96
- else:
97
- caps.append("brightness only")
98
- else:
99
- # No temperature range info, assume basic brightness
100
- caps.append("brightness only")
101
-
102
- # Add additional capabilities
103
- if product.has_infrared:
104
- caps.append("infrared")
105
- # Extended multizone is backwards compatible with multizone,
106
- # so only show multizone if extended multizone is not present
107
- if product.has_extended_multizone:
108
- caps.append("extended-multizone")
109
- elif product.has_multizone:
110
- caps.append("multizone")
111
- if product.has_matrix:
112
- caps.append("matrix")
113
- if product.has_hev:
114
- caps.append("HEV")
115
- if product.has_chain:
116
- caps.append("chain")
117
- if product.has_buttons and not product.has_relays:
118
- # Only show buttons if not already identified as switch
119
- caps.append("buttons")
120
-
121
- return ", ".join(caps) if caps else "unknown"
122
-
123
-
124
87
  @app.command
125
88
  def list_products(
126
89
  filter_type: str | None = None,
@@ -177,8 +140,7 @@ def list_products(
177
140
  print("─" * 4 + "─┼─" + "─" * 40 + "─┼─" + "─" * 40)
178
141
 
179
142
  for product in all_products:
180
- caps = _format_product_capabilities(product)
181
- print(f"{product.pid:>4} │ {product.name:<40} │ {caps}")
143
+ print(f"{product.pid:>4} {product.name:<40} │ {product.caps}")
182
144
 
183
145
  print()
184
146
  print("Use --product <PID> to emulate a specific product")
@@ -213,13 +175,11 @@ def clear_storage(
213
175
  """
214
176
  from pathlib import Path
215
177
 
216
- from lifx_emulator.async_storage import DEFAULT_STORAGE_DIR, AsyncDeviceStorage
217
-
218
178
  # Use default storage directory if not specified
219
179
  storage_path = Path(storage_dir) if storage_dir else DEFAULT_STORAGE_DIR
220
180
 
221
181
  # Create storage instance
222
- storage = AsyncDeviceStorage(storage_path)
182
+ storage = DevicePersistenceAsyncFile(storage_path)
223
183
 
224
184
  # List devices
225
185
  devices = storage.list_devices()
@@ -369,7 +329,7 @@ async def run(
369
329
  return False
370
330
 
371
331
  # Initialize storage if persistence is enabled
372
- storage = AsyncDeviceStorage() if persistent else None
332
+ storage = DevicePersistenceAsyncFile() if persistent else None
373
333
  if persistent and storage:
374
334
  logger.info("Persistent storage enabled at %s", storage.storage_dir)
375
335
 
@@ -531,14 +491,29 @@ async def run(
531
491
  caps = _format_capabilities(device)
532
492
  logger.info(" • %s (%s) - %s", label, serial, caps)
533
493
 
494
+ # Create device manager with repository
495
+ device_repository = DeviceRepository()
496
+ device_manager = DeviceManager(device_repository)
497
+
498
+ # Load scenarios from storage if persistence is enabled
499
+ scenario_manager = None
500
+ scenario_storage = None
501
+ if persistent_scenarios:
502
+ scenario_storage = ScenarioPersistenceAsyncFile()
503
+ scenario_manager = await scenario_storage.load()
504
+ logger.info("Loaded scenarios from persistent storage")
505
+
534
506
  # Start LIFX server
535
507
  server = EmulatedLifxServer(
536
508
  devices,
509
+ device_manager,
537
510
  bind,
538
511
  port,
539
512
  track_activity=api_activity if api else False,
540
513
  storage=storage,
514
+ scenario_manager=scenario_manager,
541
515
  persist_scenarios=persistent_scenarios,
516
+ scenario_storage=scenario_storage,
542
517
  )
543
518
  await server.start()
544
519
 
@@ -0,0 +1,18 @@
1
+ """FastAPI-based management API for LIFX emulator.
2
+
3
+ This package provides a comprehensive REST API for managing the LIFX emulator:
4
+ - Monitoring server statistics and activity
5
+ - Creating, listing, and deleting devices
6
+ - Managing test scenarios for protocol testing
7
+
8
+ The API is built with FastAPI and organized into routers for clean separation
9
+ of concerns.
10
+ """
11
+
12
+ # Import from new refactored structure
13
+ from lifx_emulator.api.app import create_api_app, run_api_server
14
+
15
+ # Note: HTML_UI remains in the old lifx_emulator/api.py file temporarily
16
+ # TODO: Phase 1.1d - extract HTML template to separate file
17
+
18
+ __all__ = ["create_api_app", "run_api_server"]
@@ -0,0 +1,154 @@
1
+ """FastAPI application factory for LIFX emulator management API.
2
+
3
+ This module creates the main FastAPI application by assembling routers for:
4
+ - Monitoring (server stats, activity)
5
+ - Devices (CRUD operations)
6
+ - Scenarios (test scenario management)
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import logging
12
+ from pathlib import Path
13
+ from typing import TYPE_CHECKING
14
+
15
+ from fastapi import FastAPI, Request
16
+ from fastapi.responses import HTMLResponse
17
+ from fastapi.templating import Jinja2Templates
18
+
19
+ if TYPE_CHECKING:
20
+ from lifx_emulator.server import EmulatedLifxServer
21
+
22
+ from lifx_emulator.api.routers.devices import create_devices_router
23
+ from lifx_emulator.api.routers.monitoring import create_monitoring_router
24
+ from lifx_emulator.api.routers.scenarios import create_scenarios_router
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+ # Template directory for web UI
29
+ TEMPLATES_DIR = Path(__file__).parent / "templates"
30
+ templates = Jinja2Templates(directory=str(TEMPLATES_DIR))
31
+
32
+
33
+ def create_api_app(server: EmulatedLifxServer) -> FastAPI:
34
+ """Create FastAPI application for emulator management.
35
+
36
+ This factory function assembles the complete API by:
37
+ 1. Creating the FastAPI app with metadata
38
+ 2. Including routers for monitoring, devices, and scenarios
39
+ 3. Serving the embedded web UI at the root endpoint
40
+
41
+ Args:
42
+ server: The LIFX emulator server instance
43
+
44
+ Returns:
45
+ Configured FastAPI application
46
+
47
+ Example:
48
+ >>> from lifx_emulator.server import EmulatedLifxServer
49
+ >>> server = EmulatedLifxServer(bind="127.0.0.1", port=56700)
50
+ >>> app = create_api_app(server)
51
+ >>> # Run with: uvicorn app:app --host 127.0.0.1 --port 8080
52
+ """
53
+ app = FastAPI(
54
+ title="LIFX Emulator API",
55
+ description="""
56
+ Runtime management and monitoring API for LIFX device emulator.
57
+
58
+ This API provides read-only monitoring of the emulator state and device management
59
+ capabilities (add/remove devices). Device state changes must be performed via the
60
+ LIFX LAN protocol.
61
+
62
+ ## Features
63
+ - Real-time server statistics and packet monitoring
64
+ - Device inspection and management
65
+ - Test scenario management for protocol testing
66
+ - Recent activity tracking
67
+ - OpenAPI 3.1.0 compliant schema
68
+
69
+ ## Architecture
70
+ The API is organized into three main routers:
71
+ - **Monitoring**: Server stats and activity logs
72
+ - **Devices**: Device CRUD operations
73
+ - **Scenarios**: Test scenario configuration
74
+ """,
75
+ version="1.0.0",
76
+ contact={
77
+ "name": "LIFX Emulator",
78
+ "url": "https://github.com/Djelibeybi/lifx-emulator",
79
+ },
80
+ license_info={
81
+ "name": "UPL-1.0",
82
+ "url": "https://opensource.org/licenses/UPL",
83
+ },
84
+ openapi_tags=[
85
+ {
86
+ "name": "monitoring",
87
+ "description": "Server statistics and activity monitoring",
88
+ },
89
+ {
90
+ "name": "devices",
91
+ "description": "Device management and inspection",
92
+ },
93
+ {
94
+ "name": "scenarios",
95
+ "description": "Test scenario management",
96
+ },
97
+ ],
98
+ )
99
+
100
+ @app.get("/", response_class=HTMLResponse, include_in_schema=False)
101
+ async def root(request: Request):
102
+ """Serve embedded web UI dashboard."""
103
+ return templates.TemplateResponse(request, "dashboard.html")
104
+
105
+ # Include routers with server dependency injection
106
+ monitoring_router = create_monitoring_router(server)
107
+ devices_router = create_devices_router(server)
108
+ scenarios_router = create_scenarios_router(server)
109
+
110
+ app.include_router(monitoring_router)
111
+ app.include_router(devices_router)
112
+ app.include_router(scenarios_router)
113
+
114
+ logger.info(
115
+ "API application created with 3 routers (monitoring, devices, scenarios)"
116
+ )
117
+
118
+ return app
119
+
120
+
121
+ async def run_api_server(
122
+ server: EmulatedLifxServer, host: str = "127.0.0.1", port: int = 8080
123
+ ):
124
+ """Run the FastAPI server with uvicorn.
125
+
126
+ Args:
127
+ server: The LIFX emulator server instance
128
+ host: Host to bind to (default: 127.0.0.1)
129
+ port: Port to bind to (default: 8080)
130
+
131
+ Example:
132
+ >>> import asyncio
133
+ >>> from lifx_emulator.server import EmulatedLifxServer
134
+ >>> server = EmulatedLifxServer(bind="127.0.0.1", port=56700)
135
+ >>> asyncio.run(run_api_server(server, host="0.0.0.0", port=8080))
136
+ """
137
+ import uvicorn
138
+
139
+ app = create_api_app(server)
140
+
141
+ config = uvicorn.Config(
142
+ app,
143
+ host=host,
144
+ port=port,
145
+ log_level="info",
146
+ access_log=True,
147
+ )
148
+ api_server = uvicorn.Server(config)
149
+
150
+ logger.info("Starting API server on http://%s:%s", host, port)
151
+ logger.info("OpenAPI docs available at http://%s:%s/docs", host, port)
152
+ logger.info("ReDoc docs available at http://%s:%s/redoc", host, port)
153
+
154
+ await api_server.serve()
@@ -0,0 +1,5 @@
1
+ """Mappers for converting domain models to API models."""
2
+
3
+ from lifx_emulator.api.mappers.device_mapper import DeviceMapper
4
+
5
+ __all__ = ["DeviceMapper"]
@@ -0,0 +1,114 @@
1
+ """Mapper for converting EmulatedLifxDevice to DeviceInfo API model."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ if TYPE_CHECKING:
8
+ from lifx_emulator.devices import EmulatedLifxDevice
9
+
10
+ from lifx_emulator.api.models import ColorHsbk, DeviceInfo
11
+
12
+
13
+ class DeviceMapper:
14
+ """Maps domain device models to API response models.
15
+
16
+ This mapper eliminates duplication across multiple API endpoints by
17
+ providing a single, consistent way to convert device state to API responses.
18
+
19
+ **Eliminates**: 150+ lines of duplicated code across list_devices(),
20
+ get_device(), and create_device() endpoints.
21
+ """
22
+
23
+ @staticmethod
24
+ def to_device_info(device: EmulatedLifxDevice) -> DeviceInfo:
25
+ """Convert an EmulatedLifxDevice to a DeviceInfo API model.
26
+
27
+ Args:
28
+ device: The emulated LIFX device to convert
29
+
30
+ Returns:
31
+ DeviceInfo model ready for API response
32
+
33
+ Example:
34
+ >>> device = create_color_light(serial="d073d5000001")
35
+ >>> info = DeviceMapper.to_device_info(device)
36
+ >>> info.serial
37
+ 'd073d5000001'
38
+ """
39
+ return DeviceInfo(
40
+ # Core identification
41
+ serial=device.state.serial,
42
+ label=device.state.label,
43
+ product=device.state.product,
44
+ vendor=device.state.vendor,
45
+ # Power state
46
+ power_level=device.state.power_level,
47
+ # Capability flags
48
+ has_color=device.state.has_color,
49
+ has_infrared=device.state.has_infrared,
50
+ has_multizone=device.state.has_multizone,
51
+ has_extended_multizone=device.state.has_extended_multizone,
52
+ has_matrix=device.state.has_matrix,
53
+ has_hev=device.state.has_hev,
54
+ # Zone/tile counts
55
+ zone_count=device.state.multizone.zone_count
56
+ if device.state.multizone is not None
57
+ else 0,
58
+ tile_count=device.state.matrix.tile_count
59
+ if device.state.matrix is not None
60
+ else 0,
61
+ # Color state (if applicable)
62
+ color=ColorHsbk(
63
+ hue=device.state.color.hue,
64
+ saturation=device.state.color.saturation,
65
+ brightness=device.state.color.brightness,
66
+ kelvin=device.state.color.kelvin,
67
+ )
68
+ if device.state.has_color
69
+ else None,
70
+ # Multizone colors (if applicable)
71
+ zone_colors=[
72
+ ColorHsbk(
73
+ hue=c.hue,
74
+ saturation=c.saturation,
75
+ brightness=c.brightness,
76
+ kelvin=c.kelvin,
77
+ )
78
+ for c in device.state.multizone.zone_colors
79
+ ]
80
+ if device.state.multizone is not None
81
+ else [],
82
+ # Matrix/tile devices (if applicable)
83
+ tile_devices=device.state.matrix.tile_devices
84
+ if device.state.matrix is not None
85
+ else [],
86
+ # Firmware/version metadata
87
+ version_major=device.state.version_major,
88
+ version_minor=device.state.version_minor,
89
+ build_timestamp=device.state.build_timestamp,
90
+ # Location/group metadata
91
+ group_label=device.state.group.group_label,
92
+ location_label=device.state.location.location_label,
93
+ # Runtime metadata
94
+ uptime_ns=device.state.uptime_ns,
95
+ wifi_signal=device.state.wifi_signal,
96
+ )
97
+
98
+ @staticmethod
99
+ def to_device_info_list(devices: list[EmulatedLifxDevice]) -> list[DeviceInfo]:
100
+ """Convert a list of EmulatedLifxDevice to DeviceInfo API models.
101
+
102
+ Args:
103
+ devices: List of emulated LIFX devices to convert
104
+
105
+ Returns:
106
+ List of DeviceInfo models ready for API response
107
+
108
+ Example:
109
+ >>> devices = [create_color_light(), create_multizone_light()]
110
+ >>> info_list = DeviceMapper.to_device_info_list(devices)
111
+ >>> len(info_list)
112
+ 2
113
+ """
114
+ return [DeviceMapper.to_device_info(device) for device in devices]
@@ -0,0 +1,133 @@
1
+ """Pydantic models for API requests and responses."""
2
+
3
+ from pydantic import BaseModel, Field, field_validator
4
+
5
+ # Import shared domain models
6
+ from lifx_emulator.scenarios import ScenarioConfig
7
+
8
+
9
+ class DeviceCreateRequest(BaseModel):
10
+ """Request to create a new device."""
11
+
12
+ product_id: int = Field(
13
+ ..., description="Product ID from LIFX registry", gt=0, lt=10000
14
+ )
15
+ serial: str | None = Field(
16
+ None,
17
+ description="Optional serial (auto-generated if not provided)",
18
+ min_length=12,
19
+ max_length=12,
20
+ )
21
+ zone_count: int | None = Field(
22
+ None, description="Number of zones for multizone devices", ge=0, le=1000
23
+ )
24
+ tile_count: int | None = Field(
25
+ None, description="Number of tiles for matrix devices", ge=0, le=100
26
+ )
27
+ tile_width: int | None = Field(
28
+ None, description="Width of each tile in pixels", ge=1, le=256
29
+ )
30
+ tile_height: int | None = Field(
31
+ None, description="Height of each tile in pixels", ge=1, le=256
32
+ )
33
+ firmware_major: int | None = Field(
34
+ None, description="Firmware major version", ge=0, le=255
35
+ )
36
+ firmware_minor: int | None = Field(
37
+ None, description="Firmware minor version", ge=0, le=255
38
+ )
39
+
40
+ @field_validator("serial")
41
+ @classmethod
42
+ def validate_serial_format(cls, v: str | None) -> str | None:
43
+ """Validate serial number format (12 hex characters)."""
44
+ if v is None:
45
+ return v
46
+ if len(v) != 12:
47
+ raise ValueError("Serial must be exactly 12 characters")
48
+ try:
49
+ # Validate it's valid hexadecimal by parsing as base-16 integer
50
+ int(v, 16)
51
+ except ValueError as e:
52
+ raise ValueError("Serial must be valid hexadecimal (0-9, a-f, A-F)") from e
53
+ return v.lower() # Normalize to lowercase
54
+
55
+
56
+ class ColorHsbk(BaseModel):
57
+ """HSBK color representation."""
58
+
59
+ hue: int = Field(..., ge=0, le=65535, description="Hue (0-65535)")
60
+ saturation: int = Field(..., ge=0, le=65535, description="Saturation (0-65535)")
61
+ brightness: int = Field(..., ge=0, le=65535, description="Brightness (0-65535)")
62
+ kelvin: int = Field(
63
+ ..., ge=1500, le=9000, description="Color temperature in Kelvin (1500-9000)"
64
+ )
65
+
66
+
67
+ class DeviceInfo(BaseModel):
68
+ """Device information response."""
69
+
70
+ serial: str
71
+ label: str
72
+ product: int
73
+ vendor: int
74
+ power_level: int
75
+ has_color: bool
76
+ has_infrared: bool
77
+ has_multizone: bool
78
+ has_extended_multizone: bool
79
+ has_matrix: bool
80
+ has_hev: bool
81
+ zone_count: int
82
+ tile_count: int
83
+ color: ColorHsbk | None = None
84
+ zone_colors: list[ColorHsbk] = Field(default_factory=list)
85
+ tile_devices: list[dict] = Field(default_factory=list)
86
+ # Metadata fields
87
+ version_major: int = 0
88
+ version_minor: int = 0
89
+ build_timestamp: int = 0
90
+ group_label: str = ""
91
+ location_label: str = ""
92
+ uptime_ns: int = 0
93
+ wifi_signal: float = 0.0
94
+
95
+
96
+ class ServerStats(BaseModel):
97
+ """Server statistics response."""
98
+
99
+ uptime_seconds: float
100
+ start_time: float
101
+ device_count: int
102
+ packets_received: int
103
+ packets_sent: int
104
+ packets_received_by_type: dict[int, int]
105
+ packets_sent_by_type: dict[int, int]
106
+ error_count: int
107
+ activity_enabled: bool
108
+
109
+
110
+ class ActivityEvent(BaseModel):
111
+ """Recent activity event."""
112
+
113
+ timestamp: float
114
+ direction: str
115
+ packet_type: int
116
+ packet_name: str
117
+ device: str | None = None
118
+ target: str | None = None
119
+ addr: str
120
+
121
+
122
+ class ScenarioResponse(BaseModel):
123
+ """Response model for scenario operations."""
124
+
125
+ scope: str = Field(
126
+ ..., description="Scope of the scenario (global, device, type, location, group)"
127
+ )
128
+ identifier: str | None = Field(
129
+ None, description="Identifier for the scope (serial, type name, etc.)"
130
+ )
131
+ scenario: ScenarioConfig | None = Field(
132
+ None, description="The scenario configuration (None if not set)"
133
+ )
@@ -0,0 +1,11 @@
1
+ """API routers for LIFX emulator endpoints."""
2
+
3
+ from lifx_emulator.api.routers.devices import create_devices_router
4
+ from lifx_emulator.api.routers.monitoring import create_monitoring_router
5
+ from lifx_emulator.api.routers.scenarios import create_scenarios_router
6
+
7
+ __all__ = [
8
+ "create_monitoring_router",
9
+ "create_devices_router",
10
+ "create_scenarios_router",
11
+ ]