lifx-async 4.3.6__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.
Files changed (144) hide show
  1. {lifx_async-4.3.6 → lifx_async-4.3.7}/.github/workflows/ci.yml +10 -10
  2. {lifx_async-4.3.6 → lifx_async-4.3.7}/.github/workflows/docs.yml +6 -6
  3. {lifx_async-4.3.6 → lifx_async-4.3.7}/PKG-INFO +1 -1
  4. {lifx_async-4.3.6 → lifx_async-4.3.7}/docs/changelog.md +8 -0
  5. {lifx_async-4.3.6 → lifx_async-4.3.7}/pyproject.toml +1 -1
  6. {lifx_async-4.3.6 → lifx_async-4.3.7}/src/lifx/devices/base.py +51 -6
  7. {lifx_async-4.3.6 → lifx_async-4.3.7}/src/lifx/devices/hev.py +12 -2
  8. {lifx_async-4.3.6 → lifx_async-4.3.7}/src/lifx/devices/infrared.py +5 -1
  9. {lifx_async-4.3.6 → lifx_async-4.3.7}/src/lifx/devices/light.py +18 -4
  10. {lifx_async-4.3.6 → lifx_async-4.3.7}/src/lifx/devices/matrix.py +18 -0
  11. {lifx_async-4.3.6 → lifx_async-4.3.7}/src/lifx/devices/multizone.py +19 -5
  12. {lifx_async-4.3.6 → lifx_async-4.3.7}/tests/test_devices/test_light.py +64 -0
  13. {lifx_async-4.3.6 → lifx_async-4.3.7}/uv.lock +1 -1
  14. {lifx_async-4.3.6 → lifx_async-4.3.7}/.claude/settings.json +0 -0
  15. {lifx_async-4.3.6 → lifx_async-4.3.7}/.github/dependabot.yml +0 -0
  16. {lifx_async-4.3.6 → lifx_async-4.3.7}/.github/labeler.yml +0 -0
  17. {lifx_async-4.3.6 → lifx_async-4.3.7}/.github/workflows/pr-automation.yml +0 -0
  18. {lifx_async-4.3.6 → lifx_async-4.3.7}/.gitignore +0 -0
  19. {lifx_async-4.3.6 → lifx_async-4.3.7}/.pre-commit-config.yaml +0 -0
  20. {lifx_async-4.3.6 → lifx_async-4.3.7}/CLAUDE.md +0 -0
  21. {lifx_async-4.3.6 → lifx_async-4.3.7}/LICENSE +0 -0
  22. {lifx_async-4.3.6 → lifx_async-4.3.7}/README.md +0 -0
  23. {lifx_async-4.3.6 → lifx_async-4.3.7}/docs/api/colors.md +0 -0
  24. {lifx_async-4.3.6 → lifx_async-4.3.7}/docs/api/devices.md +0 -0
  25. {lifx_async-4.3.6 → lifx_async-4.3.7}/docs/api/effects.md +0 -0
  26. {lifx_async-4.3.6 → lifx_async-4.3.7}/docs/api/exceptions.md +0 -0
  27. {lifx_async-4.3.6 → lifx_async-4.3.7}/docs/api/high-level.md +0 -0
  28. {lifx_async-4.3.6 → lifx_async-4.3.7}/docs/api/index.md +0 -0
  29. {lifx_async-4.3.6 → lifx_async-4.3.7}/docs/api/network.md +0 -0
  30. {lifx_async-4.3.6 → lifx_async-4.3.7}/docs/api/protocol.md +0 -0
  31. {lifx_async-4.3.6 → lifx_async-4.3.7}/docs/api/themes.md +0 -0
  32. {lifx_async-4.3.6 → lifx_async-4.3.7}/docs/architecture/effects-architecture.md +0 -0
  33. {lifx_async-4.3.6 → lifx_async-4.3.7}/docs/architecture/overview.md +0 -0
  34. {lifx_async-4.3.6 → lifx_async-4.3.7}/docs/faq.md +0 -0
  35. {lifx_async-4.3.6 → lifx_async-4.3.7}/docs/getting-started/effects.md +0 -0
  36. {lifx_async-4.3.6 → lifx_async-4.3.7}/docs/getting-started/installation.md +0 -0
  37. {lifx_async-4.3.6 → lifx_async-4.3.7}/docs/getting-started/quickstart.md +0 -0
  38. {lifx_async-4.3.6 → lifx_async-4.3.7}/docs/getting-started/themes.md +0 -0
  39. {lifx_async-4.3.6 → lifx_async-4.3.7}/docs/index.md +0 -0
  40. {lifx_async-4.3.6 → lifx_async-4.3.7}/docs/migration/effect-api-changes.md +0 -0
  41. {lifx_async-4.3.6 → lifx_async-4.3.7}/docs/stylesheets/extra.css +0 -0
  42. {lifx_async-4.3.6 → lifx_async-4.3.7}/docs/user-guide/advanced-usage.md +0 -0
  43. {lifx_async-4.3.6 → lifx_async-4.3.7}/docs/user-guide/effects-custom.md +0 -0
  44. {lifx_async-4.3.6 → lifx_async-4.3.7}/docs/user-guide/effects-troubleshooting.md +0 -0
  45. {lifx_async-4.3.6 → lifx_async-4.3.7}/docs/user-guide/protocol-deep-dive.md +0 -0
  46. {lifx_async-4.3.6 → lifx_async-4.3.7}/docs/user-guide/themes.md +0 -0
  47. {lifx_async-4.3.6 → lifx_async-4.3.7}/docs/user-guide/troubleshooting.md +0 -0
  48. {lifx_async-4.3.6 → lifx_async-4.3.7}/examples/01_simple_discovery.py +0 -0
  49. {lifx_async-4.3.6 → lifx_async-4.3.7}/examples/02_simple_control.py +0 -0
  50. {lifx_async-4.3.6 → lifx_async-4.3.7}/examples/03_waveforms.py +0 -0
  51. {lifx_async-4.3.6 → lifx_async-4.3.7}/examples/04_logging.py +0 -0
  52. {lifx_async-4.3.6 → lifx_async-4.3.7}/examples/06_pulse_effect.py +0 -0
  53. {lifx_async-4.3.6 → lifx_async-4.3.7}/examples/07_colorloop_effect.py +0 -0
  54. {lifx_async-4.3.6 → lifx_async-4.3.7}/examples/08_custom_effect.py +0 -0
  55. {lifx_async-4.3.6 → lifx_async-4.3.7}/examples/09_background_effect.py +0 -0
  56. {lifx_async-4.3.6 → lifx_async-4.3.7}/examples/10_find_specific_devices.py +0 -0
  57. {lifx_async-4.3.6 → lifx_async-4.3.7}/examples/11_matrix_basic.py +0 -0
  58. {lifx_async-4.3.6 → lifx_async-4.3.7}/examples/12_matrix_effects.py +0 -0
  59. {lifx_async-4.3.6 → lifx_async-4.3.7}/examples/13_matrix_large.py +0 -0
  60. {lifx_async-4.3.6 → lifx_async-4.3.7}/mkdocs.yml +0 -0
  61. {lifx_async-4.3.6 → lifx_async-4.3.7}/renovate.json +0 -0
  62. {lifx_async-4.3.6 → lifx_async-4.3.7}/src/lifx/__init__.py +0 -0
  63. {lifx_async-4.3.6 → lifx_async-4.3.7}/src/lifx/api.py +0 -0
  64. {lifx_async-4.3.6 → lifx_async-4.3.7}/src/lifx/color.py +0 -0
  65. {lifx_async-4.3.6 → lifx_async-4.3.7}/src/lifx/const.py +0 -0
  66. {lifx_async-4.3.6 → lifx_async-4.3.7}/src/lifx/devices/__init__.py +0 -0
  67. {lifx_async-4.3.6 → lifx_async-4.3.7}/src/lifx/effects/__init__.py +0 -0
  68. {lifx_async-4.3.6 → lifx_async-4.3.7}/src/lifx/effects/base.py +0 -0
  69. {lifx_async-4.3.6 → lifx_async-4.3.7}/src/lifx/effects/colorloop.py +0 -0
  70. {lifx_async-4.3.6 → lifx_async-4.3.7}/src/lifx/effects/conductor.py +0 -0
  71. {lifx_async-4.3.6 → lifx_async-4.3.7}/src/lifx/effects/const.py +0 -0
  72. {lifx_async-4.3.6 → lifx_async-4.3.7}/src/lifx/effects/models.py +0 -0
  73. {lifx_async-4.3.6 → lifx_async-4.3.7}/src/lifx/effects/pulse.py +0 -0
  74. {lifx_async-4.3.6 → lifx_async-4.3.7}/src/lifx/effects/state_manager.py +0 -0
  75. {lifx_async-4.3.6 → lifx_async-4.3.7}/src/lifx/exceptions.py +0 -0
  76. {lifx_async-4.3.6 → lifx_async-4.3.7}/src/lifx/network/__init__.py +0 -0
  77. {lifx_async-4.3.6 → lifx_async-4.3.7}/src/lifx/network/connection.py +0 -0
  78. {lifx_async-4.3.6 → lifx_async-4.3.7}/src/lifx/network/discovery.py +0 -0
  79. {lifx_async-4.3.6 → lifx_async-4.3.7}/src/lifx/network/message.py +0 -0
  80. {lifx_async-4.3.6 → lifx_async-4.3.7}/src/lifx/network/transport.py +0 -0
  81. {lifx_async-4.3.6 → lifx_async-4.3.7}/src/lifx/products/__init__.py +0 -0
  82. {lifx_async-4.3.6 → lifx_async-4.3.7}/src/lifx/products/generator.py +0 -0
  83. {lifx_async-4.3.6 → lifx_async-4.3.7}/src/lifx/products/registry.py +0 -0
  84. {lifx_async-4.3.6 → lifx_async-4.3.7}/src/lifx/protocol/__init__.py +0 -0
  85. {lifx_async-4.3.6 → lifx_async-4.3.7}/src/lifx/protocol/base.py +0 -0
  86. {lifx_async-4.3.6 → lifx_async-4.3.7}/src/lifx/protocol/generator.py +0 -0
  87. {lifx_async-4.3.6 → lifx_async-4.3.7}/src/lifx/protocol/header.py +0 -0
  88. {lifx_async-4.3.6 → lifx_async-4.3.7}/src/lifx/protocol/models.py +0 -0
  89. {lifx_async-4.3.6 → lifx_async-4.3.7}/src/lifx/protocol/packets.py +0 -0
  90. {lifx_async-4.3.6 → lifx_async-4.3.7}/src/lifx/protocol/protocol_types.py +0 -0
  91. {lifx_async-4.3.6 → lifx_async-4.3.7}/src/lifx/protocol/serializer.py +0 -0
  92. {lifx_async-4.3.6 → lifx_async-4.3.7}/src/lifx/py.typed +0 -0
  93. {lifx_async-4.3.6 → lifx_async-4.3.7}/src/lifx/theme/__init__.py +0 -0
  94. {lifx_async-4.3.6 → lifx_async-4.3.7}/src/lifx/theme/canvas.py +0 -0
  95. {lifx_async-4.3.6 → lifx_async-4.3.7}/src/lifx/theme/generators.py +0 -0
  96. {lifx_async-4.3.6 → lifx_async-4.3.7}/src/lifx/theme/library.py +0 -0
  97. {lifx_async-4.3.6 → lifx_async-4.3.7}/src/lifx/theme/theme.py +0 -0
  98. {lifx_async-4.3.6 → lifx_async-4.3.7}/tests/__init__.py +0 -0
  99. {lifx_async-4.3.6 → lifx_async-4.3.7}/tests/conftest.py +0 -0
  100. {lifx_async-4.3.6 → lifx_async-4.3.7}/tests/test_api/__init__.py +0 -0
  101. {lifx_async-4.3.6 → lifx_async-4.3.7}/tests/test_api/test_api_apply_theme.py +0 -0
  102. {lifx_async-4.3.6 → lifx_async-4.3.7}/tests/test_api/test_api_batch_errors.py +0 -0
  103. {lifx_async-4.3.6 → lifx_async-4.3.7}/tests/test_api/test_api_batch_operations.py +0 -0
  104. {lifx_async-4.3.6 → lifx_async-4.3.7}/tests/test_api/test_api_discovery.py +0 -0
  105. {lifx_async-4.3.6 → lifx_async-4.3.7}/tests/test_api/test_api_organization.py +0 -0
  106. {lifx_async-4.3.6 → lifx_async-4.3.7}/tests/test_color.py +0 -0
  107. {lifx_async-4.3.6 → lifx_async-4.3.7}/tests/test_devices/__init__.py +0 -0
  108. {lifx_async-4.3.6 → lifx_async-4.3.7}/tests/test_devices/conftest.py +0 -0
  109. {lifx_async-4.3.6 → lifx_async-4.3.7}/tests/test_devices/test_base.py +0 -0
  110. {lifx_async-4.3.6 → lifx_async-4.3.7}/tests/test_devices/test_hev.py +0 -0
  111. {lifx_async-4.3.6 → lifx_async-4.3.7}/tests/test_devices/test_infrared.py +0 -0
  112. {lifx_async-4.3.6 → lifx_async-4.3.7}/tests/test_devices/test_mac_address.py +0 -0
  113. {lifx_async-4.3.6 → lifx_async-4.3.7}/tests/test_devices/test_matrix.py +0 -0
  114. {lifx_async-4.3.6 → lifx_async-4.3.7}/tests/test_devices/test_multizone.py +0 -0
  115. {lifx_async-4.3.6 → lifx_async-4.3.7}/tests/test_effects/__init__.py +0 -0
  116. {lifx_async-4.3.6 → lifx_async-4.3.7}/tests/test_effects/test_base.py +0 -0
  117. {lifx_async-4.3.6 → lifx_async-4.3.7}/tests/test_effects/test_capability_filtering.py +0 -0
  118. {lifx_async-4.3.6 → lifx_async-4.3.7}/tests/test_effects/test_colorloop.py +0 -0
  119. {lifx_async-4.3.6 → lifx_async-4.3.7}/tests/test_effects/test_integration.py +0 -0
  120. {lifx_async-4.3.6 → lifx_async-4.3.7}/tests/test_effects/test_models.py +0 -0
  121. {lifx_async-4.3.6 → lifx_async-4.3.7}/tests/test_effects/test_pulse.py +0 -0
  122. {lifx_async-4.3.6 → lifx_async-4.3.7}/tests/test_effects/test_state_manager.py +0 -0
  123. {lifx_async-4.3.6 → lifx_async-4.3.7}/tests/test_network/__init__.py +0 -0
  124. {lifx_async-4.3.6 → lifx_async-4.3.7}/tests/test_network/test_concurrent_requests.py +0 -0
  125. {lifx_async-4.3.6 → lifx_async-4.3.7}/tests/test_network/test_connection.py +0 -0
  126. {lifx_async-4.3.6 → lifx_async-4.3.7}/tests/test_network/test_discovery_devices.py +0 -0
  127. {lifx_async-4.3.6 → lifx_async-4.3.7}/tests/test_network/test_discovery_errors.py +0 -0
  128. {lifx_async-4.3.6 → lifx_async-4.3.7}/tests/test_network/test_message.py +0 -0
  129. {lifx_async-4.3.6 → lifx_async-4.3.7}/tests/test_network/test_message_advanced.py +0 -0
  130. {lifx_async-4.3.6 → lifx_async-4.3.7}/tests/test_network/test_transport.py +0 -0
  131. {lifx_async-4.3.6 → lifx_async-4.3.7}/tests/test_products/test_product_generator.py +0 -0
  132. {lifx_async-4.3.6 → lifx_async-4.3.7}/tests/test_products/test_registry.py +0 -0
  133. {lifx_async-4.3.6 → lifx_async-4.3.7}/tests/test_protocol/test_generated.py +0 -0
  134. {lifx_async-4.3.6 → lifx_async-4.3.7}/tests/test_protocol/test_header.py +0 -0
  135. {lifx_async-4.3.6 → lifx_async-4.3.7}/tests/test_protocol/test_protocol_generator.py +0 -0
  136. {lifx_async-4.3.6 → lifx_async-4.3.7}/tests/test_protocol/test_serializer.py +0 -0
  137. {lifx_async-4.3.6 → lifx_async-4.3.7}/tests/test_theme/__init__.py +0 -0
  138. {lifx_async-4.3.6 → lifx_async-4.3.7}/tests/test_theme/conftest.py +0 -0
  139. {lifx_async-4.3.6 → lifx_async-4.3.7}/tests/test_theme/test_apply_theme.py +0 -0
  140. {lifx_async-4.3.6 → lifx_async-4.3.7}/tests/test_theme/test_canvas.py +0 -0
  141. {lifx_async-4.3.6 → lifx_async-4.3.7}/tests/test_theme/test_generators.py +0 -0
  142. {lifx_async-4.3.6 → lifx_async-4.3.7}/tests/test_theme/test_library.py +0 -0
  143. {lifx_async-4.3.6 → lifx_async-4.3.7}/tests/test_theme/test_theme.py +0 -0
  144. {lifx_async-4.3.6 → 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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
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@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7
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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
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@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7
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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
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@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7
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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
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@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7
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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
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@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7
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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
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@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7
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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
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@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7
100
+ uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7
101
101
  with:
102
102
  version: ${{ env.UV_VERSION }}
103
103
  python-version: ${{ env.PYTHON_VERSION }}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lifx-async
3
- Version: 4.3.6
3
+ Version: 4.3.7
4
4
  Summary: A modern, type-safe, async Python library for controlling LIFX lights
5
5
  Author-email: Avi Miller <me@dje.li>
6
6
  Maintainer-email: Avi Miller <me@dje.li>
@@ -2,6 +2,14 @@
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
+
5
13
  ## v4.3.6 (2025-11-25)
6
14
 
7
15
  ### Bug Fixes
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "lifx-async"
3
- version = "4.3.6"
3
+ version = "4.3.7"
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"
@@ -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