lifx-async 4.3.5__tar.gz → 4.3.7__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.5 → lifx_async-4.3.7}/.github/workflows/ci.yml +10 -10
- {lifx_async-4.3.5 → lifx_async-4.3.7}/.github/workflows/docs.yml +6 -6
- {lifx_async-4.3.5 → lifx_async-4.3.7}/PKG-INFO +1 -1
- {lifx_async-4.3.5 → lifx_async-4.3.7}/docs/changelog.md +16 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/pyproject.toml +1 -1
- {lifx_async-4.3.5 → lifx_async-4.3.7}/src/lifx/devices/base.py +51 -6
- {lifx_async-4.3.5 → lifx_async-4.3.7}/src/lifx/devices/hev.py +12 -2
- {lifx_async-4.3.5 → lifx_async-4.3.7}/src/lifx/devices/infrared.py +5 -1
- {lifx_async-4.3.5 → lifx_async-4.3.7}/src/lifx/devices/light.py +18 -4
- {lifx_async-4.3.5 → lifx_async-4.3.7}/src/lifx/devices/matrix.py +18 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/src/lifx/devices/multizone.py +19 -5
- {lifx_async-4.3.5 → lifx_async-4.3.7}/src/lifx/network/connection.py +35 -34
- {lifx_async-4.3.5 → lifx_async-4.3.7}/tests/conftest.py +56 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/tests/test_devices/test_light.py +64 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/tests/test_network/test_connection.py +51 -3
- {lifx_async-4.3.5 → lifx_async-4.3.7}/uv.lock +1 -1
- {lifx_async-4.3.5 → lifx_async-4.3.7}/.claude/settings.json +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/.github/dependabot.yml +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/.github/labeler.yml +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/.github/workflows/pr-automation.yml +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/.gitignore +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/.pre-commit-config.yaml +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/CLAUDE.md +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/LICENSE +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/README.md +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/docs/api/colors.md +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/docs/api/devices.md +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/docs/api/effects.md +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/docs/api/exceptions.md +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/docs/api/high-level.md +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/docs/api/index.md +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/docs/api/network.md +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/docs/api/protocol.md +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/docs/api/themes.md +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/docs/architecture/effects-architecture.md +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/docs/architecture/overview.md +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/docs/faq.md +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/docs/getting-started/effects.md +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/docs/getting-started/installation.md +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/docs/getting-started/quickstart.md +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/docs/getting-started/themes.md +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/docs/index.md +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/docs/migration/effect-api-changes.md +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/docs/stylesheets/extra.css +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/docs/user-guide/advanced-usage.md +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/docs/user-guide/effects-custom.md +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/docs/user-guide/effects-troubleshooting.md +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/docs/user-guide/protocol-deep-dive.md +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/docs/user-guide/themes.md +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/docs/user-guide/troubleshooting.md +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/examples/01_simple_discovery.py +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/examples/02_simple_control.py +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/examples/03_waveforms.py +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/examples/04_logging.py +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/examples/06_pulse_effect.py +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/examples/07_colorloop_effect.py +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/examples/08_custom_effect.py +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/examples/09_background_effect.py +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/examples/10_find_specific_devices.py +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/examples/11_matrix_basic.py +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/examples/12_matrix_effects.py +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/examples/13_matrix_large.py +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/mkdocs.yml +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/renovate.json +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/src/lifx/__init__.py +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/src/lifx/api.py +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/src/lifx/color.py +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/src/lifx/const.py +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/src/lifx/devices/__init__.py +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/src/lifx/effects/__init__.py +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/src/lifx/effects/base.py +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/src/lifx/effects/colorloop.py +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/src/lifx/effects/conductor.py +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/src/lifx/effects/const.py +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/src/lifx/effects/models.py +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/src/lifx/effects/pulse.py +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/src/lifx/effects/state_manager.py +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/src/lifx/exceptions.py +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/src/lifx/network/__init__.py +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/src/lifx/network/discovery.py +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/src/lifx/network/message.py +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/src/lifx/network/transport.py +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/src/lifx/products/__init__.py +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/src/lifx/products/generator.py +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/src/lifx/products/registry.py +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/src/lifx/protocol/__init__.py +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/src/lifx/protocol/base.py +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/src/lifx/protocol/generator.py +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/src/lifx/protocol/header.py +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/src/lifx/protocol/models.py +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/src/lifx/protocol/packets.py +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/src/lifx/protocol/protocol_types.py +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/src/lifx/protocol/serializer.py +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/src/lifx/py.typed +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/src/lifx/theme/__init__.py +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/src/lifx/theme/canvas.py +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/src/lifx/theme/generators.py +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/src/lifx/theme/library.py +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/src/lifx/theme/theme.py +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/tests/__init__.py +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/tests/test_api/__init__.py +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/tests/test_api/test_api_apply_theme.py +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/tests/test_api/test_api_batch_errors.py +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/tests/test_api/test_api_batch_operations.py +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/tests/test_api/test_api_discovery.py +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/tests/test_api/test_api_organization.py +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/tests/test_color.py +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/tests/test_devices/__init__.py +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/tests/test_devices/conftest.py +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/tests/test_devices/test_base.py +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/tests/test_devices/test_hev.py +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/tests/test_devices/test_infrared.py +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/tests/test_devices/test_mac_address.py +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/tests/test_devices/test_matrix.py +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/tests/test_devices/test_multizone.py +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/tests/test_effects/__init__.py +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/tests/test_effects/test_base.py +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/tests/test_effects/test_capability_filtering.py +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/tests/test_effects/test_colorloop.py +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/tests/test_effects/test_integration.py +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/tests/test_effects/test_models.py +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/tests/test_effects/test_pulse.py +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/tests/test_effects/test_state_manager.py +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/tests/test_network/__init__.py +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/tests/test_network/test_concurrent_requests.py +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/tests/test_network/test_discovery_devices.py +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/tests/test_network/test_discovery_errors.py +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/tests/test_network/test_message.py +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/tests/test_network/test_message_advanced.py +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/tests/test_network/test_transport.py +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/tests/test_products/test_product_generator.py +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/tests/test_products/test_registry.py +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/tests/test_protocol/test_generated.py +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/tests/test_protocol/test_header.py +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/tests/test_protocol/test_protocol_generator.py +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/tests/test_protocol/test_serializer.py +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/tests/test_theme/__init__.py +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/tests/test_theme/conftest.py +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/tests/test_theme/test_apply_theme.py +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/tests/test_theme/test_canvas.py +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/tests/test_theme/test_generators.py +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/tests/test_theme/test_library.py +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/tests/test_theme/test_theme.py +0 -0
- {lifx_async-4.3.5 → lifx_async-4.3.7}/tests/test_utils.py +0 -0
|
@@ -32,7 +32,7 @@ jobs:
|
|
|
32
32
|
name: Code Quality
|
|
33
33
|
runs-on: ubuntu-latest
|
|
34
34
|
steps:
|
|
35
|
-
- uses: actions/checkout@
|
|
35
|
+
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
|
|
36
36
|
|
|
37
37
|
- name: Set up Python
|
|
38
38
|
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6
|
|
@@ -40,7 +40,7 @@ jobs:
|
|
|
40
40
|
python-version: ${{ env.PYTHON_VERSION }}
|
|
41
41
|
|
|
42
42
|
- name: Install uv
|
|
43
|
-
uses: astral-sh/setup-uv@
|
|
43
|
+
uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7
|
|
44
44
|
with:
|
|
45
45
|
version: ${{ env.UV_VERSION }}
|
|
46
46
|
python-version: ${{ env.PYTHON_VERSION }}
|
|
@@ -74,7 +74,7 @@ jobs:
|
|
|
74
74
|
os: [ubuntu-latest, macos-latest, windows-latest]
|
|
75
75
|
python-version: ['3.11', '3.12', '3.13', '3.14']
|
|
76
76
|
steps:
|
|
77
|
-
- uses: actions/checkout@
|
|
77
|
+
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
|
|
78
78
|
|
|
79
79
|
- name: Set up Python ${{ matrix.python-version }}
|
|
80
80
|
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6
|
|
@@ -82,7 +82,7 @@ jobs:
|
|
|
82
82
|
python-version: ${{ matrix.python-version }}
|
|
83
83
|
|
|
84
84
|
- name: Install uv
|
|
85
|
-
uses: astral-sh/setup-uv@
|
|
85
|
+
uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7
|
|
86
86
|
with:
|
|
87
87
|
version: ${{ env.UV_VERSION }}
|
|
88
88
|
python-version: ${{ matrix.python-version }}
|
|
@@ -95,7 +95,7 @@ jobs:
|
|
|
95
95
|
run: uv run --frozen pytest
|
|
96
96
|
|
|
97
97
|
- name: Upload coverage to Codecov
|
|
98
|
-
uses: codecov/codecov-action@v5
|
|
98
|
+
uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5
|
|
99
99
|
with:
|
|
100
100
|
env_vars: OS,PYTHON
|
|
101
101
|
fail_ci_if_error: false
|
|
@@ -105,7 +105,7 @@ jobs:
|
|
|
105
105
|
verbose: true
|
|
106
106
|
|
|
107
107
|
- name: Upload test results to Codecov
|
|
108
|
-
uses: codecov/codecov-action@v5
|
|
108
|
+
uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5
|
|
109
109
|
with:
|
|
110
110
|
env_vars: OS,PYTHON
|
|
111
111
|
fail_ci_if_error: false
|
|
@@ -124,7 +124,7 @@ jobs:
|
|
|
124
124
|
if: github.event_name == 'push' && github.ref_name == 'main'
|
|
125
125
|
runs-on: ubuntu-latest
|
|
126
126
|
steps:
|
|
127
|
-
- uses: actions/checkout@
|
|
127
|
+
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
|
|
128
128
|
|
|
129
129
|
- name: Set up Python
|
|
130
130
|
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6
|
|
@@ -132,7 +132,7 @@ jobs:
|
|
|
132
132
|
python-version: ${{ env.PYTHON_VERSION }}
|
|
133
133
|
|
|
134
134
|
- name: Install uv
|
|
135
|
-
uses: astral-sh/setup-uv@
|
|
135
|
+
uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7
|
|
136
136
|
with:
|
|
137
137
|
version: ${{ env.UV_VERSION }}
|
|
138
138
|
python-version: ${{ env.PYTHON_VERSION }}
|
|
@@ -184,7 +184,7 @@ jobs:
|
|
|
184
184
|
|
|
185
185
|
steps:
|
|
186
186
|
- name: Checkout repository on release branch
|
|
187
|
-
uses: actions/checkout@
|
|
187
|
+
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
|
|
188
188
|
with:
|
|
189
189
|
ref: ${{ github.head_ref || github.ref_name }}
|
|
190
190
|
fetch-depth: 0
|
|
@@ -196,7 +196,7 @@ jobs:
|
|
|
196
196
|
python-version: ${{ env.PYTHON_VERSION }}
|
|
197
197
|
|
|
198
198
|
- name: Install uv
|
|
199
|
-
uses: astral-sh/setup-uv@
|
|
199
|
+
uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7
|
|
200
200
|
with:
|
|
201
201
|
version: ${{ env.UV_VERSION }}
|
|
202
202
|
python-version: ${{ env.PYTHON_VERSION }}
|
|
@@ -25,7 +25,7 @@ jobs:
|
|
|
25
25
|
build-docs:
|
|
26
26
|
runs-on: ubuntu-latest
|
|
27
27
|
steps:
|
|
28
|
-
- uses: actions/checkout@
|
|
28
|
+
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
|
|
29
29
|
with:
|
|
30
30
|
fetch-depth: 0 # Fetch all history for git-revision-date-localized
|
|
31
31
|
|
|
@@ -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@1e862dfacbd1d6d858c55d9b792c756523627244 # v7
|
|
39
39
|
with:
|
|
40
40
|
version: ${{ env.UV_VERSION }}
|
|
41
41
|
python-version: ${{ env.PYTHON_VERSION }}
|
|
@@ -59,7 +59,7 @@ jobs:
|
|
|
59
59
|
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
|
|
60
60
|
needs: build-docs
|
|
61
61
|
steps:
|
|
62
|
-
- uses: actions/checkout@
|
|
62
|
+
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
|
|
63
63
|
with:
|
|
64
64
|
fetch-depth: 0
|
|
65
65
|
|
|
@@ -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@1e862dfacbd1d6d858c55d9b792c756523627244 # v7
|
|
73
73
|
with:
|
|
74
74
|
version: ${{ env.UV_VERSION }}
|
|
75
75
|
python-version: ${{ env.PYTHON_VERSION }}
|
|
@@ -89,7 +89,7 @@ jobs:
|
|
|
89
89
|
validate-links:
|
|
90
90
|
runs-on: ubuntu-latest
|
|
91
91
|
steps:
|
|
92
|
-
- uses: actions/checkout@
|
|
92
|
+
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
|
|
93
93
|
|
|
94
94
|
- name: Set up Python
|
|
95
95
|
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6
|
|
@@ -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@1e862dfacbd1d6d858c55d9b792c756523627244 # v7
|
|
101
101
|
with:
|
|
102
102
|
version: ${{ env.UV_VERSION }}
|
|
103
103
|
python-version: ${{ env.PYTHON_VERSION }}
|
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
<!-- version list -->
|
|
4
4
|
|
|
5
|
+
## v4.3.7 (2025-11-25)
|
|
6
|
+
|
|
7
|
+
### Bug Fixes
|
|
8
|
+
|
|
9
|
+
- **devices**: Raise LifxUnsupportedCommandError on StateUnhandled responses
|
|
10
|
+
([`ec142cf`](https://github.com/Djelibeybi/lifx-async/commit/ec142cf0130847d65d4b9cd825575658936ef823))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
## v4.3.6 (2025-11-25)
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
- **network**: Return StateUnhandled packets instead of raising exception
|
|
18
|
+
([`f27e848`](https://github.com/Djelibeybi/lifx-async/commit/f27e84849656a84e7e120d66d1dba7bbabe18ed5))
|
|
19
|
+
|
|
20
|
+
|
|
5
21
|
## v4.3.5 (2025-11-22)
|
|
6
22
|
|
|
7
23
|
### Bug Fixes
|
|
@@ -19,7 +19,7 @@ from lifx.const import (
|
|
|
19
19
|
LIFX_LOCATION_NAMESPACE,
|
|
20
20
|
LIFX_UDP_PORT,
|
|
21
21
|
)
|
|
22
|
-
from lifx.exceptions import LifxDeviceNotFoundError
|
|
22
|
+
from lifx.exceptions import LifxDeviceNotFoundError, LifxUnsupportedCommandError
|
|
23
23
|
from lifx.network.connection import DeviceConnection
|
|
24
24
|
from lifx.products.registry import ProductInfo, get_product
|
|
25
25
|
from lifx.protocol import packets
|
|
@@ -152,6 +152,23 @@ class Device:
|
|
|
152
152
|
```
|
|
153
153
|
"""
|
|
154
154
|
|
|
155
|
+
@staticmethod
|
|
156
|
+
def _raise_if_unhandled(response: object) -> None:
|
|
157
|
+
"""Raise LifxUnsupportedCommandError if device doesn't support the command.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
response: The response from connection.request()
|
|
161
|
+
|
|
162
|
+
Raises:
|
|
163
|
+
LifxUnsupportedCommandError: If response is StateUnhandled or False
|
|
164
|
+
"""
|
|
165
|
+
if isinstance(response, packets.Device.StateUnhandled):
|
|
166
|
+
raise LifxUnsupportedCommandError(
|
|
167
|
+
f"Device does not support packet type {response.unhandled_type}"
|
|
168
|
+
)
|
|
169
|
+
if response is False:
|
|
170
|
+
raise LifxUnsupportedCommandError("Device does not support this command")
|
|
171
|
+
|
|
155
172
|
def __init__(
|
|
156
173
|
self,
|
|
157
174
|
serial: str,
|
|
@@ -456,6 +473,7 @@ class Device:
|
|
|
456
473
|
LifxDeviceNotFoundError: If device is not connected
|
|
457
474
|
LifxTimeoutError: If device does not respond
|
|
458
475
|
LifxProtocolError: If response is invalid
|
|
476
|
+
LifxUnsupportedCommandError: If device doesn't support this command
|
|
459
477
|
|
|
460
478
|
Example:
|
|
461
479
|
```python
|
|
@@ -469,6 +487,7 @@ class Device:
|
|
|
469
487
|
"""
|
|
470
488
|
# Request automatically unpacks and decodes label
|
|
471
489
|
state = await self.connection.request(packets.Device.GetLabel())
|
|
490
|
+
self._raise_if_unhandled(state)
|
|
472
491
|
|
|
473
492
|
# Store label
|
|
474
493
|
self._label = state.label
|
|
@@ -492,6 +511,7 @@ class Device:
|
|
|
492
511
|
ValueError: If label is too long
|
|
493
512
|
LifxDeviceNotFoundError: If device is not connected
|
|
494
513
|
LifxTimeoutError: If device does not respond
|
|
514
|
+
LifxUnsupportedCommandError: If device doesn't support this command
|
|
495
515
|
|
|
496
516
|
Example:
|
|
497
517
|
```python
|
|
@@ -508,9 +528,10 @@ class Device:
|
|
|
508
528
|
label_bytes = label_bytes.ljust(32, b"\x00")
|
|
509
529
|
|
|
510
530
|
# Request automatically handles acknowledgement
|
|
511
|
-
await self.connection.request(
|
|
531
|
+
result = await self.connection.request(
|
|
512
532
|
packets.Device.SetLabel(label=label_bytes),
|
|
513
533
|
)
|
|
534
|
+
self._raise_if_unhandled(result)
|
|
514
535
|
|
|
515
536
|
# Update cached state
|
|
516
537
|
self._label = label
|
|
@@ -535,6 +556,7 @@ class Device:
|
|
|
535
556
|
LifxDeviceNotFoundError: If device is not connected
|
|
536
557
|
LifxTimeoutError: If device does not respond
|
|
537
558
|
LifxProtocolError: If response is invalid
|
|
559
|
+
LifxUnsupportedCommandError: If device doesn't support this command
|
|
538
560
|
|
|
539
561
|
Example:
|
|
540
562
|
```python
|
|
@@ -544,6 +566,7 @@ class Device:
|
|
|
544
566
|
"""
|
|
545
567
|
# Request automatically unpacks response
|
|
546
568
|
state = await self.connection.request(packets.Device.GetPower())
|
|
569
|
+
self._raise_if_unhandled(state)
|
|
547
570
|
|
|
548
571
|
# Power level is uint16 (0 or 65535)
|
|
549
572
|
_LOGGER.debug(
|
|
@@ -566,6 +589,7 @@ class Device:
|
|
|
566
589
|
ValueError: If integer value is not 0 or 65535
|
|
567
590
|
LifxDeviceNotFoundError: If device is not connected
|
|
568
591
|
LifxTimeoutError: If device does not respond
|
|
592
|
+
LifxUnsupportedCommandError: If device doesn't support this command
|
|
569
593
|
|
|
570
594
|
Example:
|
|
571
595
|
```python
|
|
@@ -591,9 +615,10 @@ class Device:
|
|
|
591
615
|
raise TypeError(f"Expected bool or int, got {type(level).__name__}")
|
|
592
616
|
|
|
593
617
|
# Request automatically handles acknowledgement
|
|
594
|
-
await self.connection.request(
|
|
618
|
+
result = await self.connection.request(
|
|
595
619
|
packets.Device.SetPower(level=power_level),
|
|
596
620
|
)
|
|
621
|
+
self._raise_if_unhandled(result)
|
|
597
622
|
|
|
598
623
|
_LOGGER.debug(
|
|
599
624
|
{
|
|
@@ -616,6 +641,7 @@ class Device:
|
|
|
616
641
|
LifxDeviceNotFoundError: If device is not connected
|
|
617
642
|
LifxTimeoutError: If device does not respond
|
|
618
643
|
LifxProtocolError: If response is invalid
|
|
644
|
+
LifxUnsupportedCommandError: If device doesn't support this command
|
|
619
645
|
|
|
620
646
|
Example:
|
|
621
647
|
```python
|
|
@@ -625,6 +651,7 @@ class Device:
|
|
|
625
651
|
"""
|
|
626
652
|
# Request automatically unpacks response
|
|
627
653
|
state = await self.connection.request(packets.Device.GetVersion())
|
|
654
|
+
self._raise_if_unhandled(state)
|
|
628
655
|
|
|
629
656
|
version = DeviceVersion(
|
|
630
657
|
vendor=state.vendor,
|
|
@@ -655,6 +682,7 @@ class Device:
|
|
|
655
682
|
LifxDeviceNotFoundError: If device is not connected
|
|
656
683
|
LifxTimeoutError: If device does not respond
|
|
657
684
|
LifxProtocolError: If response is invalid
|
|
685
|
+
LifxUnsupportedCommandError: If device doesn't support this command
|
|
658
686
|
|
|
659
687
|
Example:
|
|
660
688
|
```python
|
|
@@ -665,6 +693,7 @@ class Device:
|
|
|
665
693
|
"""
|
|
666
694
|
# Request automatically unpacks response
|
|
667
695
|
state = await self.connection.request(packets.Device.GetInfo()) # type: ignore
|
|
696
|
+
self._raise_if_unhandled(state)
|
|
668
697
|
|
|
669
698
|
info = DeviceInfo(time=state.time, uptime=state.uptime, downtime=state.downtime)
|
|
670
699
|
|
|
@@ -694,6 +723,7 @@ class Device:
|
|
|
694
723
|
LifxDeviceNotFoundError: If device is not connected
|
|
695
724
|
LifxTimeoutError: If device does not respond
|
|
696
725
|
LifxProtocolError: If response is invalid
|
|
726
|
+
LifxUnsupportedCommandError: If device doesn't support this command
|
|
697
727
|
|
|
698
728
|
Example:
|
|
699
729
|
```python
|
|
@@ -704,6 +734,7 @@ class Device:
|
|
|
704
734
|
"""
|
|
705
735
|
# Request WiFi info from device
|
|
706
736
|
state = await self.connection.request(packets.Device.GetWifiInfo())
|
|
737
|
+
self._raise_if_unhandled(state)
|
|
707
738
|
|
|
708
739
|
# Extract WiFi info from response
|
|
709
740
|
wifi_info = WifiInfo(signal=state.signal)
|
|
@@ -730,6 +761,7 @@ class Device:
|
|
|
730
761
|
LifxDeviceNotFoundError: If device is not connected
|
|
731
762
|
LifxTimeoutError: If device does not respond
|
|
732
763
|
LifxProtocolError: If response is invalid
|
|
764
|
+
LifxUnsupportedCommandError: If device doesn't support this command
|
|
733
765
|
|
|
734
766
|
Example:
|
|
735
767
|
```python
|
|
@@ -739,6 +771,7 @@ class Device:
|
|
|
739
771
|
"""
|
|
740
772
|
# Request automatically unpacks response
|
|
741
773
|
state = await self.connection.request(packets.Device.GetHostFirmware()) # type: ignore
|
|
774
|
+
self._raise_if_unhandled(state)
|
|
742
775
|
|
|
743
776
|
firmware = FirmwareInfo(
|
|
744
777
|
build=state.build,
|
|
@@ -778,6 +811,7 @@ class Device:
|
|
|
778
811
|
LifxDeviceNotFoundError: If device is not connected
|
|
779
812
|
LifxTimeoutError: If device does not respond
|
|
780
813
|
LifxProtocolError: If response is invalid
|
|
814
|
+
LifxUnsupportedCommandError: If device doesn't support this command
|
|
781
815
|
|
|
782
816
|
Example:
|
|
783
817
|
```python
|
|
@@ -787,6 +821,7 @@ class Device:
|
|
|
787
821
|
"""
|
|
788
822
|
# Request automatically unpacks response
|
|
789
823
|
state = await self.connection.request(packets.Device.GetWifiFirmware()) # type: ignore
|
|
824
|
+
self._raise_if_unhandled(state)
|
|
790
825
|
|
|
791
826
|
firmware = FirmwareInfo(
|
|
792
827
|
build=state.build,
|
|
@@ -822,6 +857,7 @@ class Device:
|
|
|
822
857
|
LifxDeviceNotFoundError: If device is not connected
|
|
823
858
|
LifxTimeoutError: If device does not respond
|
|
824
859
|
LifxProtocolError: If response is invalid
|
|
860
|
+
LifxUnsupportedCommandError: If device doesn't support this command
|
|
825
861
|
|
|
826
862
|
Example:
|
|
827
863
|
```python
|
|
@@ -832,6 +868,7 @@ class Device:
|
|
|
832
868
|
"""
|
|
833
869
|
# Request automatically unpacks response
|
|
834
870
|
state = await self.connection.request(packets.Device.GetLocation()) # type: ignore
|
|
871
|
+
self._raise_if_unhandled(state)
|
|
835
872
|
|
|
836
873
|
location = LocationInfo(
|
|
837
874
|
location=state.location,
|
|
@@ -873,6 +910,7 @@ class Device:
|
|
|
873
910
|
LifxDeviceNotFoundError: If device is not connected
|
|
874
911
|
LifxTimeoutError: If device does not respond
|
|
875
912
|
ValueError: If label is invalid
|
|
913
|
+
LifxUnsupportedCommandError: If device doesn't support this command
|
|
876
914
|
|
|
877
915
|
Example:
|
|
878
916
|
```python
|
|
@@ -967,11 +1005,12 @@ class Device:
|
|
|
967
1005
|
updated_at = int(time.time() * 1e9)
|
|
968
1006
|
|
|
969
1007
|
# Update this device
|
|
970
|
-
await self.connection.request(
|
|
1008
|
+
result = await self.connection.request(
|
|
971
1009
|
packets.Device.SetLocation(
|
|
972
1010
|
location=location_uuid_to_use, label=label_bytes, updated_at=updated_at
|
|
973
1011
|
),
|
|
974
1012
|
)
|
|
1013
|
+
self._raise_if_unhandled(result)
|
|
975
1014
|
|
|
976
1015
|
# Update cached state
|
|
977
1016
|
location_info = LocationInfo(
|
|
@@ -1003,6 +1042,7 @@ class Device:
|
|
|
1003
1042
|
LifxDeviceNotFoundError: If device is not connected
|
|
1004
1043
|
LifxTimeoutError: If device does not respond
|
|
1005
1044
|
LifxProtocolError: If response is invalid
|
|
1045
|
+
LifxUnsupportedCommandError: If device doesn't support this command
|
|
1006
1046
|
|
|
1007
1047
|
Example:
|
|
1008
1048
|
```python
|
|
@@ -1013,6 +1053,7 @@ class Device:
|
|
|
1013
1053
|
"""
|
|
1014
1054
|
# Request automatically unpacks response
|
|
1015
1055
|
state = await self.connection.request(packets.Device.GetGroup()) # type: ignore
|
|
1056
|
+
self._raise_if_unhandled(state)
|
|
1016
1057
|
|
|
1017
1058
|
group = GroupInfo(
|
|
1018
1059
|
group=state.group,
|
|
@@ -1054,6 +1095,7 @@ class Device:
|
|
|
1054
1095
|
LifxDeviceNotFoundError: If device is not connected
|
|
1055
1096
|
LifxTimeoutError: If device does not respond
|
|
1056
1097
|
ValueError: If label is invalid
|
|
1098
|
+
LifxUnsupportedCommandError: If device doesn't support this command
|
|
1057
1099
|
|
|
1058
1100
|
Example:
|
|
1059
1101
|
```python
|
|
@@ -1148,11 +1190,12 @@ class Device:
|
|
|
1148
1190
|
updated_at = int(time.time() * 1e9)
|
|
1149
1191
|
|
|
1150
1192
|
# Update this device
|
|
1151
|
-
await self.connection.request(
|
|
1193
|
+
result = await self.connection.request(
|
|
1152
1194
|
packets.Device.SetGroup(
|
|
1153
1195
|
group=group_uuid_to_use, label=label_bytes, updated_at=updated_at
|
|
1154
1196
|
),
|
|
1155
1197
|
)
|
|
1198
|
+
self._raise_if_unhandled(result)
|
|
1156
1199
|
|
|
1157
1200
|
# Update cached state
|
|
1158
1201
|
group_info = GroupInfo(
|
|
@@ -1181,6 +1224,7 @@ class Device:
|
|
|
1181
1224
|
Raises:
|
|
1182
1225
|
LifxDeviceNotFoundError: If device is not connected
|
|
1183
1226
|
LifxTimeoutError: If device does not respond
|
|
1227
|
+
LifxUnsupportedCommandError: If device doesn't support this command
|
|
1184
1228
|
|
|
1185
1229
|
Example:
|
|
1186
1230
|
```python
|
|
@@ -1194,9 +1238,10 @@ class Device:
|
|
|
1194
1238
|
comes back online and is discoverable again.
|
|
1195
1239
|
"""
|
|
1196
1240
|
# Send reboot request
|
|
1197
|
-
await self.connection.request(
|
|
1241
|
+
result = await self.connection.request(
|
|
1198
1242
|
packets.Device.SetReboot(),
|
|
1199
1243
|
)
|
|
1244
|
+
self._raise_if_unhandled(result)
|
|
1200
1245
|
_LOGGER.debug(
|
|
1201
1246
|
{
|
|
1202
1247
|
"class": "Device",
|
|
@@ -70,6 +70,7 @@ class HevLight(Light):
|
|
|
70
70
|
LifxDeviceNotFoundError: If device is not connected
|
|
71
71
|
LifxTimeoutError: If device does not respond
|
|
72
72
|
LifxProtocolError: If response is invalid
|
|
73
|
+
LifxUnsupportedCommandError: If device doesn't support this command
|
|
73
74
|
|
|
74
75
|
Example:
|
|
75
76
|
```python
|
|
@@ -82,6 +83,7 @@ class HevLight(Light):
|
|
|
82
83
|
"""
|
|
83
84
|
# Request HEV cycle state
|
|
84
85
|
state = await self.connection.request(packets.Light.GetHevCycle())
|
|
86
|
+
self._raise_if_unhandled(state)
|
|
85
87
|
|
|
86
88
|
# Create state object
|
|
87
89
|
cycle_state = HevCycleState(
|
|
@@ -116,6 +118,7 @@ class HevLight(Light):
|
|
|
116
118
|
ValueError: If duration is negative
|
|
117
119
|
LifxDeviceNotFoundError: If device is not connected
|
|
118
120
|
LifxTimeoutError: If device does not respond
|
|
121
|
+
LifxUnsupportedCommandError: If device doesn't support this command
|
|
119
122
|
|
|
120
123
|
Example:
|
|
121
124
|
```python
|
|
@@ -130,12 +133,13 @@ class HevLight(Light):
|
|
|
130
133
|
raise ValueError(f"Duration must be non-negative, got {duration_seconds}")
|
|
131
134
|
|
|
132
135
|
# Request automatically handles acknowledgement
|
|
133
|
-
await self.connection.request(
|
|
136
|
+
result = await self.connection.request(
|
|
134
137
|
packets.Light.SetHevCycle(
|
|
135
138
|
enable=enable,
|
|
136
139
|
duration_s=duration_seconds,
|
|
137
140
|
),
|
|
138
141
|
)
|
|
142
|
+
self._raise_if_unhandled(result)
|
|
139
143
|
|
|
140
144
|
_LOGGER.debug(
|
|
141
145
|
{
|
|
@@ -156,6 +160,7 @@ class HevLight(Light):
|
|
|
156
160
|
LifxDeviceNotFoundError: If device is not connected
|
|
157
161
|
LifxTimeoutError: If device does not respond
|
|
158
162
|
LifxProtocolError: If response is invalid
|
|
163
|
+
LifxUnsupportedCommandError: If device doesn't support this command
|
|
159
164
|
|
|
160
165
|
Example:
|
|
161
166
|
```python
|
|
@@ -166,6 +171,7 @@ class HevLight(Light):
|
|
|
166
171
|
"""
|
|
167
172
|
# Request HEV configuration
|
|
168
173
|
state = await self.connection.request(packets.Light.GetHevCycleConfiguration())
|
|
174
|
+
self._raise_if_unhandled(state)
|
|
169
175
|
|
|
170
176
|
# Create config object
|
|
171
177
|
config = HevConfig(
|
|
@@ -201,6 +207,7 @@ class HevLight(Light):
|
|
|
201
207
|
ValueError: If duration is negative
|
|
202
208
|
LifxDeviceNotFoundError: If device is not connected
|
|
203
209
|
LifxTimeoutError: If device does not respond
|
|
210
|
+
LifxUnsupportedCommandError: If device doesn't support this command
|
|
204
211
|
|
|
205
212
|
Example:
|
|
206
213
|
```python
|
|
@@ -212,12 +219,13 @@ class HevLight(Light):
|
|
|
212
219
|
raise ValueError(f"Duration must be non-negative, got {duration_seconds}")
|
|
213
220
|
|
|
214
221
|
# Request automatically handles acknowledgement
|
|
215
|
-
await self.connection.request(
|
|
222
|
+
result = await self.connection.request(
|
|
216
223
|
packets.Light.SetHevCycleConfiguration(
|
|
217
224
|
indication=indication,
|
|
218
225
|
duration_s=duration_seconds,
|
|
219
226
|
),
|
|
220
227
|
)
|
|
228
|
+
self._raise_if_unhandled(result)
|
|
221
229
|
|
|
222
230
|
# Update cached state
|
|
223
231
|
self._hev_config = HevConfig(indication=indication, duration_s=duration_seconds)
|
|
@@ -242,6 +250,7 @@ class HevLight(Light):
|
|
|
242
250
|
LifxDeviceNotFoundError: If device is not connected
|
|
243
251
|
LifxTimeoutError: If device does not respond
|
|
244
252
|
LifxProtocolError: If response is invalid
|
|
253
|
+
LifxUnsupportedCommandError: If device doesn't support this command
|
|
245
254
|
|
|
246
255
|
Example:
|
|
247
256
|
```python
|
|
@@ -254,6 +263,7 @@ class HevLight(Light):
|
|
|
254
263
|
"""
|
|
255
264
|
# Request last HEV result
|
|
256
265
|
state = await self.connection.request(packets.Light.GetLastHevCycleResult())
|
|
266
|
+
self._raise_if_unhandled(state)
|
|
257
267
|
|
|
258
268
|
# Store cached state
|
|
259
269
|
self._hev_result = state.result
|
|
@@ -58,6 +58,7 @@ class InfraredLight(Light):
|
|
|
58
58
|
LifxDeviceNotFoundError: If device is not connected
|
|
59
59
|
LifxTimeoutError: If device does not respond
|
|
60
60
|
LifxProtocolError: If response is invalid
|
|
61
|
+
LifxUnsupportedCommandError: If device doesn't support this command
|
|
61
62
|
|
|
62
63
|
Example:
|
|
63
64
|
```python
|
|
@@ -68,6 +69,7 @@ class InfraredLight(Light):
|
|
|
68
69
|
"""
|
|
69
70
|
# Request infrared state
|
|
70
71
|
state = await self.connection.request(packets.Light.GetInfrared())
|
|
72
|
+
self._raise_if_unhandled(state)
|
|
71
73
|
|
|
72
74
|
# Convert from uint16 (0-65535) to float (0.0-1.0)
|
|
73
75
|
brightness = state.brightness / 65535.0
|
|
@@ -96,6 +98,7 @@ class InfraredLight(Light):
|
|
|
96
98
|
ValueError: If brightness is out of range
|
|
97
99
|
LifxDeviceNotFoundError: If device is not connected
|
|
98
100
|
LifxTimeoutError: If device does not respond
|
|
101
|
+
LifxUnsupportedCommandError: If device doesn't support this command
|
|
99
102
|
|
|
100
103
|
Example:
|
|
101
104
|
```python
|
|
@@ -115,9 +118,10 @@ class InfraredLight(Light):
|
|
|
115
118
|
brightness_u16 = max(0, min(65535, int(round(brightness * 65535))))
|
|
116
119
|
|
|
117
120
|
# Request automatically handles acknowledgement
|
|
118
|
-
await self.connection.request(
|
|
121
|
+
result = await self.connection.request(
|
|
119
122
|
packets.Light.SetInfrared(brightness=brightness_u16),
|
|
120
123
|
)
|
|
124
|
+
self._raise_if_unhandled(result)
|
|
121
125
|
|
|
122
126
|
# Update cached state
|
|
123
127
|
self._infrared = brightness
|