lifx-emulator 2.0.0__tar.gz → 2.1.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.
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/.github/workflows/ci.yml +3 -3
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/.github/workflows/docs.yml +3 -3
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/PKG-INFO +1 -1
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/docs/changelog.md +13 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/mkdocs.yml +51 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/pyproject.toml +2 -1
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/src/lifx_emulator/devices/states.py +14 -1
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/src/lifx_emulator/handlers/tile_handlers.py +30 -10
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/src/lifx_emulator/protocol/base.py +60 -2
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/src/lifx_emulator/protocol/protocol_types.py +35 -62
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/src/lifx_emulator/server.py +4 -2
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/tests/test_tile_handlers_extended.py +13 -32
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/uv.lock +90 -4
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/.gitignore +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/.pre-commit-config.yaml +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/CLAUDE.md +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/LICENSE +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/README.md +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/docs/advanced/device-management-api.md +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/docs/advanced/index.md +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/docs/advanced/scenario-api.md +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/docs/advanced/scenarios.md +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/docs/advanced/storage.md +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/docs/api/device.md +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/docs/api/factories.md +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/docs/api/index.md +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/docs/api/products.md +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/docs/api/protocol.md +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/docs/api/server.md +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/docs/api/storage.md +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/docs/architecture/device-state.md +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/docs/architecture/index.md +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/docs/architecture/overview.md +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/docs/architecture/packet-flow.md +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/docs/architecture/protocol.md +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/docs/assets/favicon.png +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/docs/faq.md +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/docs/getting-started/cli.md +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/docs/getting-started/index.md +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/docs/getting-started/installation.md +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/docs/getting-started/quickstart.md +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/docs/guide/best-practices.md +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/docs/guide/device-types.md +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/docs/guide/index.md +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/docs/guide/integration-testing.md +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/docs/guide/products-and-specs.md +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/docs/guide/testing-scenarios.md +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/docs/guide/web-interface.md +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/docs/index.md +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/docs/reference/glossary.md +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/docs/reference/troubleshooting.md +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/docs/stylesheets/extra.css +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/docs/tutorials/01-first-device.md +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/docs/tutorials/02-basic.md +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/docs/tutorials/03-integration.md +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/docs/tutorials/04-advanced-scenarios.md +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/docs/tutorials/05-cicd.md +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/docs/tutorials/index.md +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/renovate.json +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/src/lifx_emulator/__init__.py +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/src/lifx_emulator/__main__.py +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/src/lifx_emulator/api/__init__.py +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/src/lifx_emulator/api/app.py +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/src/lifx_emulator/api/mappers/__init__.py +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/src/lifx_emulator/api/mappers/device_mapper.py +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/src/lifx_emulator/api/models.py +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/src/lifx_emulator/api/routers/__init__.py +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/src/lifx_emulator/api/routers/devices.py +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/src/lifx_emulator/api/routers/monitoring.py +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/src/lifx_emulator/api/routers/scenarios.py +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/src/lifx_emulator/api/services/__init__.py +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/src/lifx_emulator/api/services/device_service.py +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/src/lifx_emulator/api/templates/dashboard.html +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/src/lifx_emulator/constants.py +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/src/lifx_emulator/devices/__init__.py +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/src/lifx_emulator/devices/device.py +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/src/lifx_emulator/devices/manager.py +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/src/lifx_emulator/devices/observers.py +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/src/lifx_emulator/devices/persistence.py +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/src/lifx_emulator/devices/state_restorer.py +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/src/lifx_emulator/devices/state_serializer.py +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/src/lifx_emulator/factories/__init__.py +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/src/lifx_emulator/factories/builder.py +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/src/lifx_emulator/factories/default_config.py +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/src/lifx_emulator/factories/factory.py +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/src/lifx_emulator/factories/firmware_config.py +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/src/lifx_emulator/factories/serial_generator.py +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/src/lifx_emulator/handlers/__init__.py +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/src/lifx_emulator/handlers/base.py +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/src/lifx_emulator/handlers/device_handlers.py +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/src/lifx_emulator/handlers/light_handlers.py +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/src/lifx_emulator/handlers/multizone_handlers.py +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/src/lifx_emulator/handlers/registry.py +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/src/lifx_emulator/products/__init__.py +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/src/lifx_emulator/products/generator.py +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/src/lifx_emulator/products/registry.py +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/src/lifx_emulator/products/specs.py +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/src/lifx_emulator/products/specs.yml +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/src/lifx_emulator/protocol/__init__.py +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/src/lifx_emulator/protocol/const.py +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/src/lifx_emulator/protocol/generator.py +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/src/lifx_emulator/protocol/header.py +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/src/lifx_emulator/protocol/packets.py +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/src/lifx_emulator/protocol/serializer.py +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/src/lifx_emulator/repositories/__init__.py +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/src/lifx_emulator/repositories/device_repository.py +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/src/lifx_emulator/repositories/storage_backend.py +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/src/lifx_emulator/scenarios/__init__.py +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/src/lifx_emulator/scenarios/manager.py +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/src/lifx_emulator/scenarios/models.py +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/src/lifx_emulator/scenarios/persistence.py +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/tests/conftest.py +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/tests/test_api.py +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/tests/test_api_validation.py +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/tests/test_async_storage.py +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/tests/test_cli.py +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/tests/test_cli_validation.py +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/tests/test_device.py +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/tests/test_device_edge_cases.py +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/tests/test_device_handlers_extended.py +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/tests/test_device_manager.py +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/tests/test_handler_registry.py +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/tests/test_integration.py +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/tests/test_light_handlers_extended.py +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/tests/test_multizone_handlers_extended.py +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/tests/test_observers.py +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/tests/test_products_generator.py +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/tests/test_products_specs.py +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/tests/test_protocol_generator.py +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/tests/test_protocol_types_coverage.py +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/tests/test_repositories.py +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/tests/test_scenario_manager.py +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/tests/test_scenario_persistence.py +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/tests/test_serializer.py +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.0}/tests/test_server.py +0 -0
- {lifx_emulator-2.0.0 → lifx_emulator-2.1.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@
|
|
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@
|
|
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@
|
|
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@
|
|
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@
|
|
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@
|
|
100
|
+
uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7
|
|
101
101
|
with:
|
|
102
102
|
version: ${{ env.UV_VERSION }}
|
|
103
103
|
python-version: ${{ env.PYTHON_VERSION }}
|
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
<!-- version list -->
|
|
4
4
|
|
|
5
|
+
## v2.1.0 (2025-11-18)
|
|
6
|
+
|
|
7
|
+
### Documentation
|
|
8
|
+
|
|
9
|
+
- Add mkdocs-llmstxt to generate llms.txt and llms-full.txt
|
|
10
|
+
([`4ddea81`](https://github.com/Djelibeybi/lifx-emulator/commit/4ddea813cf269991857d4871554839b5447404ae))
|
|
11
|
+
|
|
12
|
+
### Features
|
|
13
|
+
|
|
14
|
+
- **protocol**: Add Sky Effect support and protocol quirks
|
|
15
|
+
([`09422ab`](https://github.com/Djelibeybi/lifx-emulator/commit/09422ab8ab200b555ff7308c37ba087ff2e848e3))
|
|
16
|
+
|
|
17
|
+
|
|
5
18
|
## v2.0.0 (2025-11-12)
|
|
6
19
|
|
|
7
20
|
### 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.
|
|
3
|
+
version = "2.1.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:
|
|
@@ -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 = device_state.tile_effect_type
|
|
245
|
+
if effect_type == 5: # SKY effect
|
|
246
|
+
sky_type = device_state.tile_effect_sky_type or 2 # Default to 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
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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,
|
|
@@ -285,10 +293,22 @@ class SetEffectHandler(PacketHandler):
|
|
|
285
293
|
)
|
|
286
294
|
device_state.tile_effect_palette_count = packet.settings.palette_count
|
|
287
295
|
|
|
296
|
+
# Save Sky effect parameters
|
|
297
|
+
device_state.tile_effect_sky_type = int(packet.settings.parameter.sky_type)
|
|
298
|
+
device_state.tile_effect_cloud_sat_min = (
|
|
299
|
+
packet.settings.parameter.cloud_saturation_min
|
|
300
|
+
)
|
|
301
|
+
device_state.tile_effect_cloud_sat_max = (
|
|
302
|
+
packet.settings.parameter.cloud_saturation_max
|
|
303
|
+
)
|
|
304
|
+
|
|
288
305
|
logger.info(
|
|
289
306
|
f"Tile effect set: type={packet.settings.type}, "
|
|
290
307
|
f"speed={packet.settings.speed}ms, "
|
|
291
|
-
f"palette_count={packet.settings.palette_count}"
|
|
308
|
+
f"palette_count={packet.settings.palette_count}, "
|
|
309
|
+
f"sky_type={packet.settings.parameter.sky_type}, "
|
|
310
|
+
f"cloud_sat=[{packet.settings.parameter.cloud_saturation_min}, "
|
|
311
|
+
f"{packet.settings.parameter.cloud_saturation_max}]"
|
|
292
312
|
)
|
|
293
313
|
|
|
294
314
|
if res_required:
|
|
@@ -10,10 +10,13 @@ Performance optimizations:
|
|
|
10
10
|
|
|
11
11
|
from __future__ import annotations
|
|
12
12
|
|
|
13
|
+
import logging
|
|
13
14
|
import re
|
|
14
|
-
from dataclasses import dataclass
|
|
15
|
+
from dataclasses import asdict, dataclass
|
|
15
16
|
from typing import Any, ClassVar
|
|
16
17
|
|
|
18
|
+
_LOGGER = logging.getLogger(__name__)
|
|
19
|
+
|
|
17
20
|
# Performance optimization: Pre-compiled regex patterns
|
|
18
21
|
_CAMEL_TO_SNAKE_PATTERN = re.compile(r"(?<!^)(?=[A-Z])")
|
|
19
22
|
_ARRAY_TYPE_PATTERN = re.compile(r"\[(\d+)\](.+)")
|
|
@@ -39,6 +42,11 @@ class Packet:
|
|
|
39
42
|
_fields: ClassVar[list[dict[str, Any]]]
|
|
40
43
|
_field_info: ClassVar[list[tuple[str, str, int]] | None] = None
|
|
41
44
|
|
|
45
|
+
@property
|
|
46
|
+
def as_dict(self) -> dict[str, Any]:
|
|
47
|
+
"""Return packet as dictionary."""
|
|
48
|
+
return asdict(self)
|
|
49
|
+
|
|
42
50
|
def pack(self) -> bytes:
|
|
43
51
|
"""Pack packet to bytes using field metadata.
|
|
44
52
|
|
|
@@ -76,9 +84,25 @@ class Packet:
|
|
|
76
84
|
offset: Offset in bytes to start unpacking
|
|
77
85
|
|
|
78
86
|
Returns:
|
|
79
|
-
Packet instance
|
|
87
|
+
Packet instance with label fields decoded to strings
|
|
80
88
|
"""
|
|
81
89
|
packet, _ = cls._unpack_internal(data, offset)
|
|
90
|
+
|
|
91
|
+
# Decode label fields from bytes to string in-place
|
|
92
|
+
# This ensures all State packets have human-readable labels
|
|
93
|
+
cls._decode_labels_inplace(packet)
|
|
94
|
+
|
|
95
|
+
# Log packet values after unpacking and decoding labels
|
|
96
|
+
packet_values = asdict(packet)
|
|
97
|
+
_LOGGER.debug(
|
|
98
|
+
{
|
|
99
|
+
"class": "Packet",
|
|
100
|
+
"method": "unpack",
|
|
101
|
+
"packet_type": type(packet).__name__,
|
|
102
|
+
"values": packet_values,
|
|
103
|
+
}
|
|
104
|
+
)
|
|
105
|
+
|
|
82
106
|
return packet
|
|
83
107
|
|
|
84
108
|
@classmethod
|
|
@@ -177,6 +201,9 @@ class Packet:
|
|
|
177
201
|
"MultiZoneApplicationRequest",
|
|
178
202
|
"MultiZoneEffectType",
|
|
179
203
|
"MultiZoneExtendedApplicationRequest",
|
|
204
|
+
"TileEffectSkyPalette",
|
|
205
|
+
"TileEffectSkyType",
|
|
206
|
+
"TileEffectType",
|
|
180
207
|
}
|
|
181
208
|
is_enum = is_nested and base_type in enum_types
|
|
182
209
|
|
|
@@ -320,6 +347,9 @@ class Packet:
|
|
|
320
347
|
MultiZoneApplicationRequest,
|
|
321
348
|
MultiZoneEffectType,
|
|
322
349
|
MultiZoneExtendedApplicationRequest,
|
|
350
|
+
TileEffectSkyPalette,
|
|
351
|
+
TileEffectSkyType,
|
|
352
|
+
TileEffectType,
|
|
323
353
|
)
|
|
324
354
|
|
|
325
355
|
base_type, array_count, is_nested = cls._parse_field_type(field_type)
|
|
@@ -331,6 +361,9 @@ class Packet:
|
|
|
331
361
|
"MultiZoneApplicationRequest": MultiZoneApplicationRequest,
|
|
332
362
|
"MultiZoneEffectType": MultiZoneEffectType,
|
|
333
363
|
"MultiZoneExtendedApplicationRequest": MultiZoneExtendedApplicationRequest,
|
|
364
|
+
"TileEffectSkyPalette": TileEffectSkyPalette,
|
|
365
|
+
"TileEffectSkyType": TileEffectSkyType,
|
|
366
|
+
"TileEffectType": TileEffectType,
|
|
334
367
|
}
|
|
335
368
|
|
|
336
369
|
if array_count:
|
|
@@ -386,3 +419,28 @@ class Packet:
|
|
|
386
419
|
# Cache the result
|
|
387
420
|
_FIELD_TYPE_CACHE[field_type] = result
|
|
388
421
|
return result
|
|
422
|
+
|
|
423
|
+
@staticmethod
|
|
424
|
+
def _decode_labels_inplace(packet: object) -> None:
|
|
425
|
+
"""Decode label fields from bytes to string in-place.
|
|
426
|
+
|
|
427
|
+
Automatically finds and decodes any field named 'label' or ending with '_label'
|
|
428
|
+
for all State packets. This ensures human-readable labels in all contexts.
|
|
429
|
+
|
|
430
|
+
Args:
|
|
431
|
+
packet: Packet instance to process (modified in-place)
|
|
432
|
+
"""
|
|
433
|
+
from dataclasses import fields, is_dataclass
|
|
434
|
+
|
|
435
|
+
if not is_dataclass(packet):
|
|
436
|
+
return
|
|
437
|
+
|
|
438
|
+
for field_info in fields(packet):
|
|
439
|
+
# Check if this looks like a label field
|
|
440
|
+
if field_info.name == "label" or field_info.name.endswith("_label"):
|
|
441
|
+
value = getattr(packet, field_info.name)
|
|
442
|
+
if isinstance(value, bytes):
|
|
443
|
+
# Decode: strip null terminator, decode UTF-8
|
|
444
|
+
decoded = value.rstrip(b"\x00").decode("utf-8")
|
|
445
|
+
# Use object.__setattr__ to bypass frozen dataclass if needed
|
|
446
|
+
object.__setattr__(packet, field_info.name, decoded)
|
|
@@ -505,16 +505,11 @@ class TileBufferRect:
|
|
|
505
505
|
|
|
506
506
|
@dataclass
|
|
507
507
|
class TileEffectParameter:
|
|
508
|
-
"""Auto-generated field structure."""
|
|
508
|
+
"""Auto-generated field structure for Sky effects."""
|
|
509
509
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
parameter3: int
|
|
514
|
-
parameter4: int
|
|
515
|
-
parameter5: int
|
|
516
|
-
parameter6: int
|
|
517
|
-
parameter7: int
|
|
510
|
+
sky_type: TileEffectSkyType
|
|
511
|
+
cloud_saturation_min: int
|
|
512
|
+
cloud_saturation_max: int
|
|
518
513
|
|
|
519
514
|
def pack(self) -> bytes:
|
|
520
515
|
"""Pack to bytes."""
|
|
@@ -522,22 +517,18 @@ class TileEffectParameter:
|
|
|
522
517
|
|
|
523
518
|
result = b""
|
|
524
519
|
|
|
525
|
-
#
|
|
526
|
-
result += serializer.pack_value(self.
|
|
527
|
-
#
|
|
528
|
-
result += serializer.
|
|
529
|
-
#
|
|
530
|
-
result += serializer.pack_value(self.
|
|
531
|
-
#
|
|
532
|
-
result += serializer.
|
|
533
|
-
#
|
|
534
|
-
result += serializer.pack_value(self.
|
|
535
|
-
#
|
|
536
|
-
result += serializer.
|
|
537
|
-
# parameter6: uint32
|
|
538
|
-
result += serializer.pack_value(self.parameter6, "uint32")
|
|
539
|
-
# parameter7: uint32
|
|
540
|
-
result += serializer.pack_value(self.parameter7, "uint32")
|
|
520
|
+
# sky_type: TileEffectSkyType (enum)
|
|
521
|
+
result += serializer.pack_value(int(self.sky_type), "uint8")
|
|
522
|
+
# Reserved 3 bytes
|
|
523
|
+
result += serializer.pack_reserved(3)
|
|
524
|
+
# cloud_saturation_min: uint8
|
|
525
|
+
result += serializer.pack_value(self.cloud_saturation_min, "uint8")
|
|
526
|
+
# Reserved 3 bytes
|
|
527
|
+
result += serializer.pack_reserved(3)
|
|
528
|
+
# cloud_saturation_max: uint8
|
|
529
|
+
result += serializer.pack_value(self.cloud_saturation_max, "uint8")
|
|
530
|
+
# Reserved 23 bytes
|
|
531
|
+
result += serializer.pack_reserved(23)
|
|
541
532
|
|
|
542
533
|
return result
|
|
543
534
|
|
|
@@ -547,49 +538,31 @@ class TileEffectParameter:
|
|
|
547
538
|
from lifx_emulator.protocol import serializer
|
|
548
539
|
|
|
549
540
|
current_offset = offset
|
|
550
|
-
#
|
|
551
|
-
|
|
552
|
-
data, "
|
|
553
|
-
)
|
|
554
|
-
# parameter1: uint32
|
|
555
|
-
parameter1, current_offset = serializer.unpack_value(
|
|
556
|
-
data, "uint32", current_offset
|
|
557
|
-
)
|
|
558
|
-
# parameter2: uint32
|
|
559
|
-
parameter2, current_offset = serializer.unpack_value(
|
|
560
|
-
data, "uint32", current_offset
|
|
561
|
-
)
|
|
562
|
-
# parameter3: uint32
|
|
563
|
-
parameter3, current_offset = serializer.unpack_value(
|
|
564
|
-
data, "uint32", current_offset
|
|
565
|
-
)
|
|
566
|
-
# parameter4: uint32
|
|
567
|
-
parameter4, current_offset = serializer.unpack_value(
|
|
568
|
-
data, "uint32", current_offset
|
|
569
|
-
)
|
|
570
|
-
# parameter5: uint32
|
|
571
|
-
parameter5, current_offset = serializer.unpack_value(
|
|
572
|
-
data, "uint32", current_offset
|
|
541
|
+
# sky_type: TileEffectSkyType (enum)
|
|
542
|
+
sky_type_raw, current_offset = serializer.unpack_value(
|
|
543
|
+
data, "uint8", current_offset
|
|
573
544
|
)
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
545
|
+
sky_type = TileEffectSkyType(sky_type_raw)
|
|
546
|
+
# Skip reserved 3 bytes
|
|
547
|
+
current_offset += 3
|
|
548
|
+
# cloud_saturation_min: uint8
|
|
549
|
+
cloud_saturation_min, current_offset = serializer.unpack_value(
|
|
550
|
+
data, "uint8", current_offset
|
|
577
551
|
)
|
|
578
|
-
#
|
|
579
|
-
|
|
580
|
-
|
|
552
|
+
# Skip reserved 3 bytes
|
|
553
|
+
current_offset += 3
|
|
554
|
+
# cloud_saturation_max: uint8
|
|
555
|
+
cloud_saturation_max, current_offset = serializer.unpack_value(
|
|
556
|
+
data, "uint8", current_offset
|
|
581
557
|
)
|
|
558
|
+
# Skip reserved 23 bytes
|
|
559
|
+
current_offset += 23
|
|
582
560
|
|
|
583
561
|
return (
|
|
584
562
|
cls(
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
parameter3=parameter3,
|
|
589
|
-
parameter4=parameter4,
|
|
590
|
-
parameter5=parameter5,
|
|
591
|
-
parameter6=parameter6,
|
|
592
|
-
parameter7=parameter7,
|
|
563
|
+
sky_type=sky_type,
|
|
564
|
+
cloud_saturation_min=cloud_saturation_min,
|
|
565
|
+
cloud_saturation_max=cloud_saturation_max,
|
|
593
566
|
),
|
|
594
567
|
current_offset,
|
|
595
568
|
)
|
|
@@ -217,7 +217,7 @@ class EmulatedLifxServer:
|
|
|
217
217
|
resp_packet_name = _get_packet_type_name(resp_header.pkt_type)
|
|
218
218
|
resp_fields_str = _format_packet_fields(resp_packet)
|
|
219
219
|
logger.debug(
|
|
220
|
-
"→ TX %s to %s:%s (
|
|
220
|
+
"→ TX %s to %s:%s (target=%s, seq=%s) [%s]",
|
|
221
221
|
resp_packet_name,
|
|
222
222
|
addr[0],
|
|
223
223
|
addr[1],
|
|
@@ -303,7 +303,9 @@ class EmulatedLifxServer:
|
|
|
303
303
|
|
|
304
304
|
# Log received packet with details
|
|
305
305
|
packet_name = _get_packet_type_name(header.pkt_type)
|
|
306
|
-
target_str =
|
|
306
|
+
target_str = (
|
|
307
|
+
"broadcast" if header.tagged else header.target.hex().rstrip("0000")
|
|
308
|
+
)
|
|
307
309
|
fields_str = _format_packet_fields(packet)
|
|
308
310
|
logger.debug(
|
|
309
311
|
"← RX %s from %s:%s (target=%s, seq=%s) [%s]",
|
|
@@ -7,6 +7,7 @@ from lifx_emulator.protocol.protocol_types import (
|
|
|
7
7
|
TileBufferRect,
|
|
8
8
|
TileEffectParameter,
|
|
9
9
|
TileEffectSettings,
|
|
10
|
+
TileEffectSkyType,
|
|
10
11
|
TileEffectType,
|
|
11
12
|
)
|
|
12
13
|
|
|
@@ -478,14 +479,9 @@ class TestTileEffects:
|
|
|
478
479
|
palette.append(LightHsbk(hue=0, saturation=0, brightness=0, kelvin=3500))
|
|
479
480
|
|
|
480
481
|
parameter = TileEffectParameter(
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
parameter3=0,
|
|
485
|
-
parameter4=0,
|
|
486
|
-
parameter5=0,
|
|
487
|
-
parameter6=0,
|
|
488
|
-
parameter7=0,
|
|
482
|
+
sky_type=TileEffectSkyType.SUNRISE,
|
|
483
|
+
cloud_saturation_min=0,
|
|
484
|
+
cloud_saturation_max=0,
|
|
489
485
|
)
|
|
490
486
|
|
|
491
487
|
settings = TileEffectSettings(
|
|
@@ -522,14 +518,9 @@ class TestTileEffects:
|
|
|
522
518
|
LightHsbk(hue=0, saturation=0, brightness=0, kelvin=3500) for _ in range(16)
|
|
523
519
|
]
|
|
524
520
|
parameter = TileEffectParameter(
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
parameter3=0,
|
|
529
|
-
parameter4=0,
|
|
530
|
-
parameter5=0,
|
|
531
|
-
parameter6=0,
|
|
532
|
-
parameter7=0,
|
|
521
|
+
sky_type=TileEffectSkyType.SUNRISE,
|
|
522
|
+
cloud_saturation_min=0,
|
|
523
|
+
cloud_saturation_max=0,
|
|
533
524
|
)
|
|
534
525
|
|
|
535
526
|
settings = TileEffectSettings(
|
|
@@ -584,14 +575,9 @@ class TestTileEffects:
|
|
|
584
575
|
LightHsbk(hue=0, saturation=0, brightness=0, kelvin=3500) for _ in range(16)
|
|
585
576
|
]
|
|
586
577
|
parameter = TileEffectParameter(
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
parameter3=0,
|
|
591
|
-
parameter4=0,
|
|
592
|
-
parameter5=0,
|
|
593
|
-
parameter6=0,
|
|
594
|
-
parameter7=0,
|
|
578
|
+
sky_type=TileEffectSkyType.SUNRISE,
|
|
579
|
+
cloud_saturation_min=0,
|
|
580
|
+
cloud_saturation_max=0,
|
|
595
581
|
)
|
|
596
582
|
settings = TileEffectSettings(
|
|
597
583
|
instanceid=1,
|
|
@@ -629,14 +615,9 @@ class TestTileEffects:
|
|
|
629
615
|
palette.append(LightHsbk(hue=0, saturation=0, brightness=0, kelvin=3500))
|
|
630
616
|
|
|
631
617
|
parameter = TileEffectParameter(
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
parameter3=0,
|
|
636
|
-
parameter4=0,
|
|
637
|
-
parameter5=0,
|
|
638
|
-
parameter6=0,
|
|
639
|
-
parameter7=0,
|
|
618
|
+
sky_type=TileEffectSkyType.SUNRISE,
|
|
619
|
+
cloud_saturation_min=0,
|
|
620
|
+
cloud_saturation_max=0,
|
|
640
621
|
)
|
|
641
622
|
|
|
642
623
|
settings = TileEffectSettings(
|