lifx-async 4.3.6__tar.gz → 4.3.8__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.8}/.github/workflows/ci.yml +10 -10
  2. {lifx_async-4.3.6 → lifx_async-4.3.8}/.github/workflows/docs.yml +6 -6
  3. {lifx_async-4.3.6 → lifx_async-4.3.8}/PKG-INFO +1 -1
  4. {lifx_async-4.3.6 → lifx_async-4.3.8}/docs/changelog.md +16 -0
  5. {lifx_async-4.3.6 → lifx_async-4.3.8}/pyproject.toml +1 -1
  6. {lifx_async-4.3.6 → lifx_async-4.3.8}/src/lifx/devices/base.py +49 -6
  7. {lifx_async-4.3.6 → lifx_async-4.3.8}/src/lifx/devices/hev.py +12 -2
  8. {lifx_async-4.3.6 → lifx_async-4.3.8}/src/lifx/devices/infrared.py +5 -1
  9. {lifx_async-4.3.6 → lifx_async-4.3.8}/src/lifx/devices/light.py +18 -4
  10. {lifx_async-4.3.6 → lifx_async-4.3.8}/src/lifx/devices/matrix.py +18 -0
  11. {lifx_async-4.3.6 → lifx_async-4.3.8}/src/lifx/devices/multizone.py +19 -5
  12. {lifx_async-4.3.6 → lifx_async-4.3.8}/src/lifx/network/connection.py +4 -2
  13. {lifx_async-4.3.6 → lifx_async-4.3.8}/tests/test_devices/test_light.py +64 -0
  14. {lifx_async-4.3.6 → lifx_async-4.3.8}/tests/test_network/test_connection.py +7 -8
  15. {lifx_async-4.3.6 → lifx_async-4.3.8}/uv.lock +1 -1
  16. {lifx_async-4.3.6 → lifx_async-4.3.8}/.claude/settings.json +0 -0
  17. {lifx_async-4.3.6 → lifx_async-4.3.8}/.github/dependabot.yml +0 -0
  18. {lifx_async-4.3.6 → lifx_async-4.3.8}/.github/labeler.yml +0 -0
  19. {lifx_async-4.3.6 → lifx_async-4.3.8}/.github/workflows/pr-automation.yml +0 -0
  20. {lifx_async-4.3.6 → lifx_async-4.3.8}/.gitignore +0 -0
  21. {lifx_async-4.3.6 → lifx_async-4.3.8}/.pre-commit-config.yaml +0 -0
  22. {lifx_async-4.3.6 → lifx_async-4.3.8}/CLAUDE.md +0 -0
  23. {lifx_async-4.3.6 → lifx_async-4.3.8}/LICENSE +0 -0
  24. {lifx_async-4.3.6 → lifx_async-4.3.8}/README.md +0 -0
  25. {lifx_async-4.3.6 → lifx_async-4.3.8}/docs/api/colors.md +0 -0
  26. {lifx_async-4.3.6 → lifx_async-4.3.8}/docs/api/devices.md +0 -0
  27. {lifx_async-4.3.6 → lifx_async-4.3.8}/docs/api/effects.md +0 -0
  28. {lifx_async-4.3.6 → lifx_async-4.3.8}/docs/api/exceptions.md +0 -0
  29. {lifx_async-4.3.6 → lifx_async-4.3.8}/docs/api/high-level.md +0 -0
  30. {lifx_async-4.3.6 → lifx_async-4.3.8}/docs/api/index.md +0 -0
  31. {lifx_async-4.3.6 → lifx_async-4.3.8}/docs/api/network.md +0 -0
  32. {lifx_async-4.3.6 → lifx_async-4.3.8}/docs/api/protocol.md +0 -0
  33. {lifx_async-4.3.6 → lifx_async-4.3.8}/docs/api/themes.md +0 -0
  34. {lifx_async-4.3.6 → lifx_async-4.3.8}/docs/architecture/effects-architecture.md +0 -0
  35. {lifx_async-4.3.6 → lifx_async-4.3.8}/docs/architecture/overview.md +0 -0
  36. {lifx_async-4.3.6 → lifx_async-4.3.8}/docs/faq.md +0 -0
  37. {lifx_async-4.3.6 → lifx_async-4.3.8}/docs/getting-started/effects.md +0 -0
  38. {lifx_async-4.3.6 → lifx_async-4.3.8}/docs/getting-started/installation.md +0 -0
  39. {lifx_async-4.3.6 → lifx_async-4.3.8}/docs/getting-started/quickstart.md +0 -0
  40. {lifx_async-4.3.6 → lifx_async-4.3.8}/docs/getting-started/themes.md +0 -0
  41. {lifx_async-4.3.6 → lifx_async-4.3.8}/docs/index.md +0 -0
  42. {lifx_async-4.3.6 → lifx_async-4.3.8}/docs/migration/effect-api-changes.md +0 -0
  43. {lifx_async-4.3.6 → lifx_async-4.3.8}/docs/stylesheets/extra.css +0 -0
  44. {lifx_async-4.3.6 → lifx_async-4.3.8}/docs/user-guide/advanced-usage.md +0 -0
  45. {lifx_async-4.3.6 → lifx_async-4.3.8}/docs/user-guide/effects-custom.md +0 -0
  46. {lifx_async-4.3.6 → lifx_async-4.3.8}/docs/user-guide/effects-troubleshooting.md +0 -0
  47. {lifx_async-4.3.6 → lifx_async-4.3.8}/docs/user-guide/protocol-deep-dive.md +0 -0
  48. {lifx_async-4.3.6 → lifx_async-4.3.8}/docs/user-guide/themes.md +0 -0
  49. {lifx_async-4.3.6 → lifx_async-4.3.8}/docs/user-guide/troubleshooting.md +0 -0
  50. {lifx_async-4.3.6 → lifx_async-4.3.8}/examples/01_simple_discovery.py +0 -0
  51. {lifx_async-4.3.6 → lifx_async-4.3.8}/examples/02_simple_control.py +0 -0
  52. {lifx_async-4.3.6 → lifx_async-4.3.8}/examples/03_waveforms.py +0 -0
  53. {lifx_async-4.3.6 → lifx_async-4.3.8}/examples/04_logging.py +0 -0
  54. {lifx_async-4.3.6 → lifx_async-4.3.8}/examples/06_pulse_effect.py +0 -0
  55. {lifx_async-4.3.6 → lifx_async-4.3.8}/examples/07_colorloop_effect.py +0 -0
  56. {lifx_async-4.3.6 → lifx_async-4.3.8}/examples/08_custom_effect.py +0 -0
  57. {lifx_async-4.3.6 → lifx_async-4.3.8}/examples/09_background_effect.py +0 -0
  58. {lifx_async-4.3.6 → lifx_async-4.3.8}/examples/10_find_specific_devices.py +0 -0
  59. {lifx_async-4.3.6 → lifx_async-4.3.8}/examples/11_matrix_basic.py +0 -0
  60. {lifx_async-4.3.6 → lifx_async-4.3.8}/examples/12_matrix_effects.py +0 -0
  61. {lifx_async-4.3.6 → lifx_async-4.3.8}/examples/13_matrix_large.py +0 -0
  62. {lifx_async-4.3.6 → lifx_async-4.3.8}/mkdocs.yml +0 -0
  63. {lifx_async-4.3.6 → lifx_async-4.3.8}/renovate.json +0 -0
  64. {lifx_async-4.3.6 → lifx_async-4.3.8}/src/lifx/__init__.py +0 -0
  65. {lifx_async-4.3.6 → lifx_async-4.3.8}/src/lifx/api.py +0 -0
  66. {lifx_async-4.3.6 → lifx_async-4.3.8}/src/lifx/color.py +0 -0
  67. {lifx_async-4.3.6 → lifx_async-4.3.8}/src/lifx/const.py +0 -0
  68. {lifx_async-4.3.6 → lifx_async-4.3.8}/src/lifx/devices/__init__.py +0 -0
  69. {lifx_async-4.3.6 → lifx_async-4.3.8}/src/lifx/effects/__init__.py +0 -0
  70. {lifx_async-4.3.6 → lifx_async-4.3.8}/src/lifx/effects/base.py +0 -0
  71. {lifx_async-4.3.6 → lifx_async-4.3.8}/src/lifx/effects/colorloop.py +0 -0
  72. {lifx_async-4.3.6 → lifx_async-4.3.8}/src/lifx/effects/conductor.py +0 -0
  73. {lifx_async-4.3.6 → lifx_async-4.3.8}/src/lifx/effects/const.py +0 -0
  74. {lifx_async-4.3.6 → lifx_async-4.3.8}/src/lifx/effects/models.py +0 -0
  75. {lifx_async-4.3.6 → lifx_async-4.3.8}/src/lifx/effects/pulse.py +0 -0
  76. {lifx_async-4.3.6 → lifx_async-4.3.8}/src/lifx/effects/state_manager.py +0 -0
  77. {lifx_async-4.3.6 → lifx_async-4.3.8}/src/lifx/exceptions.py +0 -0
  78. {lifx_async-4.3.6 → lifx_async-4.3.8}/src/lifx/network/__init__.py +0 -0
  79. {lifx_async-4.3.6 → lifx_async-4.3.8}/src/lifx/network/discovery.py +0 -0
  80. {lifx_async-4.3.6 → lifx_async-4.3.8}/src/lifx/network/message.py +0 -0
  81. {lifx_async-4.3.6 → lifx_async-4.3.8}/src/lifx/network/transport.py +0 -0
  82. {lifx_async-4.3.6 → lifx_async-4.3.8}/src/lifx/products/__init__.py +0 -0
  83. {lifx_async-4.3.6 → lifx_async-4.3.8}/src/lifx/products/generator.py +0 -0
  84. {lifx_async-4.3.6 → lifx_async-4.3.8}/src/lifx/products/registry.py +0 -0
  85. {lifx_async-4.3.6 → lifx_async-4.3.8}/src/lifx/protocol/__init__.py +0 -0
  86. {lifx_async-4.3.6 → lifx_async-4.3.8}/src/lifx/protocol/base.py +0 -0
  87. {lifx_async-4.3.6 → lifx_async-4.3.8}/src/lifx/protocol/generator.py +0 -0
  88. {lifx_async-4.3.6 → lifx_async-4.3.8}/src/lifx/protocol/header.py +0 -0
  89. {lifx_async-4.3.6 → lifx_async-4.3.8}/src/lifx/protocol/models.py +0 -0
  90. {lifx_async-4.3.6 → lifx_async-4.3.8}/src/lifx/protocol/packets.py +0 -0
  91. {lifx_async-4.3.6 → lifx_async-4.3.8}/src/lifx/protocol/protocol_types.py +0 -0
  92. {lifx_async-4.3.6 → lifx_async-4.3.8}/src/lifx/protocol/serializer.py +0 -0
  93. {lifx_async-4.3.6 → lifx_async-4.3.8}/src/lifx/py.typed +0 -0
  94. {lifx_async-4.3.6 → lifx_async-4.3.8}/src/lifx/theme/__init__.py +0 -0
  95. {lifx_async-4.3.6 → lifx_async-4.3.8}/src/lifx/theme/canvas.py +0 -0
  96. {lifx_async-4.3.6 → lifx_async-4.3.8}/src/lifx/theme/generators.py +0 -0
  97. {lifx_async-4.3.6 → lifx_async-4.3.8}/src/lifx/theme/library.py +0 -0
  98. {lifx_async-4.3.6 → lifx_async-4.3.8}/src/lifx/theme/theme.py +0 -0
  99. {lifx_async-4.3.6 → lifx_async-4.3.8}/tests/__init__.py +0 -0
  100. {lifx_async-4.3.6 → lifx_async-4.3.8}/tests/conftest.py +0 -0
  101. {lifx_async-4.3.6 → lifx_async-4.3.8}/tests/test_api/__init__.py +0 -0
  102. {lifx_async-4.3.6 → lifx_async-4.3.8}/tests/test_api/test_api_apply_theme.py +0 -0
  103. {lifx_async-4.3.6 → lifx_async-4.3.8}/tests/test_api/test_api_batch_errors.py +0 -0
  104. {lifx_async-4.3.6 → lifx_async-4.3.8}/tests/test_api/test_api_batch_operations.py +0 -0
  105. {lifx_async-4.3.6 → lifx_async-4.3.8}/tests/test_api/test_api_discovery.py +0 -0
  106. {lifx_async-4.3.6 → lifx_async-4.3.8}/tests/test_api/test_api_organization.py +0 -0
  107. {lifx_async-4.3.6 → lifx_async-4.3.8}/tests/test_color.py +0 -0
  108. {lifx_async-4.3.6 → lifx_async-4.3.8}/tests/test_devices/__init__.py +0 -0
  109. {lifx_async-4.3.6 → lifx_async-4.3.8}/tests/test_devices/conftest.py +0 -0
  110. {lifx_async-4.3.6 → lifx_async-4.3.8}/tests/test_devices/test_base.py +0 -0
  111. {lifx_async-4.3.6 → lifx_async-4.3.8}/tests/test_devices/test_hev.py +0 -0
  112. {lifx_async-4.3.6 → lifx_async-4.3.8}/tests/test_devices/test_infrared.py +0 -0
  113. {lifx_async-4.3.6 → lifx_async-4.3.8}/tests/test_devices/test_mac_address.py +0 -0
  114. {lifx_async-4.3.6 → lifx_async-4.3.8}/tests/test_devices/test_matrix.py +0 -0
  115. {lifx_async-4.3.6 → lifx_async-4.3.8}/tests/test_devices/test_multizone.py +0 -0
  116. {lifx_async-4.3.6 → lifx_async-4.3.8}/tests/test_effects/__init__.py +0 -0
  117. {lifx_async-4.3.6 → lifx_async-4.3.8}/tests/test_effects/test_base.py +0 -0
  118. {lifx_async-4.3.6 → lifx_async-4.3.8}/tests/test_effects/test_capability_filtering.py +0 -0
  119. {lifx_async-4.3.6 → lifx_async-4.3.8}/tests/test_effects/test_colorloop.py +0 -0
  120. {lifx_async-4.3.6 → lifx_async-4.3.8}/tests/test_effects/test_integration.py +0 -0
  121. {lifx_async-4.3.6 → lifx_async-4.3.8}/tests/test_effects/test_models.py +0 -0
  122. {lifx_async-4.3.6 → lifx_async-4.3.8}/tests/test_effects/test_pulse.py +0 -0
  123. {lifx_async-4.3.6 → lifx_async-4.3.8}/tests/test_effects/test_state_manager.py +0 -0
  124. {lifx_async-4.3.6 → lifx_async-4.3.8}/tests/test_network/__init__.py +0 -0
  125. {lifx_async-4.3.6 → lifx_async-4.3.8}/tests/test_network/test_concurrent_requests.py +0 -0
  126. {lifx_async-4.3.6 → lifx_async-4.3.8}/tests/test_network/test_discovery_devices.py +0 -0
  127. {lifx_async-4.3.6 → lifx_async-4.3.8}/tests/test_network/test_discovery_errors.py +0 -0
  128. {lifx_async-4.3.6 → lifx_async-4.3.8}/tests/test_network/test_message.py +0 -0
  129. {lifx_async-4.3.6 → lifx_async-4.3.8}/tests/test_network/test_message_advanced.py +0 -0
  130. {lifx_async-4.3.6 → lifx_async-4.3.8}/tests/test_network/test_transport.py +0 -0
  131. {lifx_async-4.3.6 → lifx_async-4.3.8}/tests/test_products/test_product_generator.py +0 -0
  132. {lifx_async-4.3.6 → lifx_async-4.3.8}/tests/test_products/test_registry.py +0 -0
  133. {lifx_async-4.3.6 → lifx_async-4.3.8}/tests/test_protocol/test_generated.py +0 -0
  134. {lifx_async-4.3.6 → lifx_async-4.3.8}/tests/test_protocol/test_header.py +0 -0
  135. {lifx_async-4.3.6 → lifx_async-4.3.8}/tests/test_protocol/test_protocol_generator.py +0 -0
  136. {lifx_async-4.3.6 → lifx_async-4.3.8}/tests/test_protocol/test_serializer.py +0 -0
  137. {lifx_async-4.3.6 → lifx_async-4.3.8}/tests/test_theme/__init__.py +0 -0
  138. {lifx_async-4.3.6 → lifx_async-4.3.8}/tests/test_theme/conftest.py +0 -0
  139. {lifx_async-4.3.6 → lifx_async-4.3.8}/tests/test_theme/test_apply_theme.py +0 -0
  140. {lifx_async-4.3.6 → lifx_async-4.3.8}/tests/test_theme/test_canvas.py +0 -0
  141. {lifx_async-4.3.6 → lifx_async-4.3.8}/tests/test_theme/test_generators.py +0 -0
  142. {lifx_async-4.3.6 → lifx_async-4.3.8}/tests/test_theme/test_library.py +0 -0
  143. {lifx_async-4.3.6 → lifx_async-4.3.8}/tests/test_theme/test_theme.py +0 -0
  144. {lifx_async-4.3.6 → lifx_async-4.3.8}/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.8
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,22 @@
2
2
 
3
3
  <!-- version list -->
4
4
 
5
+ ## v4.3.8 (2025-11-25)
6
+
7
+ ### Bug Fixes
8
+
9
+ - **network**: Raise exception on StateUnhandled instead of returning False
10
+ ([`5ca3e8a`](https://github.com/Djelibeybi/lifx-async/commit/5ca3e8abcde0ec0eefe77645aeb0a2e63b18418c))
11
+
12
+
13
+ ## v4.3.7 (2025-11-25)
14
+
15
+ ### Bug Fixes
16
+
17
+ - **devices**: Raise LifxUnsupportedCommandError on StateUnhandled responses
18
+ ([`ec142cf`](https://github.com/Djelibeybi/lifx-async/commit/ec142cf0130847d65d4b9cd825575658936ef823))
19
+
20
+
5
21
  ## v4.3.6 (2025-11-25)
6
22
 
7
23
  ### Bug Fixes
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "lifx-async"
3
- version = "4.3.6"
3
+ version = "4.3.8"
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,21 @@ 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
+
155
170
  def __init__(
156
171
  self,
157
172
  serial: str,
@@ -456,6 +471,7 @@ class Device:
456
471
  LifxDeviceNotFoundError: If device is not connected
457
472
  LifxTimeoutError: If device does not respond
458
473
  LifxProtocolError: If response is invalid
474
+ LifxUnsupportedCommandError: If device doesn't support this command
459
475
 
460
476
  Example:
461
477
  ```python
@@ -469,6 +485,7 @@ class Device:
469
485
  """
470
486
  # Request automatically unpacks and decodes label
471
487
  state = await self.connection.request(packets.Device.GetLabel())
488
+ self._raise_if_unhandled(state)
472
489
 
473
490
  # Store label
474
491
  self._label = state.label
@@ -492,6 +509,7 @@ class Device:
492
509
  ValueError: If label is too long
493
510
  LifxDeviceNotFoundError: If device is not connected
494
511
  LifxTimeoutError: If device does not respond
512
+ LifxUnsupportedCommandError: If device doesn't support this command
495
513
 
496
514
  Example:
497
515
  ```python
@@ -508,9 +526,10 @@ class Device:
508
526
  label_bytes = label_bytes.ljust(32, b"\x00")
509
527
 
510
528
  # Request automatically handles acknowledgement
511
- await self.connection.request(
529
+ result = await self.connection.request(
512
530
  packets.Device.SetLabel(label=label_bytes),
513
531
  )
532
+ self._raise_if_unhandled(result)
514
533
 
515
534
  # Update cached state
516
535
  self._label = label
@@ -535,6 +554,7 @@ class Device:
535
554
  LifxDeviceNotFoundError: If device is not connected
536
555
  LifxTimeoutError: If device does not respond
537
556
  LifxProtocolError: If response is invalid
557
+ LifxUnsupportedCommandError: If device doesn't support this command
538
558
 
539
559
  Example:
540
560
  ```python
@@ -544,6 +564,7 @@ class Device:
544
564
  """
545
565
  # Request automatically unpacks response
546
566
  state = await self.connection.request(packets.Device.GetPower())
567
+ self._raise_if_unhandled(state)
547
568
 
548
569
  # Power level is uint16 (0 or 65535)
549
570
  _LOGGER.debug(
@@ -566,6 +587,7 @@ class Device:
566
587
  ValueError: If integer value is not 0 or 65535
567
588
  LifxDeviceNotFoundError: If device is not connected
568
589
  LifxTimeoutError: If device does not respond
590
+ LifxUnsupportedCommandError: If device doesn't support this command
569
591
 
570
592
  Example:
571
593
  ```python
@@ -591,9 +613,10 @@ class Device:
591
613
  raise TypeError(f"Expected bool or int, got {type(level).__name__}")
592
614
 
593
615
  # Request automatically handles acknowledgement
594
- await self.connection.request(
616
+ result = await self.connection.request(
595
617
  packets.Device.SetPower(level=power_level),
596
618
  )
619
+ self._raise_if_unhandled(result)
597
620
 
598
621
  _LOGGER.debug(
599
622
  {
@@ -616,6 +639,7 @@ class Device:
616
639
  LifxDeviceNotFoundError: If device is not connected
617
640
  LifxTimeoutError: If device does not respond
618
641
  LifxProtocolError: If response is invalid
642
+ LifxUnsupportedCommandError: If device doesn't support this command
619
643
 
620
644
  Example:
621
645
  ```python
@@ -625,6 +649,7 @@ class Device:
625
649
  """
626
650
  # Request automatically unpacks response
627
651
  state = await self.connection.request(packets.Device.GetVersion())
652
+ self._raise_if_unhandled(state)
628
653
 
629
654
  version = DeviceVersion(
630
655
  vendor=state.vendor,
@@ -655,6 +680,7 @@ class Device:
655
680
  LifxDeviceNotFoundError: If device is not connected
656
681
  LifxTimeoutError: If device does not respond
657
682
  LifxProtocolError: If response is invalid
683
+ LifxUnsupportedCommandError: If device doesn't support this command
658
684
 
659
685
  Example:
660
686
  ```python
@@ -665,6 +691,7 @@ class Device:
665
691
  """
666
692
  # Request automatically unpacks response
667
693
  state = await self.connection.request(packets.Device.GetInfo()) # type: ignore
694
+ self._raise_if_unhandled(state)
668
695
 
669
696
  info = DeviceInfo(time=state.time, uptime=state.uptime, downtime=state.downtime)
670
697
 
@@ -694,6 +721,7 @@ class Device:
694
721
  LifxDeviceNotFoundError: If device is not connected
695
722
  LifxTimeoutError: If device does not respond
696
723
  LifxProtocolError: If response is invalid
724
+ LifxUnsupportedCommandError: If device doesn't support this command
697
725
 
698
726
  Example:
699
727
  ```python
@@ -704,6 +732,7 @@ class Device:
704
732
  """
705
733
  # Request WiFi info from device
706
734
  state = await self.connection.request(packets.Device.GetWifiInfo())
735
+ self._raise_if_unhandled(state)
707
736
 
708
737
  # Extract WiFi info from response
709
738
  wifi_info = WifiInfo(signal=state.signal)
@@ -730,6 +759,7 @@ class Device:
730
759
  LifxDeviceNotFoundError: If device is not connected
731
760
  LifxTimeoutError: If device does not respond
732
761
  LifxProtocolError: If response is invalid
762
+ LifxUnsupportedCommandError: If device doesn't support this command
733
763
 
734
764
  Example:
735
765
  ```python
@@ -739,6 +769,7 @@ class Device:
739
769
  """
740
770
  # Request automatically unpacks response
741
771
  state = await self.connection.request(packets.Device.GetHostFirmware()) # type: ignore
772
+ self._raise_if_unhandled(state)
742
773
 
743
774
  firmware = FirmwareInfo(
744
775
  build=state.build,
@@ -778,6 +809,7 @@ class Device:
778
809
  LifxDeviceNotFoundError: If device is not connected
779
810
  LifxTimeoutError: If device does not respond
780
811
  LifxProtocolError: If response is invalid
812
+ LifxUnsupportedCommandError: If device doesn't support this command
781
813
 
782
814
  Example:
783
815
  ```python
@@ -787,6 +819,7 @@ class Device:
787
819
  """
788
820
  # Request automatically unpacks response
789
821
  state = await self.connection.request(packets.Device.GetWifiFirmware()) # type: ignore
822
+ self._raise_if_unhandled(state)
790
823
 
791
824
  firmware = FirmwareInfo(
792
825
  build=state.build,
@@ -822,6 +855,7 @@ class Device:
822
855
  LifxDeviceNotFoundError: If device is not connected
823
856
  LifxTimeoutError: If device does not respond
824
857
  LifxProtocolError: If response is invalid
858
+ LifxUnsupportedCommandError: If device doesn't support this command
825
859
 
826
860
  Example:
827
861
  ```python
@@ -832,6 +866,7 @@ class Device:
832
866
  """
833
867
  # Request automatically unpacks response
834
868
  state = await self.connection.request(packets.Device.GetLocation()) # type: ignore
869
+ self._raise_if_unhandled(state)
835
870
 
836
871
  location = LocationInfo(
837
872
  location=state.location,
@@ -873,6 +908,7 @@ class Device:
873
908
  LifxDeviceNotFoundError: If device is not connected
874
909
  LifxTimeoutError: If device does not respond
875
910
  ValueError: If label is invalid
911
+ LifxUnsupportedCommandError: If device doesn't support this command
876
912
 
877
913
  Example:
878
914
  ```python
@@ -967,11 +1003,12 @@ class Device:
967
1003
  updated_at = int(time.time() * 1e9)
968
1004
 
969
1005
  # Update this device
970
- await self.connection.request(
1006
+ result = await self.connection.request(
971
1007
  packets.Device.SetLocation(
972
1008
  location=location_uuid_to_use, label=label_bytes, updated_at=updated_at
973
1009
  ),
974
1010
  )
1011
+ self._raise_if_unhandled(result)
975
1012
 
976
1013
  # Update cached state
977
1014
  location_info = LocationInfo(
@@ -1003,6 +1040,7 @@ class Device:
1003
1040
  LifxDeviceNotFoundError: If device is not connected
1004
1041
  LifxTimeoutError: If device does not respond
1005
1042
  LifxProtocolError: If response is invalid
1043
+ LifxUnsupportedCommandError: If device doesn't support this command
1006
1044
 
1007
1045
  Example:
1008
1046
  ```python
@@ -1013,6 +1051,7 @@ class Device:
1013
1051
  """
1014
1052
  # Request automatically unpacks response
1015
1053
  state = await self.connection.request(packets.Device.GetGroup()) # type: ignore
1054
+ self._raise_if_unhandled(state)
1016
1055
 
1017
1056
  group = GroupInfo(
1018
1057
  group=state.group,
@@ -1054,6 +1093,7 @@ class Device:
1054
1093
  LifxDeviceNotFoundError: If device is not connected
1055
1094
  LifxTimeoutError: If device does not respond
1056
1095
  ValueError: If label is invalid
1096
+ LifxUnsupportedCommandError: If device doesn't support this command
1057
1097
 
1058
1098
  Example:
1059
1099
  ```python
@@ -1148,11 +1188,12 @@ class Device:
1148
1188
  updated_at = int(time.time() * 1e9)
1149
1189
 
1150
1190
  # Update this device
1151
- await self.connection.request(
1191
+ result = await self.connection.request(
1152
1192
  packets.Device.SetGroup(
1153
1193
  group=group_uuid_to_use, label=label_bytes, updated_at=updated_at
1154
1194
  ),
1155
1195
  )
1196
+ self._raise_if_unhandled(result)
1156
1197
 
1157
1198
  # Update cached state
1158
1199
  group_info = GroupInfo(
@@ -1181,6 +1222,7 @@ class Device:
1181
1222
  Raises:
1182
1223
  LifxDeviceNotFoundError: If device is not connected
1183
1224
  LifxTimeoutError: If device does not respond
1225
+ LifxUnsupportedCommandError: If device doesn't support this command
1184
1226
 
1185
1227
  Example:
1186
1228
  ```python
@@ -1194,9 +1236,10 @@ class Device:
1194
1236
  comes back online and is discoverable again.
1195
1237
  """
1196
1238
  # Send reboot request
1197
- await self.connection.request(
1239
+ result = await self.connection.request(
1198
1240
  packets.Device.SetReboot(),
1199
1241
  )
1242
+ self._raise_if_unhandled(result)
1200
1243
  _LOGGER.debug(
1201
1244
  {
1202
1245
  "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