lifx-emulator 2.0.0__tar.gz → 2.2.0__tar.gz

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 (136) hide show
  1. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/.github/workflows/ci.yml +3 -3
  2. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/.github/workflows/docs.yml +3 -3
  3. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/PKG-INFO +1 -1
  4. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/docs/changelog.md +21 -0
  5. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/mkdocs.yml +51 -0
  6. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/pyproject.toml +2 -1
  7. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/src/lifx_emulator/devices/states.py +14 -1
  8. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/src/lifx_emulator/factories/builder.py +3 -1
  9. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/src/lifx_emulator/factories/firmware_config.py +19 -1
  10. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/src/lifx_emulator/handlers/tile_handlers.py +51 -10
  11. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/src/lifx_emulator/products/generator.py +36 -1
  12. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/src/lifx_emulator/products/specs.py +43 -0
  13. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/src/lifx_emulator/products/specs.yml +30 -5
  14. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/src/lifx_emulator/protocol/base.py +60 -2
  15. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/src/lifx_emulator/protocol/protocol_types.py +35 -62
  16. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/src/lifx_emulator/server.py +4 -2
  17. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/tests/test_products_specs.py +144 -0
  18. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/tests/test_tile_handlers_extended.py +270 -32
  19. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/uv.lock +90 -4
  20. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/.gitignore +0 -0
  21. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/.pre-commit-config.yaml +0 -0
  22. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/CLAUDE.md +0 -0
  23. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/LICENSE +0 -0
  24. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/README.md +0 -0
  25. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/docs/advanced/device-management-api.md +0 -0
  26. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/docs/advanced/index.md +0 -0
  27. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/docs/advanced/scenario-api.md +0 -0
  28. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/docs/advanced/scenarios.md +0 -0
  29. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/docs/advanced/storage.md +0 -0
  30. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/docs/api/device.md +0 -0
  31. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/docs/api/factories.md +0 -0
  32. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/docs/api/index.md +0 -0
  33. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/docs/api/products.md +0 -0
  34. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/docs/api/protocol.md +0 -0
  35. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/docs/api/server.md +0 -0
  36. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/docs/api/storage.md +0 -0
  37. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/docs/architecture/device-state.md +0 -0
  38. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/docs/architecture/index.md +0 -0
  39. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/docs/architecture/overview.md +0 -0
  40. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/docs/architecture/packet-flow.md +0 -0
  41. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/docs/architecture/protocol.md +0 -0
  42. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/docs/assets/favicon.png +0 -0
  43. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/docs/faq.md +0 -0
  44. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/docs/getting-started/cli.md +0 -0
  45. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/docs/getting-started/index.md +0 -0
  46. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/docs/getting-started/installation.md +0 -0
  47. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/docs/getting-started/quickstart.md +0 -0
  48. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/docs/guide/best-practices.md +0 -0
  49. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/docs/guide/device-types.md +0 -0
  50. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/docs/guide/index.md +0 -0
  51. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/docs/guide/integration-testing.md +0 -0
  52. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/docs/guide/products-and-specs.md +0 -0
  53. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/docs/guide/testing-scenarios.md +0 -0
  54. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/docs/guide/web-interface.md +0 -0
  55. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/docs/index.md +0 -0
  56. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/docs/reference/glossary.md +0 -0
  57. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/docs/reference/troubleshooting.md +0 -0
  58. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/docs/stylesheets/extra.css +0 -0
  59. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/docs/tutorials/01-first-device.md +0 -0
  60. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/docs/tutorials/02-basic.md +0 -0
  61. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/docs/tutorials/03-integration.md +0 -0
  62. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/docs/tutorials/04-advanced-scenarios.md +0 -0
  63. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/docs/tutorials/05-cicd.md +0 -0
  64. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/docs/tutorials/index.md +0 -0
  65. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/renovate.json +0 -0
  66. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/src/lifx_emulator/__init__.py +0 -0
  67. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/src/lifx_emulator/__main__.py +0 -0
  68. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/src/lifx_emulator/api/__init__.py +0 -0
  69. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/src/lifx_emulator/api/app.py +0 -0
  70. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/src/lifx_emulator/api/mappers/__init__.py +0 -0
  71. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/src/lifx_emulator/api/mappers/device_mapper.py +0 -0
  72. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/src/lifx_emulator/api/models.py +0 -0
  73. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/src/lifx_emulator/api/routers/__init__.py +0 -0
  74. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/src/lifx_emulator/api/routers/devices.py +0 -0
  75. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/src/lifx_emulator/api/routers/monitoring.py +0 -0
  76. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/src/lifx_emulator/api/routers/scenarios.py +0 -0
  77. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/src/lifx_emulator/api/services/__init__.py +0 -0
  78. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/src/lifx_emulator/api/services/device_service.py +0 -0
  79. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/src/lifx_emulator/api/templates/dashboard.html +0 -0
  80. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/src/lifx_emulator/constants.py +0 -0
  81. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/src/lifx_emulator/devices/__init__.py +0 -0
  82. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/src/lifx_emulator/devices/device.py +0 -0
  83. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/src/lifx_emulator/devices/manager.py +0 -0
  84. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/src/lifx_emulator/devices/observers.py +0 -0
  85. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/src/lifx_emulator/devices/persistence.py +0 -0
  86. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/src/lifx_emulator/devices/state_restorer.py +0 -0
  87. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/src/lifx_emulator/devices/state_serializer.py +0 -0
  88. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/src/lifx_emulator/factories/__init__.py +0 -0
  89. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/src/lifx_emulator/factories/default_config.py +0 -0
  90. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/src/lifx_emulator/factories/factory.py +0 -0
  91. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/src/lifx_emulator/factories/serial_generator.py +0 -0
  92. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/src/lifx_emulator/handlers/__init__.py +0 -0
  93. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/src/lifx_emulator/handlers/base.py +0 -0
  94. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/src/lifx_emulator/handlers/device_handlers.py +0 -0
  95. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/src/lifx_emulator/handlers/light_handlers.py +0 -0
  96. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/src/lifx_emulator/handlers/multizone_handlers.py +0 -0
  97. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/src/lifx_emulator/handlers/registry.py +0 -0
  98. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/src/lifx_emulator/products/__init__.py +0 -0
  99. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/src/lifx_emulator/products/registry.py +0 -0
  100. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/src/lifx_emulator/protocol/__init__.py +0 -0
  101. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/src/lifx_emulator/protocol/const.py +0 -0
  102. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/src/lifx_emulator/protocol/generator.py +0 -0
  103. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/src/lifx_emulator/protocol/header.py +0 -0
  104. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/src/lifx_emulator/protocol/packets.py +0 -0
  105. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/src/lifx_emulator/protocol/serializer.py +0 -0
  106. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/src/lifx_emulator/repositories/__init__.py +0 -0
  107. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/src/lifx_emulator/repositories/device_repository.py +0 -0
  108. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/src/lifx_emulator/repositories/storage_backend.py +0 -0
  109. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/src/lifx_emulator/scenarios/__init__.py +0 -0
  110. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/src/lifx_emulator/scenarios/manager.py +0 -0
  111. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/src/lifx_emulator/scenarios/models.py +0 -0
  112. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/src/lifx_emulator/scenarios/persistence.py +0 -0
  113. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/tests/conftest.py +0 -0
  114. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/tests/test_api.py +0 -0
  115. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/tests/test_api_validation.py +0 -0
  116. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/tests/test_async_storage.py +0 -0
  117. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/tests/test_cli.py +0 -0
  118. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/tests/test_cli_validation.py +0 -0
  119. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/tests/test_device.py +0 -0
  120. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/tests/test_device_edge_cases.py +0 -0
  121. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/tests/test_device_handlers_extended.py +0 -0
  122. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/tests/test_device_manager.py +0 -0
  123. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/tests/test_handler_registry.py +0 -0
  124. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/tests/test_integration.py +0 -0
  125. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/tests/test_light_handlers_extended.py +0 -0
  126. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/tests/test_multizone_handlers_extended.py +0 -0
  127. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/tests/test_observers.py +0 -0
  128. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/tests/test_products_generator.py +0 -0
  129. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/tests/test_protocol_generator.py +0 -0
  130. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/tests/test_protocol_types_coverage.py +0 -0
  131. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/tests/test_repositories.py +0 -0
  132. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/tests/test_scenario_manager.py +0 -0
  133. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/tests/test_scenario_persistence.py +0 -0
  134. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/tests/test_serializer.py +0 -0
  135. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/tests/test_server.py +0 -0
  136. {lifx_emulator-2.0.0 → lifx_emulator-2.2.0}/tests/test_state_restorer.py +0 -0
@@ -25,7 +25,7 @@ jobs:
25
25
  python-version: ${{ env.PYTHON_VERSION }}
26
26
 
27
27
  - name: Install uv
28
- uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7
28
+ uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7
29
29
  with:
30
30
  version: ${{ env.UV_VERSION }}
31
31
  python-version: ${{ env.PYTHON_VERSION }}
@@ -65,7 +65,7 @@ jobs:
65
65
  python-version: ${{ matrix.python-version }}
66
66
 
67
67
  - name: Install uv
68
- uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7
68
+ uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7
69
69
  with:
70
70
  version: ${{ env.UV_VERSION }}
71
71
  python-version: ${{ matrix.python-version }}
@@ -124,7 +124,7 @@ jobs:
124
124
  python-version: ${{ env.PYTHON_VERSION }}
125
125
 
126
126
  - name: Install uv
127
- uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7
127
+ uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7
128
128
  with:
129
129
  version: ${{ env.UV_VERSION }}
130
130
  python-version: ${{ env.PYTHON_VERSION }}
@@ -35,7 +35,7 @@ jobs:
35
35
  python-version: ${{ env.PYTHON_VERSION }}
36
36
 
37
37
  - name: Install uv
38
- uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7
38
+ uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7
39
39
  with:
40
40
  version: ${{ env.UV_VERSION }}
41
41
  python-version: ${{ env.PYTHON_VERSION }}
@@ -69,7 +69,7 @@ jobs:
69
69
  python-version: ${{ env.PYTHON_VERSION }}
70
70
 
71
71
  - name: Install uv
72
- uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7
72
+ uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7
73
73
  with:
74
74
  version: ${{ env.UV_VERSION }}
75
75
  python-version: ${{ env.PYTHON_VERSION }}
@@ -97,7 +97,7 @@ jobs:
97
97
  python-version: ${{ env.PYTHON_VERSION }}
98
98
 
99
99
  - name: Install uv
100
- uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7
100
+ uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7
101
101
  with:
102
102
  version: ${{ env.UV_VERSION }}
103
103
  python-version: ${{ env.PYTHON_VERSION }}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lifx-emulator
3
- Version: 2.0.0
3
+ Version: 2.2.0
4
4
  Summary: LIFX Emulator for testing LIFX LAN protocol libraries
5
5
  Author-email: Avi Miller <me@dje.li>
6
6
  Maintainer-email: Avi Miller <me@dje.li>
@@ -2,6 +2,27 @@
2
2
 
3
3
  <!-- version list -->
4
4
 
5
+ ## v2.2.0 (2025-11-18)
6
+
7
+ ### Features
8
+
9
+ - **products**: Add per-product firmware version defaults to specs.yml
10
+ ([`336692c`](https://github.com/Djelibeybi/lifx-emulator/commit/336692c381b94956ab29235d81e358fcb2c91089))
11
+
12
+
13
+ ## v2.1.0 (2025-11-18)
14
+
15
+ ### Documentation
16
+
17
+ - Add mkdocs-llmstxt to generate llms.txt and llms-full.txt
18
+ ([`4ddea81`](https://github.com/Djelibeybi/lifx-emulator/commit/4ddea813cf269991857d4871554839b5447404ae))
19
+
20
+ ### Features
21
+
22
+ - **protocol**: Add Sky Effect support and protocol quirks
23
+ ([`09422ab`](https://github.com/Djelibeybi/lifx-emulator/commit/09422ab8ab200b555ff7308c37ba087ff2e848e3))
24
+
25
+
5
26
  ## v2.0.0 (2025-11-12)
6
27
 
7
28
  ### Documentation
@@ -49,6 +49,57 @@ theme:
49
49
  plugins:
50
50
  - search:
51
51
  lang: en
52
+ - llmstxt:
53
+ markdown_description: |
54
+ A comprehensive LIFX device emulator for testing LIFX LAN protocol libraries.
55
+
56
+ This emulator implements the binary UDP protocol from https://lan.developer.lifx.com
57
+ and emulates various LIFX device types including color lights, multizone strips,
58
+ tiles, infrared, and HEV devices. It provides a complete testing environment with
59
+ persistent storage, HTTP management API, and advanced scenario testing capabilities
60
+ for simulating protocol edge cases, packet loss, delays, and malformed responses.
61
+ full_output: llms-full.txt
62
+ sections:
63
+ Getting Started:
64
+ - getting-started/index.md: Overview of getting started with the emulator
65
+ - getting-started/installation.md: Installation instructions
66
+ - getting-started/quickstart.md: Quick start guide
67
+ - getting-started/cli.md: Command-line interface usage
68
+ Tutorials:
69
+ - tutorials/index.md: Tutorial overview
70
+ - tutorials/01-first-device.md: Creating your first emulated device
71
+ - tutorials/02-basic.md: Basic usage patterns
72
+ - tutorials/03-integration.md: Integration testing
73
+ - tutorials/04-advanced-scenarios.md: Advanced testing scenarios
74
+ - tutorials/05-cicd.md: CI/CD integration
75
+ User Guide:
76
+ - guide/index.md: User guide overview
77
+ - guide/device-types.md: Available device types
78
+ - guide/products-and-specs.md: Product registry and specifications
79
+ - guide/web-interface.md: HTTP management API and web interface
80
+ - guide/testing-scenarios.md: Testing scenario capabilities
81
+ - guide/best-practices.md: Best practices
82
+ - guide/integration-testing.md: Advanced testing patterns
83
+ API Reference:
84
+ - api/index.md: API reference overview
85
+ - api/factories.md: Device factory functions
86
+ - api/device.md: Device API
87
+ - api/server.md: Server API
88
+ - api/protocol.md: Protocol types
89
+ - api/storage.md: Storage and persistence
90
+ - api/products.md: Product registry
91
+ Advanced Topics:
92
+ - advanced/index.md: Advanced topics overview
93
+ - advanced/storage.md: Persistent storage
94
+ - advanced/device-management-api.md: Device management API
95
+ - advanced/scenarios.md: Custom scenarios
96
+ - advanced/scenario-api.md: Scenario management API
97
+ Architecture:
98
+ - architecture/index.md: Architecture overview
99
+ - architecture/overview.md: System architecture
100
+ - architecture/packet-flow.md: Packet flow and processing
101
+ - architecture/protocol.md: Protocol layer details
102
+ - architecture/device-state.md: Device state management
52
103
  - mkdocstrings:
53
104
  handlers:
54
105
  python:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "lifx-emulator"
3
- version = "2.0.0"
3
+ version = "2.2.0"
4
4
  description = "LIFX Emulator for testing LIFX LAN protocol libraries"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -51,6 +51,7 @@ dev = [
51
51
  "mkdocs-material>=9.6.0",
52
52
  "mkdocstrings[python]>=0.27.0",
53
53
  "mkdocs-git-revision-date-localized-plugin>=1.4.0",
54
+ "mkdocs-llmstxt>=0.3.0",
54
55
  ]
55
56
 
56
57
  [build-system]
@@ -90,10 +90,17 @@ class MatrixState:
90
90
  tile_devices: list[dict[str, Any]]
91
91
  tile_width: int
92
92
  tile_height: int
93
- effect_type: int = 0 # 0=OFF, 2=MORPH, 3=FLAME
93
+ effect_type: int = 0 # 0=OFF, 2=MORPH, 3=FLAME, 5=SKY
94
94
  effect_speed: int = 5 # Duration of one cycle in seconds
95
95
  effect_palette_count: int = 0
96
96
  effect_palette: list[LightHsbk] = field(default_factory=list)
97
+ effect_sky_type: int = 0 # 0=SUNRISE, 1=SUNSET, 2=CLOUDS (only when effect_type=5)
98
+ effect_cloud_sat_min: int = (
99
+ 0 # Min cloud saturation 0-200 (only when effect_type=5)
100
+ )
101
+ effect_cloud_sat_max: int = (
102
+ 0 # Max cloud saturation 0-200 (only when effect_type=5)
103
+ )
97
104
 
98
105
 
99
106
  @dataclass
@@ -205,6 +212,9 @@ class DeviceState:
205
212
  "tile_effect_speed": ("matrix", "effect_speed"),
206
213
  "tile_effect_palette_count": ("matrix", "effect_palette_count"),
207
214
  "tile_effect_palette": ("matrix", "effect_palette"),
215
+ "tile_effect_sky_type": ("matrix", "effect_sky_type"),
216
+ "tile_effect_cloud_sat_min": ("matrix", "effect_cloud_sat_min"),
217
+ "tile_effect_cloud_sat_max": ("matrix", "effect_cloud_sat_max"),
208
218
  }
209
219
 
210
220
  # Default values for optional state attributes when state object is None
@@ -227,6 +237,9 @@ class DeviceState:
227
237
  "tile_effect_speed": 0,
228
238
  "tile_effect_palette_count": 0,
229
239
  "tile_effect_palette": [],
240
+ "tile_effect_sky_type": 0,
241
+ "tile_effect_cloud_sat_min": 0,
242
+ "tile_effect_cloud_sat_max": 0,
230
243
  }
231
244
 
232
245
  def get_target_bytes(self) -> bytes:
@@ -211,7 +211,9 @@ class DeviceBuilder:
211
211
 
212
212
  # 3. Determine firmware version
213
213
  version_major, version_minor = self._firmware_config.get_firmware_version(
214
- extended_multizone=self._extended_multizone, override=self._firmware_version
214
+ product_id=self._product_info.pid,
215
+ extended_multizone=self._extended_multizone,
216
+ override=self._firmware_version,
215
217
  )
216
218
 
217
219
  # 4. Get default color
@@ -2,6 +2,8 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ from lifx_emulator.products.specs import get_default_firmware_version
6
+
5
7
 
6
8
  class FirmwareConfig:
7
9
  """Determines firmware versions for devices.
@@ -25,12 +27,19 @@ class FirmwareConfig:
25
27
 
26
28
  def get_firmware_version(
27
29
  self,
30
+ product_id: int | None = None,
28
31
  extended_multizone: bool | None = None,
29
32
  override: tuple[int, int] | None = None,
30
33
  ) -> tuple[int, int]:
31
- """Get firmware version based on extended multizone support.
34
+ """Get firmware version based on product specs or extended multizone support.
35
+
36
+ Precedence order:
37
+ 1. Explicit override parameter
38
+ 2. Product-specific default from specs.yml
39
+ 3. Extended multizone flag (3.70 for True/None, 2.60 for False)
32
40
 
33
41
  Args:
42
+ product_id: Optional product ID to check specs for defaults
34
43
  extended_multizone: Whether device supports extended multizone.
35
44
  None or True defaults to 3.70, False gives 2.60
36
45
  override: Optional explicit firmware version to use
@@ -46,11 +55,20 @@ class FirmwareConfig:
46
55
  (2, 60)
47
56
  >>> config.get_firmware_version(override=(4, 0))
48
57
  (4, 0)
58
+ >>> # With product_id, uses specs if defined
59
+ >>> config.get_firmware_version(product_id=27) # doctest: +SKIP
60
+ (3, 70)
49
61
  """
50
62
  # Explicit override takes precedence
51
63
  if override is not None:
52
64
  return override
53
65
 
66
+ # Check product-specific defaults from specs
67
+ if product_id is not None:
68
+ specs_version = get_default_firmware_version(product_id)
69
+ if specs_version is not None:
70
+ return specs_version
71
+
54
72
  # None or True defaults to extended (3.70)
55
73
  # Only explicit False gives legacy (2.60)
56
74
  if extended_multizone is False:
@@ -237,16 +237,24 @@ class GetEffectHandler(PacketHandler):
237
237
  while len(palette) < 16:
238
238
  palette.append(LightHsbk(hue=0, saturation=0, brightness=0, kelvin=3500))
239
239
 
240
- # Create effect settings
240
+ # Create effect settings with Sky parameters
241
+ from lifx_emulator.protocol.protocol_types import TileEffectSkyType
242
+
243
+ # Use defaults for SKY effect (type=5), otherwise use stored values
244
+ effect_type = TileEffectType(device_state.tile_effect_type)
245
+ if effect_type == TileEffectType.SKY:
246
+ sky_type = device_state.tile_effect_sky_type or TileEffectSkyType.CLOUDS
247
+ cloud_sat_min = device_state.tile_effect_cloud_sat_min or 50
248
+ cloud_sat_max = device_state.tile_effect_cloud_sat_max or 180
249
+ else:
250
+ sky_type = device_state.tile_effect_sky_type
251
+ cloud_sat_min = device_state.tile_effect_cloud_sat_min
252
+ cloud_sat_max = device_state.tile_effect_cloud_sat_max
253
+
241
254
  parameter = TileEffectParameter(
242
- parameter0=0,
243
- parameter1=0,
244
- parameter2=0,
245
- parameter3=0,
246
- parameter4=0,
247
- parameter5=0,
248
- parameter6=0,
249
- parameter7=0,
255
+ sky_type=TileEffectSkyType(sky_type),
256
+ cloud_saturation_min=cloud_sat_min,
257
+ cloud_saturation_max=cloud_sat_max,
250
258
  )
251
259
  settings = TileEffectSettings(
252
260
  instanceid=0,
@@ -276,6 +284,27 @@ class SetEffectHandler(PacketHandler):
276
284
  return []
277
285
 
278
286
  if packet:
287
+ # Sky effect is only supported on LIFX Ceiling devices (176, 177, 201, 202)
288
+ # running firmware 4.4 or higher
289
+ if packet.settings.type == TileEffectType.SKY:
290
+ ceiling_product_ids = {176, 177, 201, 202}
291
+ is_ceiling = device_state.product in ceiling_product_ids
292
+
293
+ # Check firmware version >= 4.4
294
+ firmware_supported = device_state.version_major > 4 or (
295
+ device_state.version_major == 4 and device_state.version_minor >= 4
296
+ )
297
+
298
+ if not (is_ceiling and firmware_supported):
299
+ logger.debug(
300
+ f"Ignoring SKY effect request: "
301
+ f"product={device_state.product}, "
302
+ f"firmware={device_state.version_major}."
303
+ f"{device_state.version_minor} "
304
+ f"(requires Ceiling product and firmware >= 4.4)"
305
+ )
306
+ return []
307
+
279
308
  device_state.tile_effect_type = int(packet.settings.type)
280
309
  device_state.tile_effect_speed = (
281
310
  packet.settings.speed // 1000
@@ -285,10 +314,22 @@ class SetEffectHandler(PacketHandler):
285
314
  )
286
315
  device_state.tile_effect_palette_count = packet.settings.palette_count
287
316
 
317
+ # Save Sky effect parameters
318
+ device_state.tile_effect_sky_type = int(packet.settings.parameter.sky_type)
319
+ device_state.tile_effect_cloud_sat_min = (
320
+ packet.settings.parameter.cloud_saturation_min
321
+ )
322
+ device_state.tile_effect_cloud_sat_max = (
323
+ packet.settings.parameter.cloud_saturation_max
324
+ )
325
+
288
326
  logger.info(
289
327
  f"Tile effect set: type={packet.settings.type}, "
290
328
  f"speed={packet.settings.speed}ms, "
291
- f"palette_count={packet.settings.palette_count}"
329
+ f"palette_count={packet.settings.palette_count}, "
330
+ f"sky_type={packet.settings.parameter.sky_type}, "
331
+ f"cloud_sat=[{packet.settings.parameter.cloud_saturation_min}, "
332
+ f"{packet.settings.parameter.cloud_saturation_max}]"
292
333
  )
293
334
 
294
335
  if res_required:
@@ -788,7 +788,7 @@ def _generate_yaml_header() -> list[str]:
788
788
  "#",
789
789
  "# This file contains product-specific details that are not available in the",
790
790
  "# upstream LIFX products.json catalog, such as default zone counts, tile",
791
- "# configurations, and other device-specific defaults.",
791
+ "# configurations, firmware versions, and other device-specific defaults.",
792
792
  "#",
793
793
  "# These values are used by the emulator to create realistic device",
794
794
  "# configurations when specific parameters are not provided by the user.",
@@ -809,8 +809,25 @@ def _generate_yaml_header() -> list[str]:
809
809
  "# tile_width: <number> # Width of each tile in pixels",
810
810
  "# tile_height: <number> # Height of each tile in pixels",
811
811
  "#",
812
+ "# # Host firmware version (optional, overrides auto firmware selection)",
813
+ "# default_firmware_major: <number> # Firmware major version (e.g., 3)",
814
+ "# default_firmware_minor: <number> # Firmware minor version (e.g., 70)",
815
+ "#",
812
816
  "# # Other device-specific defaults",
813
817
  '# notes: "<string>" # Notes about product',
818
+ "#",
819
+ "# Firmware Version Notes:",
820
+ "# ----------------------",
821
+ "# If default_firmware_major and default_firmware_minor are both specified ",
822
+ "# they will be used as the default firmware version when creating devices",
823
+ "# of that type. This overrides the automatic firmware version selection",
824
+ "# based on extended_multizone capability (which defaults to 3.70 for extended",
825
+ "# multizone or 2.60 for non-extended).",
826
+ "#",
827
+ "# Precedence order for firmware version:",
828
+ "# 1. Explicit firmware_version parameter to create_device()",
829
+ "# 2. Product-specific default from this specs.yml file",
830
+ "# 3. Automatic selection based on extended_multizone flag",
814
831
  "",
815
832
  "products:",
816
833
  ]
@@ -847,6 +864,15 @@ def _generate_multizone_section(
847
864
  lines.append(f" min_zone_count: {specs['min_zone_count']}")
848
865
  lines.append(f" max_zone_count: {specs['max_zone_count']}")
849
866
 
867
+ # Add firmware version if present
868
+ if "default_firmware_major" in specs and "default_firmware_minor" in specs:
869
+ lines.append(
870
+ f" default_firmware_major: {specs['default_firmware_major']}"
871
+ )
872
+ lines.append(
873
+ f" default_firmware_minor: {specs['default_firmware_minor']}"
874
+ )
875
+
850
876
  notes = specs.get("notes", "")
851
877
  if notes:
852
878
  notes_escaped = notes.replace('"', '\\"')
@@ -889,6 +915,15 @@ def _generate_matrix_section(
889
915
  lines.append(f" tile_width: {specs['tile_width']}")
890
916
  lines.append(f" tile_height: {specs['tile_height']}")
891
917
 
918
+ # Add firmware version if present
919
+ if "default_firmware_major" in specs and "default_firmware_minor" in specs:
920
+ lines.append(
921
+ f" default_firmware_major: {specs['default_firmware_major']}"
922
+ )
923
+ lines.append(
924
+ f" default_firmware_minor: {specs['default_firmware_minor']}"
925
+ )
926
+
892
927
  notes = specs.get("notes", "")
893
928
  if notes:
894
929
  notes_escaped = notes.replace('"', '\\"')
@@ -27,6 +27,8 @@ class ProductSpecs:
27
27
  max_tile_count: Maximum tiles supported
28
28
  tile_width: Width of each tile in pixels
29
29
  tile_height: Height of each tile in pixels
30
+ default_firmware_major: Default firmware major version
31
+ default_firmware_minor: Default firmware minor version
30
32
  notes: Human-readable notes about this product
31
33
  """
32
34
 
@@ -39,6 +41,8 @@ class ProductSpecs:
39
41
  max_tile_count: int | None = None
40
42
  tile_width: int | None = None
41
43
  tile_height: int | None = None
44
+ default_firmware_major: int | None = None
45
+ default_firmware_minor: int | None = None
42
46
  notes: str | None = None
43
47
 
44
48
  @property
@@ -51,6 +55,14 @@ class ProductSpecs:
51
55
  """Check if this product has matrix-specific specs."""
52
56
  return self.tile_width is not None or self.tile_height is not None
53
57
 
58
+ @property
59
+ def has_firmware_specs(self) -> bool:
60
+ """Check if this product has firmware version specs."""
61
+ return (
62
+ self.default_firmware_major is not None
63
+ and self.default_firmware_minor is not None
64
+ )
65
+
54
66
 
55
67
  class SpecsRegistry:
56
68
  """Registry of product specs loaded from specs.yml."""
@@ -93,6 +105,8 @@ class SpecsRegistry:
93
105
  max_tile_count=specs_data.get("max_tile_count"),
94
106
  tile_width=specs_data.get("tile_width"),
95
107
  tile_height=specs_data.get("tile_height"),
108
+ default_firmware_major=specs_data.get("default_firmware_major"),
109
+ default_firmware_minor=specs_data.get("default_firmware_minor"),
96
110
  notes=specs_data.get("notes"),
97
111
  )
98
112
 
@@ -169,6 +183,23 @@ class SpecsRegistry:
169
183
  return (specs.tile_width, specs.tile_height)
170
184
  return None
171
185
 
186
+ def get_default_firmware_version(self, product_id: int) -> tuple[int, int] | None:
187
+ """Get default firmware version for a product.
188
+
189
+ Args:
190
+ product_id: Product ID
191
+
192
+ Returns:
193
+ Tuple of (major, minor) if defined, None otherwise
194
+ """
195
+ specs = self.get_specs(product_id)
196
+ if specs and specs.has_firmware_specs:
197
+ # has_firmware_specs ensures both values are not None
198
+ assert specs.default_firmware_major is not None # nosec
199
+ assert specs.default_firmware_minor is not None # nosec
200
+ return (specs.default_firmware_major, specs.default_firmware_minor)
201
+ return None
202
+
172
203
  def __len__(self) -> int:
173
204
  """Get number of products with specs."""
174
205
  if not self._loaded:
@@ -239,3 +270,15 @@ def get_tile_dimensions(product_id: int) -> tuple[int, int] | None:
239
270
  Tuple of (width, height) if defined, None otherwise
240
271
  """
241
272
  return _specs_registry.get_tile_dimensions(product_id)
273
+
274
+
275
+ def get_default_firmware_version(product_id: int) -> tuple[int, int] | None:
276
+ """Get default firmware version for a product.
277
+
278
+ Args:
279
+ product_id: Product ID
280
+
281
+ Returns:
282
+ Tuple of (major, minor) if defined, None otherwise
283
+ """
284
+ return _specs_registry.get_default_firmware_version(product_id)
@@ -3,7 +3,7 @@
3
3
  #
4
4
  # This file contains product-specific details that are not available in the
5
5
  # upstream LIFX products.json catalog, such as default zone counts, tile
6
- # configurations, and other device-specific defaults.
6
+ # configurations, firmware versions, and other device-specific defaults.
7
7
  #
8
8
  # These values are used by the emulator to create realistic device
9
9
  # configurations when specific parameters are not provided by the user.
@@ -24,8 +24,25 @@
24
24
  # tile_width: <number> # Width of each tile in pixels
25
25
  # tile_height: <number> # Height of each tile in pixels
26
26
  #
27
+ # # Host firmware version (optional, overrides automatic firmware selection)
28
+ # default_firmware_major: <number> # Firmware major version (e.g., 3)
29
+ # default_firmware_minor: <number> # Firmware minor version (e.g., 70)
30
+ #
27
31
  # # Other device-specific defaults
28
32
  # notes: "<string>" # Notes about product
33
+ #
34
+ # Firmware Version Notes:
35
+ # ----------------------
36
+ # If default_firmware_major and default_firmware_minor are both specified for a
37
+ # product, they will be used as the default firmware version when creating devices
38
+ # of that product type. This overrides the automatic firmware version selection
39
+ # based on extended_multizone capability (which defaults to 3.70 for extended
40
+ # multizone or 2.60 for non-extended).
41
+ #
42
+ # Precedence order for firmware version:
43
+ # 1. Explicit firmware_version parameter to create_device()
44
+ # 2. Product-specific default from this specs.yml file
45
+ # 3. Automatic selection based on extended_multizone flag
29
46
 
30
47
  products:
31
48
  # ========================================
@@ -220,15 +237,19 @@ products:
220
237
  max_tile_count: 1
221
238
  tile_width: 8
222
239
  tile_height: 8
240
+ default_firmware_major: 4
241
+ default_firmware_minor: 10
223
242
  notes: 'LIFX Ceiling, 8x8 matrix, zones 1-63: downlight, zone 64: uplight'
224
243
 
225
- 177: # LIFX Tube Intl
244
+ 177: # LIFX Ceiling Intl
226
245
  default_tile_count: 1
227
246
  min_tile_count: 1
228
247
  max_tile_count: 1
229
- tile_width: 5
230
- tile_height: 11
231
- notes: LIFX Tube Intl
248
+ tile_width: 8
249
+ tile_height: 8
250
+ default_firmware_major: 4
251
+ default_firmware_minor: 10
252
+ notes: LIFX Ceiling Intl
232
253
 
233
254
  185: # LIFX Candle Color US
234
255
  default_tile_count: 1
@@ -252,6 +273,8 @@ products:
252
273
  max_tile_count: 1
253
274
  tile_width: 16
254
275
  tile_height: 8
276
+ default_firmware_major: 4
277
+ default_firmware_minor: 10
255
278
  notes: LIFX Ceiling 13x26" US
256
279
 
257
280
  202: # LIFX Ceiling 13x26" Intl
@@ -260,6 +283,8 @@ products:
260
283
  max_tile_count: 1
261
284
  tile_width: 16
262
285
  tile_height: 8
286
+ default_firmware_major: 4
287
+ default_firmware_minor: 10
263
288
  notes: LIFX Ceiling 13x26" Intl
264
289
 
265
290
  215: # LIFX Candle Color US