lifx-async 4.3.8__tar.gz → 4.4.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_async-4.3.8 → lifx_async-4.4.0}/.github/workflows/ci.yml +8 -6
- {lifx_async-4.3.8 → lifx_async-4.4.0}/.github/workflows/docs.yml +3 -3
- {lifx_async-4.3.8 → lifx_async-4.4.0}/CLAUDE.md +15 -26
- {lifx_async-4.3.8 → lifx_async-4.4.0}/PKG-INFO +1 -1
- {lifx_async-4.3.8 → lifx_async-4.4.0}/docs/changelog.md +16 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/pyproject.toml +8 -4
- {lifx_async-4.3.8 → lifx_async-4.4.0}/src/lifx/__init__.py +10 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/src/lifx/api.py +19 -23
- {lifx_async-4.3.8 → lifx_async-4.4.0}/src/lifx/const.py +2 -1
- {lifx_async-4.3.8 → lifx_async-4.4.0}/src/lifx/devices/__init__.py +12 -9
- {lifx_async-4.3.8 → lifx_async-4.4.0}/src/lifx/devices/base.py +590 -58
- {lifx_async-4.3.8 → lifx_async-4.4.0}/src/lifx/devices/hev.py +168 -8
- {lifx_async-4.3.8 → lifx_async-4.4.0}/src/lifx/devices/infrared.py +117 -4
- {lifx_async-4.3.8 → lifx_async-4.4.0}/src/lifx/devices/light.py +175 -10
- {lifx_async-4.3.8 → lifx_async-4.4.0}/src/lifx/devices/matrix.py +172 -14
- {lifx_async-4.3.8 → lifx_async-4.4.0}/src/lifx/devices/multizone.py +156 -21
- {lifx_async-4.3.8 → lifx_async-4.4.0}/src/lifx/network/connection.py +5 -6
- {lifx_async-4.3.8 → lifx_async-4.4.0}/src/lifx/protocol/generator.py +41 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/src/lifx/protocol/protocol_types.py +9 -4
- lifx_async-4.4.0/tests/conftest.py +535 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/tests/test_api/test_api_apply_theme.py +0 -3
- {lifx_async-4.3.8 → lifx_async-4.4.0}/tests/test_api/test_api_batch_errors.py +100 -77
- {lifx_async-4.3.8 → lifx_async-4.4.0}/tests/test_api/test_api_batch_operations.py +3 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/tests/test_api/test_api_discovery.py +60 -41
- {lifx_async-4.3.8 → lifx_async-4.4.0}/tests/test_api/test_api_organization.py +5 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/tests/test_color.py +4 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/tests/test_devices/test_base.py +35 -28
- {lifx_async-4.3.8 → lifx_async-4.4.0}/tests/test_devices/test_matrix.py +54 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/tests/test_devices/test_multizone.py +2 -2
- lifx_async-4.4.0/tests/test_devices/test_state_hev.py +281 -0
- lifx_async-4.4.0/tests/test_devices/test_state_infrared.py +199 -0
- lifx_async-4.4.0/tests/test_devices/test_state_light.py +528 -0
- lifx_async-4.4.0/tests/test_devices/test_state_management.py +1064 -0
- lifx_async-4.4.0/tests/test_devices/test_state_matrix.py +197 -0
- lifx_async-4.4.0/tests/test_devices/test_state_multizone.py +199 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/tests/test_effects/test_colorloop.py +187 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/tests/test_network/test_concurrent_requests.py +49 -30
- {lifx_async-4.3.8 → lifx_async-4.4.0}/tests/test_network/test_connection.py +1 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/tests/test_network/test_discovery_devices.py +12 -16
- {lifx_async-4.3.8 → lifx_async-4.4.0}/tests/test_network/test_discovery_errors.py +6 -4
- lifx_async-4.4.0/tests/test_network/test_transport.py +382 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/tests/test_protocol/test_serializer.py +224 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/tests/test_theme/test_apply_theme.py +3 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/tests/test_theme/test_canvas.py +64 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/uv.lock +11 -205
- lifx_async-4.3.8/tests/conftest.py +0 -470
- lifx_async-4.3.8/tests/test_network/test_transport.py +0 -67
- {lifx_async-4.3.8 → lifx_async-4.4.0}/.claude/settings.json +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/.github/dependabot.yml +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/.github/labeler.yml +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/.github/workflows/pr-automation.yml +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/.gitignore +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/.pre-commit-config.yaml +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/LICENSE +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/README.md +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/docs/api/colors.md +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/docs/api/devices.md +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/docs/api/effects.md +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/docs/api/exceptions.md +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/docs/api/high-level.md +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/docs/api/index.md +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/docs/api/network.md +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/docs/api/protocol.md +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/docs/api/themes.md +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/docs/architecture/effects-architecture.md +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/docs/architecture/overview.md +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/docs/faq.md +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/docs/getting-started/effects.md +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/docs/getting-started/installation.md +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/docs/getting-started/quickstart.md +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/docs/getting-started/themes.md +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/docs/index.md +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/docs/migration/effect-api-changes.md +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/docs/stylesheets/extra.css +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/docs/user-guide/advanced-usage.md +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/docs/user-guide/effects-custom.md +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/docs/user-guide/effects-troubleshooting.md +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/docs/user-guide/protocol-deep-dive.md +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/docs/user-guide/themes.md +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/docs/user-guide/troubleshooting.md +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/examples/01_simple_discovery.py +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/examples/02_simple_control.py +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/examples/03_waveforms.py +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/examples/04_logging.py +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/examples/06_pulse_effect.py +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/examples/07_colorloop_effect.py +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/examples/08_custom_effect.py +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/examples/09_background_effect.py +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/examples/10_find_specific_devices.py +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/examples/11_matrix_basic.py +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/examples/12_matrix_effects.py +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/examples/13_matrix_large.py +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/mkdocs.yml +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/renovate.json +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/src/lifx/color.py +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/src/lifx/effects/__init__.py +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/src/lifx/effects/base.py +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/src/lifx/effects/colorloop.py +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/src/lifx/effects/conductor.py +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/src/lifx/effects/const.py +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/src/lifx/effects/models.py +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/src/lifx/effects/pulse.py +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/src/lifx/effects/state_manager.py +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/src/lifx/exceptions.py +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/src/lifx/network/__init__.py +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/src/lifx/network/discovery.py +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/src/lifx/network/message.py +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/src/lifx/network/transport.py +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/src/lifx/products/__init__.py +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/src/lifx/products/generator.py +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/src/lifx/products/registry.py +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/src/lifx/protocol/__init__.py +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/src/lifx/protocol/base.py +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/src/lifx/protocol/header.py +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/src/lifx/protocol/models.py +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/src/lifx/protocol/packets.py +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/src/lifx/protocol/serializer.py +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/src/lifx/py.typed +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/src/lifx/theme/__init__.py +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/src/lifx/theme/canvas.py +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/src/lifx/theme/generators.py +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/src/lifx/theme/library.py +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/src/lifx/theme/theme.py +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/tests/__init__.py +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/tests/test_api/__init__.py +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/tests/test_devices/__init__.py +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/tests/test_devices/conftest.py +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/tests/test_devices/test_hev.py +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/tests/test_devices/test_infrared.py +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/tests/test_devices/test_light.py +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/tests/test_devices/test_mac_address.py +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/tests/test_effects/__init__.py +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/tests/test_effects/test_base.py +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/tests/test_effects/test_capability_filtering.py +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/tests/test_effects/test_integration.py +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/tests/test_effects/test_models.py +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/tests/test_effects/test_pulse.py +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/tests/test_effects/test_state_manager.py +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/tests/test_network/__init__.py +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/tests/test_network/test_message.py +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/tests/test_network/test_message_advanced.py +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/tests/test_products/test_product_generator.py +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/tests/test_products/test_registry.py +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/tests/test_protocol/test_generated.py +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/tests/test_protocol/test_header.py +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/tests/test_protocol/test_protocol_generator.py +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/tests/test_theme/__init__.py +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/tests/test_theme/conftest.py +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/tests/test_theme/test_generators.py +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/tests/test_theme/test_library.py +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/tests/test_theme/test_theme.py +0 -0
- {lifx_async-4.3.8 → lifx_async-4.4.0}/tests/test_utils.py +0 -0
|
@@ -35,7 +35,7 @@ jobs:
|
|
|
35
35
|
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
|
|
36
36
|
|
|
37
37
|
- name: Set up Python
|
|
38
|
-
uses: actions/setup-python@
|
|
38
|
+
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6
|
|
39
39
|
with:
|
|
40
40
|
python-version: ${{ env.PYTHON_VERSION }}
|
|
41
41
|
|
|
@@ -59,8 +59,8 @@ jobs:
|
|
|
59
59
|
|
|
60
60
|
- name: Check for security issues
|
|
61
61
|
run: |
|
|
62
|
-
uv pip install bandit
|
|
63
|
-
uv run bandit -r src/
|
|
62
|
+
uv pip install "bandit[toml]"
|
|
63
|
+
uv run bandit -c pyproject.toml -r src/
|
|
64
64
|
|
|
65
65
|
# Testing matrix across Python versions and platforms
|
|
66
66
|
test:
|
|
@@ -77,7 +77,7 @@ jobs:
|
|
|
77
77
|
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
|
|
78
78
|
|
|
79
79
|
- name: Set up Python ${{ matrix.python-version }}
|
|
80
|
-
uses: actions/setup-python@
|
|
80
|
+
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6
|
|
81
81
|
with:
|
|
82
82
|
python-version: ${{ matrix.python-version }}
|
|
83
83
|
|
|
@@ -95,6 +95,7 @@ jobs:
|
|
|
95
95
|
run: uv run --frozen pytest
|
|
96
96
|
|
|
97
97
|
- name: Upload coverage to Codecov
|
|
98
|
+
if: matrix.os == 'ubuntu-latest'
|
|
98
99
|
uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5
|
|
99
100
|
with:
|
|
100
101
|
env_vars: OS,PYTHON
|
|
@@ -105,6 +106,7 @@ jobs:
|
|
|
105
106
|
verbose: true
|
|
106
107
|
|
|
107
108
|
- name: Upload test results to Codecov
|
|
109
|
+
if: matrix.os == 'ubuntu-latest'
|
|
108
110
|
uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5
|
|
109
111
|
with:
|
|
110
112
|
env_vars: OS,PYTHON
|
|
@@ -127,7 +129,7 @@ jobs:
|
|
|
127
129
|
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
|
|
128
130
|
|
|
129
131
|
- name: Set up Python
|
|
130
|
-
uses: actions/setup-python@
|
|
132
|
+
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6
|
|
131
133
|
with:
|
|
132
134
|
python-version: ${{ env.PYTHON_VERSION }}
|
|
133
135
|
|
|
@@ -191,7 +193,7 @@ jobs:
|
|
|
191
193
|
ssh-key: ${{ secrets.DEPLOY_KEY }}
|
|
192
194
|
|
|
193
195
|
- name: Set up Python
|
|
194
|
-
uses: actions/setup-python@
|
|
196
|
+
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6
|
|
195
197
|
with:
|
|
196
198
|
python-version: ${{ env.PYTHON_VERSION }}
|
|
197
199
|
|
|
@@ -30,7 +30,7 @@ jobs:
|
|
|
30
30
|
fetch-depth: 0 # Fetch all history for git-revision-date-localized
|
|
31
31
|
|
|
32
32
|
- name: Set up Python
|
|
33
|
-
uses: actions/setup-python@
|
|
33
|
+
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6
|
|
34
34
|
with:
|
|
35
35
|
python-version: ${{ env.PYTHON_VERSION }}
|
|
36
36
|
|
|
@@ -64,7 +64,7 @@ jobs:
|
|
|
64
64
|
fetch-depth: 0
|
|
65
65
|
|
|
66
66
|
- name: Set up Python
|
|
67
|
-
uses: actions/setup-python@
|
|
67
|
+
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6
|
|
68
68
|
with:
|
|
69
69
|
python-version: ${{ env.PYTHON_VERSION }}
|
|
70
70
|
|
|
@@ -92,7 +92,7 @@ jobs:
|
|
|
92
92
|
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
|
|
93
93
|
|
|
94
94
|
- name: Set up Python
|
|
95
|
-
uses: actions/setup-python@
|
|
95
|
+
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6
|
|
96
96
|
with:
|
|
97
97
|
python-version: ${{ env.PYTHON_VERSION }}
|
|
98
98
|
|
|
@@ -12,7 +12,7 @@ structures from a YAML specification.
|
|
|
12
12
|
**Python Versions**: 3.11, 3.12, 3.13, 3.14 (tested on all versions via CI)
|
|
13
13
|
**Runtime Dependencies**: Zero - completely dependency-free!
|
|
14
14
|
**Async Framework**: Python's built-in `asyncio` (no external async library required)
|
|
15
|
-
**Test Isolation**: lifx-emulator runs
|
|
15
|
+
**Test Isolation**: lifx-emulator-core runs embedded in-process for fast, cross-platform testing
|
|
16
16
|
|
|
17
17
|
## Essential Commands
|
|
18
18
|
|
|
@@ -610,38 +610,27 @@ The `discover_devices()` function implements DoS protection through:
|
|
|
610
610
|
Test files mirror source structure: `tests/test_devices/test_light.py` tests
|
|
611
611
|
`src/lifx/devices/light.py`
|
|
612
612
|
|
|
613
|
-
### Integration Tests with lifx-emulator
|
|
613
|
+
### Integration Tests with lifx-emulator-core
|
|
614
614
|
|
|
615
|
-
Some tests require
|
|
616
|
-
The emulator runs
|
|
615
|
+
Some tests require `lifx-emulator-core` to run integration tests against real protocol implementations.
|
|
616
|
+
The emulator runs **embedded in-process** as a dev dependency, providing:
|
|
617
|
+
- Fast startup (~5-10ms vs 500ms+ for subprocess)
|
|
618
|
+
- Cross-platform support (Windows, macOS, Linux)
|
|
619
|
+
- Direct access to emulator internals for scenario testing
|
|
617
620
|
|
|
618
|
-
**Setup
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
cd ..
|
|
623
|
-
git clone https://github.com/Djelibeybi/lifx-emulator.git
|
|
624
|
-
cd lifx-emulator
|
|
625
|
-
uv sync
|
|
626
|
-
cd ../lifx
|
|
627
|
-
```
|
|
628
|
-
|
|
629
|
-
2. **System install**: Install lifx-emulator globally (requires Python 3.13+)
|
|
630
|
-
```bash
|
|
631
|
-
uv tool install lifx-emulator
|
|
632
|
-
```
|
|
621
|
+
**Setup**: The emulator is automatically installed as a dev dependency:
|
|
622
|
+
```bash
|
|
623
|
+
uv sync # Installs lifx-emulator-core automatically
|
|
624
|
+
```
|
|
633
625
|
|
|
634
626
|
**Running Integration Tests**:
|
|
627
|
+
- Tests marked with `@pytest.mark.emulator` use the embedded emulator
|
|
635
628
|
- If emulator is not available, these tests are automatically skipped
|
|
636
|
-
-
|
|
637
|
-
- **Works on all Python versions (3.11+)** since emulator runs as separate process
|
|
638
|
-
|
|
639
|
-
**Note**: The emulator itself requires Python 3.13+, but it runs as a subprocess so your
|
|
640
|
-
lifx tests can run on any supported Python version (3.11-3.14).
|
|
629
|
+
- **Works on all Python versions (3.11+)**
|
|
641
630
|
|
|
642
631
|
**External Emulator Management**:
|
|
643
632
|
|
|
644
|
-
For cases where you want to manage the emulator separately (or test against actual hardware)
|
|
633
|
+
For cases where you want to manage the emulator separately (or test against actual hardware):
|
|
645
634
|
|
|
646
635
|
```bash
|
|
647
636
|
# Use an externally managed emulator instance
|
|
@@ -654,7 +643,6 @@ LIFX_EMULATOR_EXTERNAL=1 pytest
|
|
|
654
643
|
This is useful when:
|
|
655
644
|
- Testing against actual LIFX hardware on your network
|
|
656
645
|
- Running the emulator with custom configuration or device setup
|
|
657
|
-
- Using a shared emulator instance across multiple test runs
|
|
658
646
|
- Debugging emulator behavior separately from the test suite
|
|
659
647
|
|
|
660
648
|
**Key Test Files:**
|
|
@@ -801,3 +789,4 @@ Critical constants are defined in `src/lifx/const.py`:
|
|
|
801
789
|
- Button/Relay/Switch devices are explicitly out of scope (library focuses on lighting devices)
|
|
802
790
|
- Not yet published to PyPI
|
|
803
791
|
- Never update docs/changelog.md manually as it is auto-generated during the release process by the CI/CD workflow.
|
|
792
|
+
- If a field is user-visible, it must never be bytes. This means things like serial, label, location and group must always be converted to a string prior to storing it anywhere a user would be able to access it. Conversion to and from bytes should happen either as close to sending or receiving the packet as possible.
|
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
<!-- version list -->
|
|
4
4
|
|
|
5
|
+
## v4.4.0 (2025-11-29)
|
|
6
|
+
|
|
7
|
+
### Features
|
|
8
|
+
|
|
9
|
+
- **devices**: Add factory pattern with automatic type detection and state management
|
|
10
|
+
([`4374248`](https://github.com/Djelibeybi/lifx-async/commit/4374248bb46cb5af1cf303866ad82b6692bb8932))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
## v4.3.9 (2025-11-27)
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
- **network**: Propagate timeout from request() to internal methods
|
|
18
|
+
([`b35ebea`](https://github.com/Djelibeybi/lifx-async/commit/b35ebea46120bfd4ad9ce149f5e25125d3694b30))
|
|
19
|
+
|
|
20
|
+
|
|
5
21
|
## v4.3.8 (2025-11-25)
|
|
6
22
|
|
|
7
23
|
### Bug Fixes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "lifx-async"
|
|
3
|
-
version = "4.
|
|
3
|
+
version = "4.4.0"
|
|
4
4
|
description = "A modern, type-safe, async Python library for controlling LIFX lights"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|
|
@@ -31,7 +31,7 @@ classifiers = [
|
|
|
31
31
|
[dependency-groups]
|
|
32
32
|
dev = [
|
|
33
33
|
"hatchling>=1.27.0",
|
|
34
|
-
"lifx-emulator>=
|
|
34
|
+
"lifx-emulator-core>=3.0.3",
|
|
35
35
|
"mkdocs-git-revision-date-localized-plugin>=1.4.7",
|
|
36
36
|
"mkdocs-llmstxt>=0.4.0",
|
|
37
37
|
"mkdocs-material>=9.6.22",
|
|
@@ -41,7 +41,6 @@ dev = [
|
|
|
41
41
|
"pytest-asyncio>=0.24.0",
|
|
42
42
|
"pytest-cov>=7.0.0",
|
|
43
43
|
"pytest-sugar>=1.1.1",
|
|
44
|
-
"pytest-xprocess>=1.0.2",
|
|
45
44
|
"pyyaml>=6.0.3",
|
|
46
45
|
"ruff>=0.14.2",
|
|
47
46
|
]
|
|
@@ -73,7 +72,6 @@ ignore = []
|
|
|
73
72
|
"src/lifx/protocol/packets.py" = ["E501"]
|
|
74
73
|
"src/lifx/products/registry.py" = ["E501"]
|
|
75
74
|
|
|
76
|
-
|
|
77
75
|
[tool.pyright]
|
|
78
76
|
typeCheckingMode = "standard"
|
|
79
77
|
pythonVersion = "3.11"
|
|
@@ -84,6 +82,9 @@ exclude = [
|
|
|
84
82
|
"**/registry.py", # Auto-generated from products.json
|
|
85
83
|
]
|
|
86
84
|
|
|
85
|
+
[tool.bandit]
|
|
86
|
+
skips = ["B101"]
|
|
87
|
+
|
|
87
88
|
[tool.pytest.ini_options]
|
|
88
89
|
testpaths = ["tests"]
|
|
89
90
|
pythonpath = ["src"]
|
|
@@ -102,6 +103,9 @@ addopts = """\
|
|
|
102
103
|
"""
|
|
103
104
|
asyncio_mode = "auto"
|
|
104
105
|
asyncio_default_fixture_loop_scope = "function"
|
|
106
|
+
markers = [
|
|
107
|
+
"emulator: tests that require the lifx-emulator-core embedded emulator",
|
|
108
|
+
]
|
|
105
109
|
|
|
106
110
|
[tool.coverage.run]
|
|
107
111
|
omit = [
|
|
@@ -21,12 +21,17 @@ from lifx.devices import (
|
|
|
21
21
|
DeviceVersion,
|
|
22
22
|
FirmwareInfo,
|
|
23
23
|
HevLight,
|
|
24
|
+
HevLightState,
|
|
24
25
|
InfraredLight,
|
|
26
|
+
InfraredLightState,
|
|
25
27
|
Light,
|
|
28
|
+
LightState,
|
|
26
29
|
MatrixEffect,
|
|
27
30
|
MatrixLight,
|
|
31
|
+
MatrixLightState,
|
|
28
32
|
MultiZoneEffect,
|
|
29
33
|
MultiZoneLight,
|
|
34
|
+
MultiZoneLightState,
|
|
30
35
|
TileInfo,
|
|
31
36
|
WifiInfo,
|
|
32
37
|
)
|
|
@@ -57,10 +62,15 @@ __all__ = [
|
|
|
57
62
|
# Core classes
|
|
58
63
|
"Device",
|
|
59
64
|
"Light",
|
|
65
|
+
"LightState",
|
|
60
66
|
"HevLight",
|
|
67
|
+
"HevLightState",
|
|
61
68
|
"InfraredLight",
|
|
69
|
+
"InfraredLightState",
|
|
62
70
|
"MultiZoneLight",
|
|
71
|
+
"MultiZoneLightState",
|
|
63
72
|
"MatrixLight",
|
|
73
|
+
"MatrixLightState",
|
|
64
74
|
# Color
|
|
65
75
|
"HSBK",
|
|
66
76
|
"Colors",
|
|
@@ -10,7 +10,6 @@ This module provides simplified interfaces for common operations:
|
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
12
|
import asyncio
|
|
13
|
-
import logging
|
|
14
13
|
from collections import defaultdict
|
|
15
14
|
from collections.abc import AsyncGenerator, Iterator, Sequence
|
|
16
15
|
from dataclasses import dataclass
|
|
@@ -27,12 +26,11 @@ from lifx.const import (
|
|
|
27
26
|
MAX_RESPONSE_TIME,
|
|
28
27
|
)
|
|
29
28
|
from lifx.devices import (
|
|
29
|
+
CollectionInfo,
|
|
30
30
|
Device,
|
|
31
|
-
GroupInfo,
|
|
32
31
|
HevLight,
|
|
33
32
|
InfraredLight,
|
|
34
33
|
Light,
|
|
35
|
-
LocationInfo,
|
|
36
34
|
MatrixLight,
|
|
37
35
|
MultiZoneLight,
|
|
38
36
|
)
|
|
@@ -44,14 +42,12 @@ from lifx.network.discovery import (
|
|
|
44
42
|
from lifx.protocol import packets
|
|
45
43
|
from lifx.theme import Theme
|
|
46
44
|
|
|
47
|
-
_LOGGER = logging.getLogger(__name__)
|
|
48
|
-
|
|
49
45
|
|
|
50
46
|
@dataclass
|
|
51
47
|
class LocationGrouping:
|
|
52
48
|
"""Organizational structure for location-based grouping."""
|
|
53
49
|
|
|
54
|
-
uuid:
|
|
50
|
+
uuid: str
|
|
55
51
|
label: str
|
|
56
52
|
devices: list[Device]
|
|
57
53
|
updated_at: int # Most recent updated_at from all devices
|
|
@@ -65,7 +61,7 @@ class LocationGrouping:
|
|
|
65
61
|
class GroupGrouping:
|
|
66
62
|
"""Organizational structure for group-based grouping."""
|
|
67
63
|
|
|
68
|
-
uuid:
|
|
64
|
+
uuid: str
|
|
69
65
|
label: str
|
|
70
66
|
devices: list[Device]
|
|
71
67
|
updated_at: int
|
|
@@ -117,8 +113,8 @@ class DeviceGroup:
|
|
|
117
113
|
self._matrix_lights = [light for light in devices if type(light) is MatrixLight]
|
|
118
114
|
self._locations_cache: dict[str, DeviceGroup] | None = None
|
|
119
115
|
self._groups_cache: dict[str, DeviceGroup] | None = None
|
|
120
|
-
self._location_metadata: dict[
|
|
121
|
-
self._group_metadata: dict[
|
|
116
|
+
self._location_metadata: dict[str, LocationGrouping] | None = None
|
|
117
|
+
self._group_metadata: dict[str, GroupGrouping] | None = None
|
|
122
118
|
|
|
123
119
|
async def __aenter__(self) -> DeviceGroup:
|
|
124
120
|
"""Enter async context manager."""
|
|
@@ -278,17 +274,17 @@ class DeviceGroup:
|
|
|
278
274
|
Skips devices with empty UUID (b'\\x00' * 16).
|
|
279
275
|
Logs warnings for failed queries but continues gracefully.
|
|
280
276
|
"""
|
|
281
|
-
location_data: dict[
|
|
277
|
+
location_data: dict[str, list[tuple[Device, CollectionInfo]]] = defaultdict(
|
|
282
278
|
list
|
|
283
279
|
)
|
|
284
280
|
|
|
285
281
|
# Fetch all location info concurrently
|
|
286
|
-
tasks: dict[str, asyncio.Task[
|
|
282
|
+
tasks: dict[str, asyncio.Task[CollectionInfo | None]] = {}
|
|
287
283
|
async with asyncio.TaskGroup() as tg:
|
|
288
284
|
for device in self._devices:
|
|
289
285
|
tasks[device.serial] = tg.create_task(device.get_location())
|
|
290
286
|
|
|
291
|
-
results: list[tuple[Device,
|
|
287
|
+
results: list[tuple[Device, CollectionInfo | None]] = []
|
|
292
288
|
for device in self._devices:
|
|
293
289
|
results.append((device, tasks[device.serial].result()))
|
|
294
290
|
|
|
@@ -298,10 +294,10 @@ class DeviceGroup:
|
|
|
298
294
|
continue
|
|
299
295
|
|
|
300
296
|
# Skip empty UUIDs (unassigned)
|
|
301
|
-
if location_info.
|
|
297
|
+
if location_info.uuid == "0000000000000000":
|
|
302
298
|
continue
|
|
303
299
|
|
|
304
|
-
location_data[location_info.
|
|
300
|
+
location_data[location_info.uuid].append((device, location_info))
|
|
305
301
|
|
|
306
302
|
# Build metadata dictionary with conflict resolution
|
|
307
303
|
self._location_metadata = {}
|
|
@@ -333,15 +329,15 @@ class DeviceGroup:
|
|
|
333
329
|
Logs warnings for failed queries but continues gracefully.
|
|
334
330
|
"""
|
|
335
331
|
# Collect group info from all devices concurrently
|
|
336
|
-
group_data: dict[
|
|
332
|
+
group_data: dict[str, list[tuple[Device, CollectionInfo]]] = defaultdict(list)
|
|
337
333
|
|
|
338
|
-
tasks: dict[str, asyncio.Task[
|
|
334
|
+
tasks: dict[str, asyncio.Task[CollectionInfo | None]] = {}
|
|
339
335
|
async with asyncio.TaskGroup() as tg:
|
|
340
336
|
for device in self._devices:
|
|
341
337
|
tasks[device.serial] = tg.create_task(device.get_group())
|
|
342
338
|
|
|
343
339
|
# Fetch all group info concurrently
|
|
344
|
-
results: list[tuple[Device,
|
|
340
|
+
results: list[tuple[Device, CollectionInfo | None]] = []
|
|
345
341
|
for device in self._devices:
|
|
346
342
|
results.append((device, tasks[device.serial].result()))
|
|
347
343
|
|
|
@@ -351,10 +347,10 @@ class DeviceGroup:
|
|
|
351
347
|
continue
|
|
352
348
|
|
|
353
349
|
# Skip empty UUIDs (unassigned)
|
|
354
|
-
if group_info.
|
|
350
|
+
if group_info.uuid == "0000000000000000":
|
|
355
351
|
continue
|
|
356
352
|
|
|
357
|
-
group_data[group_info.
|
|
353
|
+
group_data[group_info.uuid].append((device, group_info))
|
|
358
354
|
|
|
359
355
|
# Build metadata dictionary with conflict resolution
|
|
360
356
|
self._group_metadata = {}
|
|
@@ -397,7 +393,7 @@ class DeviceGroup:
|
|
|
397
393
|
)
|
|
398
394
|
|
|
399
395
|
result: dict[str, DeviceGroup] = {}
|
|
400
|
-
label_uuids: dict[str,
|
|
396
|
+
label_uuids: dict[str, str] = {}
|
|
401
397
|
|
|
402
398
|
for location_uuid, grouping in self._location_metadata.items():
|
|
403
399
|
label = grouping.label
|
|
@@ -405,7 +401,7 @@ class DeviceGroup:
|
|
|
405
401
|
# Handle naming conflicts: if two different UUIDs have the same label,
|
|
406
402
|
# append UUID suffix
|
|
407
403
|
if label in label_uuids and label_uuids[label] != location_uuid:
|
|
408
|
-
label = f"{label} ({location_uuid
|
|
404
|
+
label = f"{label} ({location_uuid[:8]})"
|
|
409
405
|
|
|
410
406
|
label_uuids[label] = location_uuid
|
|
411
407
|
result[label] = DeviceGroup(grouping.devices)
|
|
@@ -436,7 +432,7 @@ class DeviceGroup:
|
|
|
436
432
|
)
|
|
437
433
|
|
|
438
434
|
result: dict[str, DeviceGroup] = {}
|
|
439
|
-
label_uuids: dict[str,
|
|
435
|
+
label_uuids: dict[str, str] = {}
|
|
440
436
|
|
|
441
437
|
for group_uuid, grouping in self._group_metadata.items():
|
|
442
438
|
label = grouping.label
|
|
@@ -444,7 +440,7 @@ class DeviceGroup:
|
|
|
444
440
|
# Handle naming conflicts: if two different UUIDs have the same label,
|
|
445
441
|
# append UUID suffix
|
|
446
442
|
if label in label_uuids and label_uuids[label] != group_uuid:
|
|
447
|
-
label = f"{label} ({group_uuid
|
|
443
|
+
label = f"{label} ({group_uuid[:8]})"
|
|
448
444
|
|
|
449
445
|
label_uuids[label] = group_uuid
|
|
450
446
|
result[label] = DeviceGroup(grouping.devices)
|
|
@@ -32,7 +32,8 @@ MAX_RESPONSE_TIME: Final[float] = 1.0 # 1 second
|
|
|
32
32
|
IDLE_TIMEOUT_MULTIPLIER: Final[float] = 4.0 # 4 seconds (1.0 x 4.0)
|
|
33
33
|
|
|
34
34
|
# Default timeout for device requests in seconds
|
|
35
|
-
DEFAULT_REQUEST_TIMEOUT: Final[float] =
|
|
35
|
+
DEFAULT_REQUEST_TIMEOUT: Final[float] = 16.0
|
|
36
|
+
STATE_REFRESH_DEBOUNCE_MS: Final[int] = 300
|
|
36
37
|
|
|
37
38
|
# Default maximum number of retry attempts for failed requests
|
|
38
39
|
DEFAULT_MAX_RETRIES: Final[int] = 8
|
|
@@ -3,34 +3,37 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from lifx.devices.base import (
|
|
6
|
+
CollectionInfo,
|
|
6
7
|
Device,
|
|
7
8
|
DeviceInfo,
|
|
8
9
|
DeviceVersion,
|
|
9
10
|
FirmwareInfo,
|
|
10
|
-
GroupInfo,
|
|
11
|
-
LocationInfo,
|
|
12
11
|
WifiInfo,
|
|
13
12
|
)
|
|
14
|
-
from lifx.devices.hev import HevLight
|
|
15
|
-
from lifx.devices.infrared import InfraredLight
|
|
16
|
-
from lifx.devices.light import Light
|
|
17
|
-
from lifx.devices.matrix import MatrixEffect, MatrixLight, TileInfo
|
|
18
|
-
from lifx.devices.multizone import MultiZoneEffect, MultiZoneLight
|
|
13
|
+
from lifx.devices.hev import HevLight, HevLightState
|
|
14
|
+
from lifx.devices.infrared import InfraredLight, InfraredLightState
|
|
15
|
+
from lifx.devices.light import Light, LightState
|
|
16
|
+
from lifx.devices.matrix import MatrixEffect, MatrixLight, MatrixLightState, TileInfo
|
|
17
|
+
from lifx.devices.multizone import MultiZoneEffect, MultiZoneLight, MultiZoneLightState
|
|
19
18
|
|
|
20
19
|
__all__ = [
|
|
20
|
+
"CollectionInfo",
|
|
21
21
|
"Device",
|
|
22
22
|
"DeviceInfo",
|
|
23
23
|
"DeviceVersion",
|
|
24
24
|
"FirmwareInfo",
|
|
25
|
-
"GroupInfo",
|
|
26
25
|
"HevLight",
|
|
26
|
+
"HevLightState",
|
|
27
27
|
"InfraredLight",
|
|
28
|
+
"InfraredLightState",
|
|
28
29
|
"Light",
|
|
29
|
-
"
|
|
30
|
+
"LightState",
|
|
30
31
|
"MatrixEffect",
|
|
31
32
|
"MatrixLight",
|
|
33
|
+
"MatrixLightState",
|
|
32
34
|
"MultiZoneEffect",
|
|
33
35
|
"MultiZoneLight",
|
|
36
|
+
"MultiZoneLightState",
|
|
34
37
|
"TileInfo",
|
|
35
38
|
"WifiInfo",
|
|
36
39
|
]
|