lifx-async 4.3.9__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.9 → lifx_async-4.4.0}/.github/workflows/ci.yml +6 -6
- {lifx_async-4.3.9 → lifx_async-4.4.0}/.github/workflows/docs.yml +3 -3
- {lifx_async-4.3.9 → lifx_async-4.4.0}/CLAUDE.md +1 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/PKG-INFO +1 -1
- {lifx_async-4.3.9 → lifx_async-4.4.0}/docs/changelog.md +8 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/pyproject.toml +4 -2
- {lifx_async-4.3.9 → lifx_async-4.4.0}/src/lifx/__init__.py +10 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/src/lifx/api.py +19 -23
- {lifx_async-4.3.9 → lifx_async-4.4.0}/src/lifx/const.py +2 -1
- {lifx_async-4.3.9 → lifx_async-4.4.0}/src/lifx/devices/__init__.py +12 -9
- {lifx_async-4.3.9 → lifx_async-4.4.0}/src/lifx/devices/base.py +590 -58
- {lifx_async-4.3.9 → lifx_async-4.4.0}/src/lifx/devices/hev.py +168 -8
- {lifx_async-4.3.9 → lifx_async-4.4.0}/src/lifx/devices/infrared.py +117 -4
- {lifx_async-4.3.9 → lifx_async-4.4.0}/src/lifx/devices/light.py +175 -10
- {lifx_async-4.3.9 → lifx_async-4.4.0}/src/lifx/devices/matrix.py +172 -14
- {lifx_async-4.3.9 → lifx_async-4.4.0}/src/lifx/devices/multizone.py +156 -21
- {lifx_async-4.3.9 → lifx_async-4.4.0}/src/lifx/protocol/generator.py +41 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/src/lifx/protocol/protocol_types.py +9 -4
- {lifx_async-4.3.9 → lifx_async-4.4.0}/tests/conftest.py +5 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/tests/test_devices/test_base.py +9 -10
- {lifx_async-4.3.9 → lifx_async-4.4.0}/tests/test_devices/test_matrix.py +54 -0
- {lifx_async-4.3.9 → 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.9 → lifx_async-4.4.0}/uv.lock +1 -1
- {lifx_async-4.3.9 → lifx_async-4.4.0}/.claude/settings.json +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/.github/dependabot.yml +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/.github/labeler.yml +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/.github/workflows/pr-automation.yml +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/.gitignore +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/.pre-commit-config.yaml +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/LICENSE +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/README.md +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/docs/api/colors.md +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/docs/api/devices.md +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/docs/api/effects.md +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/docs/api/exceptions.md +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/docs/api/high-level.md +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/docs/api/index.md +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/docs/api/network.md +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/docs/api/protocol.md +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/docs/api/themes.md +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/docs/architecture/effects-architecture.md +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/docs/architecture/overview.md +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/docs/faq.md +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/docs/getting-started/effects.md +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/docs/getting-started/installation.md +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/docs/getting-started/quickstart.md +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/docs/getting-started/themes.md +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/docs/index.md +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/docs/migration/effect-api-changes.md +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/docs/stylesheets/extra.css +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/docs/user-guide/advanced-usage.md +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/docs/user-guide/effects-custom.md +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/docs/user-guide/effects-troubleshooting.md +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/docs/user-guide/protocol-deep-dive.md +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/docs/user-guide/themes.md +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/docs/user-guide/troubleshooting.md +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/examples/01_simple_discovery.py +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/examples/02_simple_control.py +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/examples/03_waveforms.py +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/examples/04_logging.py +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/examples/06_pulse_effect.py +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/examples/07_colorloop_effect.py +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/examples/08_custom_effect.py +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/examples/09_background_effect.py +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/examples/10_find_specific_devices.py +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/examples/11_matrix_basic.py +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/examples/12_matrix_effects.py +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/examples/13_matrix_large.py +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/mkdocs.yml +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/renovate.json +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/src/lifx/color.py +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/src/lifx/effects/__init__.py +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/src/lifx/effects/base.py +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/src/lifx/effects/colorloop.py +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/src/lifx/effects/conductor.py +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/src/lifx/effects/const.py +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/src/lifx/effects/models.py +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/src/lifx/effects/pulse.py +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/src/lifx/effects/state_manager.py +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/src/lifx/exceptions.py +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/src/lifx/network/__init__.py +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/src/lifx/network/connection.py +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/src/lifx/network/discovery.py +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/src/lifx/network/message.py +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/src/lifx/network/transport.py +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/src/lifx/products/__init__.py +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/src/lifx/products/generator.py +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/src/lifx/products/registry.py +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/src/lifx/protocol/__init__.py +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/src/lifx/protocol/base.py +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/src/lifx/protocol/header.py +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/src/lifx/protocol/models.py +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/src/lifx/protocol/packets.py +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/src/lifx/protocol/serializer.py +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/src/lifx/py.typed +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/src/lifx/theme/__init__.py +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/src/lifx/theme/canvas.py +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/src/lifx/theme/generators.py +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/src/lifx/theme/library.py +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/src/lifx/theme/theme.py +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/tests/__init__.py +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/tests/test_api/__init__.py +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/tests/test_api/test_api_apply_theme.py +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/tests/test_api/test_api_batch_errors.py +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/tests/test_api/test_api_batch_operations.py +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/tests/test_api/test_api_discovery.py +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/tests/test_api/test_api_organization.py +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/tests/test_color.py +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/tests/test_devices/__init__.py +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/tests/test_devices/conftest.py +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/tests/test_devices/test_hev.py +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/tests/test_devices/test_infrared.py +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/tests/test_devices/test_light.py +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/tests/test_devices/test_mac_address.py +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/tests/test_effects/__init__.py +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/tests/test_effects/test_base.py +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/tests/test_effects/test_capability_filtering.py +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/tests/test_effects/test_colorloop.py +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/tests/test_effects/test_integration.py +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/tests/test_effects/test_models.py +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/tests/test_effects/test_pulse.py +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/tests/test_effects/test_state_manager.py +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/tests/test_network/__init__.py +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/tests/test_network/test_concurrent_requests.py +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/tests/test_network/test_connection.py +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/tests/test_network/test_discovery_devices.py +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/tests/test_network/test_discovery_errors.py +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/tests/test_network/test_message.py +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/tests/test_network/test_message_advanced.py +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/tests/test_network/test_transport.py +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/tests/test_products/test_product_generator.py +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/tests/test_products/test_registry.py +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/tests/test_protocol/test_generated.py +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/tests/test_protocol/test_header.py +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/tests/test_protocol/test_protocol_generator.py +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/tests/test_protocol/test_serializer.py +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/tests/test_theme/__init__.py +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/tests/test_theme/conftest.py +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/tests/test_theme/test_apply_theme.py +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/tests/test_theme/test_canvas.py +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/tests/test_theme/test_generators.py +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/tests/test_theme/test_library.py +0 -0
- {lifx_async-4.3.9 → lifx_async-4.4.0}/tests/test_theme/test_theme.py +0 -0
- {lifx_async-4.3.9 → 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
|
|
|
@@ -129,7 +129,7 @@ jobs:
|
|
|
129
129
|
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
|
|
130
130
|
|
|
131
131
|
- name: Set up Python
|
|
132
|
-
uses: actions/setup-python@
|
|
132
|
+
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6
|
|
133
133
|
with:
|
|
134
134
|
python-version: ${{ env.PYTHON_VERSION }}
|
|
135
135
|
|
|
@@ -193,7 +193,7 @@ jobs:
|
|
|
193
193
|
ssh-key: ${{ secrets.DEPLOY_KEY }}
|
|
194
194
|
|
|
195
195
|
- name: Set up Python
|
|
196
|
-
uses: actions/setup-python@
|
|
196
|
+
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6
|
|
197
197
|
with:
|
|
198
198
|
python-version: ${{ env.PYTHON_VERSION }}
|
|
199
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
|
|
|
@@ -789,3 +789,4 @@ Critical constants are defined in `src/lifx/const.py`:
|
|
|
789
789
|
- Button/Relay/Switch devices are explicitly out of scope (library focuses on lighting devices)
|
|
790
790
|
- Not yet published to PyPI
|
|
791
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,14 @@
|
|
|
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
|
+
|
|
5
13
|
## v4.3.9 (2025-11-27)
|
|
6
14
|
|
|
7
15
|
### 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"
|
|
@@ -72,7 +72,6 @@ ignore = []
|
|
|
72
72
|
"src/lifx/protocol/packets.py" = ["E501"]
|
|
73
73
|
"src/lifx/products/registry.py" = ["E501"]
|
|
74
74
|
|
|
75
|
-
|
|
76
75
|
[tool.pyright]
|
|
77
76
|
typeCheckingMode = "standard"
|
|
78
77
|
pythonVersion = "3.11"
|
|
@@ -83,6 +82,9 @@ exclude = [
|
|
|
83
82
|
"**/registry.py", # Auto-generated from products.json
|
|
84
83
|
]
|
|
85
84
|
|
|
85
|
+
[tool.bandit]
|
|
86
|
+
skips = ["B101"]
|
|
87
|
+
|
|
86
88
|
[tool.pytest.ini_options]
|
|
87
89
|
testpaths = ["tests"]
|
|
88
90
|
pythonpath = ["src"]
|
|
@@ -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
|
]
|