lifx-async 4.8.0__tar.gz → 4.9.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {lifx_async-4.8.0 → lifx_async-4.9.0}/PKG-INFO +1 -1
- {lifx_async-4.8.0 → lifx_async-4.9.0}/docs/changelog.md +16 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/pyproject.toml +2 -1
- {lifx_async-4.8.0 → lifx_async-4.9.0}/src/lifx/color.py +223 -23
- {lifx_async-4.8.0 → lifx_async-4.9.0}/src/lifx/products/generator.py +7 -5
- {lifx_async-4.8.0 → lifx_async-4.9.0}/src/lifx/protocol/generator.py +7 -5
- {lifx_async-4.8.0 → lifx_async-4.9.0}/tests/conftest.py +29 -6
- {lifx_async-4.8.0 → lifx_async-4.9.0}/tests/test_devices/test_matrix.py +8 -6
- {lifx_async-4.8.0 → lifx_async-4.9.0}/uv.lock +15 -1
- {lifx_async-4.8.0 → lifx_async-4.9.0}/.claude/settings.json +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/.github/dependabot.yml +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/.github/labeler.yml +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/.github/workflows/ci.yml +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/.github/workflows/docs.yml +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/.github/workflows/pr-automation.yml +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/.gitignore +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/.pre-commit-config.yaml +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/CLAUDE.md +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/LICENSE +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/README.md +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/context7.json +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/docs/api/colors.md +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/docs/api/devices.md +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/docs/api/effects.md +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/docs/api/exceptions.md +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/docs/api/high-level.md +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/docs/api/index.md +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/docs/api/network.md +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/docs/api/protocol.md +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/docs/api/themes.md +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/docs/architecture/effects-architecture.md +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/docs/architecture/overview.md +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/docs/faq.md +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/docs/getting-started/effects.md +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/docs/getting-started/installation.md +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/docs/getting-started/quickstart.md +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/docs/getting-started/themes.md +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/docs/index.md +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/docs/migration/effect-api-changes.md +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/docs/stylesheets/extra.css +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/docs/user-guide/advanced-usage.md +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/docs/user-guide/ceiling-lights.md +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/docs/user-guide/effects-custom.md +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/docs/user-guide/effects-troubleshooting.md +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/docs/user-guide/protocol-deep-dive.md +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/docs/user-guide/themes.md +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/docs/user-guide/troubleshooting.md +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/examples/01_simple_discovery.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/examples/02_simple_control.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/examples/03_waveforms.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/examples/04_logging.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/examples/06_pulse_effect.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/examples/07_colorloop_effect.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/examples/08_custom_effect.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/examples/09_background_effect.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/examples/10_find_specific_devices.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/examples/11_matrix_basic.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/examples/12_matrix_effects.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/examples/13_matrix_large.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/examples/14_mdns_discovery.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/mkdocs.yml +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/renovate.json +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/scripts/mdns_probe.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/src/lifx/__init__.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/src/lifx/api.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/src/lifx/const.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/src/lifx/devices/__init__.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/src/lifx/devices/base.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/src/lifx/devices/ceiling.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/src/lifx/devices/hev.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/src/lifx/devices/infrared.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/src/lifx/devices/light.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/src/lifx/devices/matrix.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/src/lifx/devices/multizone.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/src/lifx/effects/__init__.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/src/lifx/effects/base.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/src/lifx/effects/colorloop.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/src/lifx/effects/conductor.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/src/lifx/effects/const.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/src/lifx/effects/models.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/src/lifx/effects/pulse.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/src/lifx/effects/state_manager.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/src/lifx/exceptions.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/src/lifx/network/__init__.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/src/lifx/network/connection.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/src/lifx/network/discovery.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/src/lifx/network/mdns/__init__.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/src/lifx/network/mdns/discovery.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/src/lifx/network/mdns/dns.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/src/lifx/network/mdns/transport.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/src/lifx/network/mdns/types.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/src/lifx/network/message.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/src/lifx/network/transport.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/src/lifx/products/__init__.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/src/lifx/products/quirks.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/src/lifx/products/registry.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/src/lifx/protocol/__init__.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/src/lifx/protocol/base.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/src/lifx/protocol/header.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/src/lifx/protocol/models.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/src/lifx/protocol/packets.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/src/lifx/protocol/protocol_types.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/src/lifx/protocol/serializer.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/src/lifx/py.typed +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/src/lifx/theme/__init__.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/src/lifx/theme/canvas.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/src/lifx/theme/generators.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/src/lifx/theme/library.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/src/lifx/theme/theme.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/tests/__init__.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/tests/test_api/__init__.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/tests/test_api/test_api_apply_theme.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/tests/test_api/test_api_batch_errors.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/tests/test_api/test_api_batch_operations.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/tests/test_api/test_api_discovery.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/tests/test_api/test_api_organization.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/tests/test_color.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/tests/test_devices/__init__.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/tests/test_devices/conftest.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/tests/test_devices/test_base.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/tests/test_devices/test_ceiling.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/tests/test_devices/test_hev.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/tests/test_devices/test_infrared.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/tests/test_devices/test_light.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/tests/test_devices/test_mac_address.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/tests/test_devices/test_multizone.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/tests/test_devices/test_state_ceiling.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/tests/test_devices/test_state_hev.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/tests/test_devices/test_state_infrared.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/tests/test_devices/test_state_light.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/tests/test_devices/test_state_management.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/tests/test_devices/test_state_matrix.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/tests/test_devices/test_state_multizone.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/tests/test_effects/__init__.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/tests/test_effects/test_base.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/tests/test_effects/test_capability_filtering.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/tests/test_effects/test_colorloop.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/tests/test_effects/test_integration.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/tests/test_effects/test_models.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/tests/test_effects/test_pulse.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/tests/test_effects/test_state_manager.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/tests/test_network/__init__.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/tests/test_network/test_concurrent_requests.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/tests/test_network/test_connection.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/tests/test_network/test_discovery_devices.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/tests/test_network/test_discovery_errors.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/tests/test_network/test_mdns/__init__.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/tests/test_network/test_mdns/conftest.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/tests/test_network/test_mdns/test_discovery.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/tests/test_network/test_mdns/test_dns.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/tests/test_network/test_mdns/test_transport.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/tests/test_network/test_message.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/tests/test_network/test_message_advanced.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/tests/test_network/test_transport.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/tests/test_products/test_product_generator.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/tests/test_products/test_registry.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/tests/test_protocol/test_generated.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/tests/test_protocol/test_header.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/tests/test_protocol/test_protocol_generator.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/tests/test_protocol/test_serializer.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/tests/test_theme/__init__.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/tests/test_theme/conftest.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/tests/test_theme/test_apply_theme.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/tests/test_theme/test_canvas.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/tests/test_theme/test_generators.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/tests/test_theme/test_library.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/tests/test_theme/test_theme.py +0 -0
- {lifx_async-4.8.0 → lifx_async-4.9.0}/tests/test_utils.py +0 -0
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
<!-- version list -->
|
|
4
4
|
|
|
5
|
+
## v4.9.0 (2025-12-30)
|
|
6
|
+
|
|
7
|
+
### Features
|
|
8
|
+
|
|
9
|
+
- **api**: Add HTML named colors and kelvin temperature presets
|
|
10
|
+
([`b631d43`](https://github.com/Djelibeybi/lifx-async/commit/b631d43f9ba61db5f77e233a6bc0745bb3fed8b8))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
## v4.8.1 (2025-12-24)
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
- Tighten up the URL parsing to be even more specific
|
|
18
|
+
([`0222362`](https://github.com/Djelibeybi/lifx-async/commit/0222362ace8d7c8bbbe7d5a50f9fb21b7cb89cd5))
|
|
19
|
+
|
|
20
|
+
|
|
5
21
|
## v4.8.0 (2025-12-20)
|
|
6
22
|
|
|
7
23
|
### Features
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "lifx-async"
|
|
3
|
-
version = "4.
|
|
3
|
+
version = "4.9.0"
|
|
4
4
|
description = "A modern, type-safe, async Python library for controlling LIFX lights"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|
|
@@ -40,6 +40,7 @@ dev = [
|
|
|
40
40
|
"pytest>=8.4.2",
|
|
41
41
|
"pytest-asyncio>=0.24.0",
|
|
42
42
|
"pytest-cov>=7.0.0",
|
|
43
|
+
"pytest-retry>=1.7.0",
|
|
43
44
|
"pytest-sugar>=1.1.1",
|
|
44
45
|
"pyyaml>=6.0.3",
|
|
45
46
|
"ruff>=0.14.2",
|
|
@@ -10,9 +10,23 @@ import colorsys
|
|
|
10
10
|
import math
|
|
11
11
|
|
|
12
12
|
from lifx.const import (
|
|
13
|
+
KELVIN_AMBER,
|
|
14
|
+
KELVIN_BLUE_DAYLIGHT,
|
|
15
|
+
KELVIN_BLUE_ICE,
|
|
16
|
+
KELVIN_BLUE_OVERCAST,
|
|
17
|
+
KELVIN_BRIGHT_DAYLIGHT,
|
|
18
|
+
KELVIN_CANDLELIGHT,
|
|
19
|
+
KELVIN_CLOUDY_DAYLIGHT,
|
|
13
20
|
KELVIN_COOL,
|
|
21
|
+
KELVIN_COOL_DAYLIGHT,
|
|
14
22
|
KELVIN_DAYLIGHT,
|
|
23
|
+
KELVIN_INCANDESCENT,
|
|
15
24
|
KELVIN_NEUTRAL,
|
|
25
|
+
KELVIN_NEUTRAL_WARM,
|
|
26
|
+
KELVIN_NOON_DAYLIGHT,
|
|
27
|
+
KELVIN_SOFT_DAYLIGHT,
|
|
28
|
+
KELVIN_SUNSET,
|
|
29
|
+
KELVIN_ULTRA_WARM,
|
|
16
30
|
KELVIN_WARM,
|
|
17
31
|
MAX_BRIGHTNESS,
|
|
18
32
|
MAX_HUE,
|
|
@@ -520,34 +534,220 @@ class HSBK:
|
|
|
520
534
|
|
|
521
535
|
# Common color presets
|
|
522
536
|
class Colors:
|
|
523
|
-
"""Common color presets for convenience.
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
537
|
+
"""Common color presets for convenience.
|
|
538
|
+
|
|
539
|
+
Includes all 140 standard HTML/CSS named colors plus white temperature
|
|
540
|
+
variants and pastel variations.
|
|
541
|
+
|
|
542
|
+
HTML color reference: https://www.w3.org/TR/css-color-3/#svg-color
|
|
543
|
+
"""
|
|
544
|
+
|
|
545
|
+
# Off / Black (brightness=0 turns light off or sets zone to black)
|
|
546
|
+
OFF = HSBK(hue=0, saturation=0.0, brightness=0.0, kelvin=KELVIN_NEUTRAL)
|
|
547
|
+
|
|
548
|
+
# HTML Named Colors (alphabetical order)
|
|
549
|
+
ALICE_BLUE = HSBK.from_rgb(240, 248, 255)
|
|
550
|
+
ANTIQUE_WHITE = HSBK.from_rgb(250, 235, 215)
|
|
551
|
+
AQUA = HSBK.from_rgb(0, 255, 255)
|
|
552
|
+
AQUAMARINE = HSBK.from_rgb(127, 255, 212)
|
|
553
|
+
AZURE = HSBK.from_rgb(240, 255, 255)
|
|
554
|
+
BEIGE = HSBK.from_rgb(245, 245, 220)
|
|
555
|
+
BISQUE = HSBK.from_rgb(255, 228, 196)
|
|
556
|
+
BLACK = HSBK.from_rgb(0, 0, 0)
|
|
557
|
+
BLANCHED_ALMOND = HSBK.from_rgb(255, 235, 205)
|
|
558
|
+
BLUE = HSBK.from_rgb(0, 0, 255)
|
|
559
|
+
BLUE_VIOLET = HSBK.from_rgb(138, 43, 226)
|
|
560
|
+
BROWN = HSBK.from_rgb(165, 42, 42)
|
|
561
|
+
BURLYWOOD = HSBK.from_rgb(222, 184, 135)
|
|
562
|
+
CADET_BLUE = HSBK.from_rgb(95, 158, 160)
|
|
563
|
+
CHARTREUSE = HSBK.from_rgb(127, 255, 0)
|
|
564
|
+
CHOCOLATE = HSBK.from_rgb(210, 105, 30)
|
|
565
|
+
CORAL = HSBK.from_rgb(255, 127, 80)
|
|
566
|
+
CORNFLOWER_BLUE = HSBK.from_rgb(100, 149, 237)
|
|
567
|
+
CORNSILK = HSBK.from_rgb(255, 248, 220)
|
|
568
|
+
CRIMSON = HSBK.from_rgb(220, 20, 60)
|
|
569
|
+
CYAN = HSBK.from_rgb(0, 255, 255)
|
|
570
|
+
DARK_BLUE = HSBK.from_rgb(0, 0, 139)
|
|
571
|
+
DARK_CYAN = HSBK.from_rgb(0, 139, 139)
|
|
572
|
+
DARK_GOLDENROD = HSBK.from_rgb(184, 134, 11)
|
|
573
|
+
DARK_GRAY = HSBK.from_rgb(169, 169, 169)
|
|
574
|
+
DARK_GREEN = HSBK.from_rgb(0, 100, 0)
|
|
575
|
+
DARK_GREY = HSBK.from_rgb(169, 169, 169)
|
|
576
|
+
DARK_KHAKI = HSBK.from_rgb(189, 183, 107)
|
|
577
|
+
DARK_MAGENTA = HSBK.from_rgb(139, 0, 139)
|
|
578
|
+
DARK_OLIVE_GREEN = HSBK.from_rgb(85, 107, 47)
|
|
579
|
+
DARK_ORANGE = HSBK.from_rgb(255, 140, 0)
|
|
580
|
+
DARK_ORCHID = HSBK.from_rgb(153, 50, 204)
|
|
581
|
+
DARK_RED = HSBK.from_rgb(139, 0, 0)
|
|
582
|
+
DARK_SALMON = HSBK.from_rgb(233, 150, 122)
|
|
583
|
+
DARK_SEA_GREEN = HSBK.from_rgb(143, 188, 143)
|
|
584
|
+
DARK_SLATE_BLUE = HSBK.from_rgb(72, 61, 139)
|
|
585
|
+
DARK_SLATE_GRAY = HSBK.from_rgb(47, 79, 79)
|
|
586
|
+
DARK_SLATE_GREY = HSBK.from_rgb(47, 79, 79)
|
|
587
|
+
DARK_TURQUOISE = HSBK.from_rgb(0, 206, 209)
|
|
588
|
+
DARK_VIOLET = HSBK.from_rgb(148, 0, 211)
|
|
589
|
+
DEEP_PINK = HSBK.from_rgb(255, 20, 147)
|
|
590
|
+
DEEP_SKY_BLUE = HSBK.from_rgb(0, 191, 255)
|
|
591
|
+
DIM_GRAY = HSBK.from_rgb(105, 105, 105)
|
|
592
|
+
DIM_GREY = HSBK.from_rgb(105, 105, 105)
|
|
593
|
+
DODGER_BLUE = HSBK.from_rgb(30, 144, 255)
|
|
594
|
+
FIREBRICK = HSBK.from_rgb(178, 34, 34)
|
|
595
|
+
FLORAL_WHITE = HSBK.from_rgb(255, 250, 240)
|
|
596
|
+
FOREST_GREEN = HSBK.from_rgb(34, 139, 34)
|
|
597
|
+
FUCHSIA = HSBK.from_rgb(255, 0, 255)
|
|
598
|
+
GAINSBORO = HSBK.from_rgb(220, 220, 220)
|
|
599
|
+
GHOST_WHITE = HSBK.from_rgb(248, 248, 255)
|
|
600
|
+
GOLD = HSBK.from_rgb(255, 215, 0)
|
|
601
|
+
GOLDENROD = HSBK.from_rgb(218, 165, 32)
|
|
602
|
+
GRAY = HSBK.from_rgb(128, 128, 128)
|
|
603
|
+
GREEN = HSBK.from_rgb(0, 128, 0)
|
|
604
|
+
GREEN_YELLOW = HSBK.from_rgb(173, 255, 47)
|
|
605
|
+
GREY = HSBK.from_rgb(128, 128, 128)
|
|
606
|
+
HONEYDEW = HSBK.from_rgb(240, 255, 240)
|
|
607
|
+
HOT_PINK = HSBK.from_rgb(255, 105, 180)
|
|
608
|
+
INDIAN_RED = HSBK.from_rgb(205, 92, 92)
|
|
609
|
+
INDIGO = HSBK.from_rgb(75, 0, 130)
|
|
610
|
+
IVORY = HSBK.from_rgb(255, 255, 240)
|
|
611
|
+
KHAKI = HSBK.from_rgb(240, 230, 140)
|
|
612
|
+
LAVENDER = HSBK.from_rgb(230, 230, 250)
|
|
613
|
+
LAVENDER_BLUSH = HSBK.from_rgb(255, 240, 245)
|
|
614
|
+
LAWN_GREEN = HSBK.from_rgb(124, 252, 0)
|
|
615
|
+
LEMON_CHIFFON = HSBK.from_rgb(255, 250, 205)
|
|
616
|
+
LIGHT_BLUE = HSBK.from_rgb(173, 216, 230)
|
|
617
|
+
LIGHT_CORAL = HSBK.from_rgb(240, 128, 128)
|
|
618
|
+
LIGHT_CYAN = HSBK.from_rgb(224, 255, 255)
|
|
619
|
+
LIGHT_GOLDENROD_YELLOW = HSBK.from_rgb(250, 250, 210)
|
|
620
|
+
LIGHT_GRAY = HSBK.from_rgb(211, 211, 211)
|
|
621
|
+
LIGHT_GREEN = HSBK.from_rgb(144, 238, 144)
|
|
622
|
+
LIGHT_GREY = HSBK.from_rgb(211, 211, 211)
|
|
623
|
+
LIGHT_PINK = HSBK.from_rgb(255, 182, 193)
|
|
624
|
+
LIGHT_SALMON = HSBK.from_rgb(255, 160, 122)
|
|
625
|
+
LIGHT_SEA_GREEN = HSBK.from_rgb(32, 178, 170)
|
|
626
|
+
LIGHT_SKY_BLUE = HSBK.from_rgb(135, 206, 250)
|
|
627
|
+
LIGHT_SLATE_GRAY = HSBK.from_rgb(119, 136, 153)
|
|
628
|
+
LIGHT_SLATE_GREY = HSBK.from_rgb(119, 136, 153)
|
|
629
|
+
LIGHT_STEEL_BLUE = HSBK.from_rgb(176, 196, 222)
|
|
630
|
+
LIGHT_YELLOW = HSBK.from_rgb(255, 255, 224)
|
|
631
|
+
LIME = HSBK.from_rgb(0, 255, 0)
|
|
632
|
+
LIME_GREEN = HSBK.from_rgb(50, 205, 50)
|
|
633
|
+
LINEN = HSBK.from_rgb(250, 240, 230)
|
|
634
|
+
MAGENTA = HSBK.from_rgb(255, 0, 255)
|
|
635
|
+
MAROON = HSBK.from_rgb(128, 0, 0)
|
|
636
|
+
MEDIUM_AQUAMARINE = HSBK.from_rgb(102, 205, 170)
|
|
637
|
+
MEDIUM_BLUE = HSBK.from_rgb(0, 0, 205)
|
|
638
|
+
MEDIUM_ORCHID = HSBK.from_rgb(186, 85, 211)
|
|
639
|
+
MEDIUM_PURPLE = HSBK.from_rgb(147, 112, 219)
|
|
640
|
+
MEDIUM_SEA_GREEN = HSBK.from_rgb(60, 179, 113)
|
|
641
|
+
MEDIUM_SLATE_BLUE = HSBK.from_rgb(123, 104, 238)
|
|
642
|
+
MEDIUM_SPRING_GREEN = HSBK.from_rgb(0, 250, 154)
|
|
643
|
+
MEDIUM_TURQUOISE = HSBK.from_rgb(72, 209, 204)
|
|
644
|
+
MEDIUM_VIOLET_RED = HSBK.from_rgb(199, 21, 133)
|
|
645
|
+
MIDNIGHT_BLUE = HSBK.from_rgb(25, 25, 112)
|
|
646
|
+
MINT_CREAM = HSBK.from_rgb(245, 255, 250)
|
|
647
|
+
MISTY_ROSE = HSBK.from_rgb(255, 228, 225)
|
|
648
|
+
MOCCASIN = HSBK.from_rgb(255, 228, 181)
|
|
649
|
+
NAVAJO_WHITE = HSBK.from_rgb(255, 222, 173)
|
|
650
|
+
NAVY = HSBK.from_rgb(0, 0, 128)
|
|
651
|
+
OLD_LACE = HSBK.from_rgb(253, 245, 230)
|
|
652
|
+
OLIVE = HSBK.from_rgb(128, 128, 0)
|
|
653
|
+
OLIVE_DRAB = HSBK.from_rgb(107, 142, 35)
|
|
654
|
+
ORANGE = HSBK.from_rgb(255, 165, 0)
|
|
655
|
+
ORANGE_RED = HSBK.from_rgb(255, 69, 0)
|
|
656
|
+
ORCHID = HSBK.from_rgb(218, 112, 214)
|
|
657
|
+
PALE_GOLDENROD = HSBK.from_rgb(238, 232, 170)
|
|
658
|
+
PALE_GREEN = HSBK.from_rgb(152, 251, 152)
|
|
659
|
+
PALE_TURQUOISE = HSBK.from_rgb(175, 238, 238)
|
|
660
|
+
PALE_VIOLET_RED = HSBK.from_rgb(219, 112, 147)
|
|
661
|
+
PAPAYA_WHIP = HSBK.from_rgb(255, 239, 213)
|
|
662
|
+
PEACH_PUFF = HSBK.from_rgb(255, 218, 185)
|
|
663
|
+
PERU = HSBK.from_rgb(205, 133, 63)
|
|
664
|
+
PINK = HSBK.from_rgb(255, 192, 203)
|
|
665
|
+
PLUM = HSBK.from_rgb(221, 160, 221)
|
|
666
|
+
POWDER_BLUE = HSBK.from_rgb(176, 224, 230)
|
|
667
|
+
PURPLE = HSBK.from_rgb(128, 0, 128)
|
|
668
|
+
REBECCA_PURPLE = HSBK.from_rgb(102, 51, 153)
|
|
669
|
+
RED = HSBK.from_rgb(255, 0, 0)
|
|
670
|
+
ROSY_BROWN = HSBK.from_rgb(188, 143, 143)
|
|
671
|
+
ROYAL_BLUE = HSBK.from_rgb(65, 105, 225)
|
|
672
|
+
SADDLE_BROWN = HSBK.from_rgb(139, 69, 19)
|
|
673
|
+
SALMON = HSBK.from_rgb(250, 128, 114)
|
|
674
|
+
SANDY_BROWN = HSBK.from_rgb(244, 164, 96)
|
|
675
|
+
SEA_GREEN = HSBK.from_rgb(46, 139, 87)
|
|
676
|
+
SEASHELL = HSBK.from_rgb(255, 245, 238)
|
|
677
|
+
SIENNA = HSBK.from_rgb(160, 82, 45)
|
|
678
|
+
SILVER = HSBK.from_rgb(192, 192, 192)
|
|
679
|
+
SKY_BLUE = HSBK.from_rgb(135, 206, 235)
|
|
680
|
+
SLATE_BLUE = HSBK.from_rgb(106, 90, 205)
|
|
681
|
+
SLATE_GRAY = HSBK.from_rgb(112, 128, 144)
|
|
682
|
+
SLATE_GREY = HSBK.from_rgb(112, 128, 144)
|
|
683
|
+
SNOW = HSBK.from_rgb(255, 250, 250)
|
|
684
|
+
SPRING_GREEN = HSBK.from_rgb(0, 255, 127)
|
|
685
|
+
STEEL_BLUE = HSBK.from_rgb(70, 130, 180)
|
|
686
|
+
TAN = HSBK.from_rgb(210, 180, 140)
|
|
687
|
+
TEAL = HSBK.from_rgb(0, 128, 128)
|
|
688
|
+
THISTLE = HSBK.from_rgb(216, 191, 216)
|
|
689
|
+
TOMATO = HSBK.from_rgb(255, 99, 71)
|
|
690
|
+
TURQUOISE = HSBK.from_rgb(64, 224, 208)
|
|
691
|
+
VIOLET = HSBK.from_rgb(238, 130, 238)
|
|
692
|
+
WHEAT = HSBK.from_rgb(245, 222, 179)
|
|
693
|
+
WHITE = HSBK.from_rgb(255, 255, 255)
|
|
694
|
+
WHITE_SMOKE = HSBK.from_rgb(245, 245, 245)
|
|
695
|
+
YELLOW = HSBK.from_rgb(255, 255, 0)
|
|
696
|
+
YELLOW_GREEN = HSBK.from_rgb(154, 205, 50)
|
|
697
|
+
|
|
698
|
+
# White temperature variants (kelvin-based, warmest to coolest)
|
|
699
|
+
CANDLELIGHT = HSBK(hue=0, saturation=0.0, brightness=1.0, kelvin=KELVIN_CANDLELIGHT)
|
|
700
|
+
SUNSET = HSBK(hue=0, saturation=0.0, brightness=1.0, kelvin=KELVIN_SUNSET)
|
|
701
|
+
AMBER = HSBK(hue=0, saturation=0.0, brightness=1.0, kelvin=KELVIN_AMBER)
|
|
702
|
+
ULTRA_WARM = HSBK(hue=0, saturation=0.0, brightness=1.0, kelvin=KELVIN_ULTRA_WARM)
|
|
703
|
+
INCANDESCENT = HSBK(
|
|
704
|
+
hue=0, saturation=0.0, brightness=1.0, kelvin=KELVIN_INCANDESCENT
|
|
705
|
+
)
|
|
706
|
+
WARM = HSBK(hue=0, saturation=0.0, brightness=1.0, kelvin=KELVIN_WARM)
|
|
707
|
+
NEUTRAL_WARM = HSBK(
|
|
708
|
+
hue=0, saturation=0.0, brightness=1.0, kelvin=KELVIN_NEUTRAL_WARM
|
|
709
|
+
)
|
|
710
|
+
NEUTRAL = HSBK(hue=0, saturation=0.0, brightness=1.0, kelvin=KELVIN_NEUTRAL)
|
|
711
|
+
COOL = HSBK(hue=0, saturation=0.0, brightness=1.0, kelvin=KELVIN_COOL)
|
|
712
|
+
COOL_DAYLIGHT = HSBK(
|
|
713
|
+
hue=0, saturation=0.0, brightness=1.0, kelvin=KELVIN_COOL_DAYLIGHT
|
|
714
|
+
)
|
|
715
|
+
SOFT_DAYLIGHT = HSBK(
|
|
716
|
+
hue=0, saturation=0.0, brightness=1.0, kelvin=KELVIN_SOFT_DAYLIGHT
|
|
717
|
+
)
|
|
718
|
+
DAYLIGHT = HSBK(hue=0, saturation=0.0, brightness=1.0, kelvin=KELVIN_DAYLIGHT)
|
|
719
|
+
NOON_DAYLIGHT = HSBK(
|
|
720
|
+
hue=0, saturation=0.0, brightness=1.0, kelvin=KELVIN_NOON_DAYLIGHT
|
|
721
|
+
)
|
|
722
|
+
BRIGHT_DAYLIGHT = HSBK(
|
|
723
|
+
hue=0, saturation=0.0, brightness=1.0, kelvin=KELVIN_BRIGHT_DAYLIGHT
|
|
724
|
+
)
|
|
725
|
+
CLOUDY_DAYLIGHT = HSBK(
|
|
726
|
+
hue=0, saturation=0.0, brightness=1.0, kelvin=KELVIN_CLOUDY_DAYLIGHT
|
|
727
|
+
)
|
|
728
|
+
BLUE_DAYLIGHT = HSBK(
|
|
729
|
+
hue=0, saturation=0.0, brightness=1.0, kelvin=KELVIN_BLUE_DAYLIGHT
|
|
730
|
+
)
|
|
731
|
+
BLUE_OVERCAST = HSBK(
|
|
732
|
+
hue=0, saturation=0.0, brightness=1.0, kelvin=KELVIN_BLUE_OVERCAST
|
|
733
|
+
)
|
|
734
|
+
BLUE_ICE = HSBK(hue=0, saturation=0.0, brightness=1.0, kelvin=KELVIN_BLUE_ICE)
|
|
735
|
+
|
|
736
|
+
# Pastel variants (reduced saturation)
|
|
543
737
|
PASTEL_RED = HSBK(hue=0, saturation=0.3, brightness=1.0, kelvin=KELVIN_NEUTRAL)
|
|
544
|
-
PASTEL_ORANGE = HSBK(hue=
|
|
738
|
+
PASTEL_ORANGE = HSBK(hue=39, saturation=0.3, brightness=1.0, kelvin=KELVIN_NEUTRAL)
|
|
545
739
|
PASTEL_YELLOW = HSBK(hue=60, saturation=0.3, brightness=1.0, kelvin=KELVIN_NEUTRAL)
|
|
546
740
|
PASTEL_GREEN = HSBK(hue=120, saturation=0.3, brightness=1.0, kelvin=KELVIN_NEUTRAL)
|
|
547
741
|
PASTEL_CYAN = HSBK(hue=180, saturation=0.3, brightness=1.0, kelvin=KELVIN_NEUTRAL)
|
|
548
742
|
PASTEL_BLUE = HSBK(hue=240, saturation=0.3, brightness=1.0, kelvin=KELVIN_NEUTRAL)
|
|
549
|
-
PASTEL_PURPLE = HSBK(hue=
|
|
743
|
+
PASTEL_PURPLE = HSBK(hue=300, saturation=0.3, brightness=1.0, kelvin=KELVIN_NEUTRAL)
|
|
550
744
|
PASTEL_MAGENTA = HSBK(
|
|
551
745
|
hue=300, saturation=0.3, brightness=1.0, kelvin=KELVIN_NEUTRAL
|
|
552
746
|
)
|
|
553
|
-
PASTEL_PINK = HSBK(hue=
|
|
747
|
+
PASTEL_PINK = HSBK(hue=350, saturation=0.3, brightness=1.0, kelvin=KELVIN_NEUTRAL)
|
|
748
|
+
|
|
749
|
+
# Backwards compatibility aliases
|
|
750
|
+
WHITE_WARM = WARM
|
|
751
|
+
WHITE_NEUTRAL = NEUTRAL
|
|
752
|
+
WHITE_COOL = COOL
|
|
753
|
+
WHITE_DAYLIGHT = DAYLIGHT
|
|
@@ -16,7 +16,7 @@ from urllib.request import urlopen
|
|
|
16
16
|
from lifx.const import PRODUCTS_URL
|
|
17
17
|
|
|
18
18
|
|
|
19
|
-
def download_products() -> dict[str, Any] | list[dict[str, Any]]:
|
|
19
|
+
def download_products() -> dict[str, Any] | list[dict[str, Any]] | None:
|
|
20
20
|
"""Download and parse products.json from LIFX GitHub repository.
|
|
21
21
|
|
|
22
22
|
Returns:
|
|
@@ -27,11 +27,13 @@ def download_products() -> dict[str, Any] | list[dict[str, Any]]:
|
|
|
27
27
|
json.JSONDecodeError: If parsing fails
|
|
28
28
|
"""
|
|
29
29
|
parsed_url = urlparse(PRODUCTS_URL)
|
|
30
|
-
if
|
|
31
|
-
|
|
30
|
+
if (
|
|
31
|
+
parsed_url.scheme == "https"
|
|
32
|
+
and parsed_url.netloc == "raw.githubusercontent.com"
|
|
33
|
+
and parsed_url.path.startswith("/LIFX/")
|
|
32
34
|
):
|
|
33
|
-
print(f"Downloading products.json from {
|
|
34
|
-
with urlopen(
|
|
35
|
+
print(f"Downloading products.json from {parsed_url.geturl()}...")
|
|
36
|
+
with urlopen(parsed_url.geturl()) as response: # nosec B310
|
|
35
37
|
products_data = response.read()
|
|
36
38
|
|
|
37
39
|
print("Parsing products specification...")
|
|
@@ -1397,7 +1397,7 @@ from lifx.protocol.base import Packet
|
|
|
1397
1397
|
return code
|
|
1398
1398
|
|
|
1399
1399
|
|
|
1400
|
-
def download_protocol() -> dict[str, Any]:
|
|
1400
|
+
def download_protocol() -> dict[str, Any] | None:
|
|
1401
1401
|
"""Download and parse protocol.yml from LIFX GitHub repository.
|
|
1402
1402
|
|
|
1403
1403
|
Returns:
|
|
@@ -1408,11 +1408,13 @@ def download_protocol() -> dict[str, Any]:
|
|
|
1408
1408
|
yaml.YAMLError: If parsing fails
|
|
1409
1409
|
"""
|
|
1410
1410
|
parsed_url = urlparse(PROTOCOL_URL)
|
|
1411
|
-
if
|
|
1412
|
-
|
|
1411
|
+
if (
|
|
1412
|
+
parsed_url.scheme == "https"
|
|
1413
|
+
and parsed_url.netloc == "raw.githubusercontent.com"
|
|
1414
|
+
and parsed_url.path.startswith("/LIFX/")
|
|
1413
1415
|
):
|
|
1414
|
-
print(f"Downloading protocol.yml from {
|
|
1415
|
-
with urlopen(
|
|
1416
|
+
print(f"Downloading protocol.yml from {parsed_url.geturl()}...")
|
|
1417
|
+
with urlopen(parsed_url.geturl()) as response: # nosec B310
|
|
1416
1418
|
protocol_data = response.read()
|
|
1417
1419
|
|
|
1418
1420
|
print("Parsing protocol specification...")
|
|
@@ -10,8 +10,6 @@ from collections.abc import Generator
|
|
|
10
10
|
from contextlib import contextmanager
|
|
11
11
|
|
|
12
12
|
import pytest
|
|
13
|
-
|
|
14
|
-
# Import emulator library components
|
|
15
13
|
from lifx_emulator import EmulatedLifxServer
|
|
16
14
|
from lifx_emulator.devices import DeviceManager
|
|
17
15
|
from lifx_emulator.factories import (
|
|
@@ -33,9 +31,29 @@ from lifx.devices import HevLight, InfraredLight, Light, MultiZoneLight
|
|
|
33
31
|
from lifx.devices.base import Device
|
|
34
32
|
from lifx.devices.ceiling import CeilingLight
|
|
35
33
|
from lifx.devices.matrix import MatrixLight
|
|
34
|
+
from lifx.exceptions import LifxConnectionError, LifxTimeoutError
|
|
36
35
|
from lifx.network.connection import DeviceConnection
|
|
37
36
|
|
|
38
37
|
|
|
38
|
+
def pytest_addoption(parser: pytest.Parser) -> None:
|
|
39
|
+
"""Add custom command-line options for pytest."""
|
|
40
|
+
parser.addoption(
|
|
41
|
+
"--disable-emulator",
|
|
42
|
+
action="store_true",
|
|
43
|
+
default=False,
|
|
44
|
+
help="Disable lifx-emulator tests for this test run",
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def pytest_set_filtered_exceptions() -> list[type[Exception]]:
|
|
49
|
+
"""Configure pytest-retry to only retry on network-related exceptions.
|
|
50
|
+
|
|
51
|
+
Tests that fail with LifxTimeoutError or LifxConnectionError will be
|
|
52
|
+
retried automatically, as these are typically transient network issues.
|
|
53
|
+
"""
|
|
54
|
+
return [LifxTimeoutError, LifxConnectionError]
|
|
55
|
+
|
|
56
|
+
|
|
39
57
|
def get_free_port() -> int:
|
|
40
58
|
"""Get a free UDP port."""
|
|
41
59
|
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
|
|
@@ -84,15 +102,20 @@ class EmulatorRunner:
|
|
|
84
102
|
|
|
85
103
|
|
|
86
104
|
@pytest.fixture(scope="session")
|
|
87
|
-
def emulator_available() -> bool:
|
|
105
|
+
def emulator_available(request: pytest.FixtureRequest) -> bool:
|
|
88
106
|
"""Check if lifx-emulator-core is available.
|
|
89
107
|
|
|
90
108
|
The library is always available as it's a direct dependency.
|
|
91
|
-
|
|
109
|
+
Emulator tests are enabled on all platforms by default. Use --disable-emulator
|
|
110
|
+
to disable them if needed.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
request: Pytest fixture request for accessing command-line options
|
|
92
114
|
"""
|
|
93
|
-
|
|
115
|
+
# Check command-line flags
|
|
116
|
+
disable_emulator = request.config.getoption("--disable-emulator", default=False)
|
|
94
117
|
|
|
95
|
-
if
|
|
118
|
+
if disable_emulator:
|
|
96
119
|
return False
|
|
97
120
|
|
|
98
121
|
try:
|
|
@@ -563,7 +563,7 @@ class TestMatrixLight:
|
|
|
563
563
|
2. Send two set64() messages to fb_index=1 (non-visible buffer)
|
|
564
564
|
3. Send copy_frame_buffer() to copy fb_index=1 -> fb_index=0
|
|
565
565
|
4. Send two get64() messages to retrieve all 128 zones from fb_index=0
|
|
566
|
-
5. Verify the colors match what we set (blue top half,
|
|
566
|
+
5. Verify the colors match what we set (blue top half, lime bottom half)
|
|
567
567
|
"""
|
|
568
568
|
matrix = ceiling_device
|
|
569
569
|
async with matrix:
|
|
@@ -581,9 +581,11 @@ class TestMatrixLight:
|
|
|
581
581
|
tile_index=0, colors=white_colors, duration=0
|
|
582
582
|
)
|
|
583
583
|
|
|
584
|
-
# Create colors: first 64 zones blue, second 64 zones
|
|
584
|
+
# Create colors: first 64 zones blue, second 64 zones lime
|
|
585
|
+
# Note: Colors.LIME (RGB 0,255,0) has full brightness, while
|
|
586
|
+
# Colors.GREEN (RGB 0,128,0) follows CSS3 spec with 50% brightness
|
|
585
587
|
blue_colors = [Colors.BLUE] * 64
|
|
586
|
-
|
|
588
|
+
lime_colors = [Colors.LIME] * 64
|
|
587
589
|
|
|
588
590
|
# Step 1: Set first 64 zones (rows 0-3) to blue in frame buffer 1
|
|
589
591
|
await matrix.set64(
|
|
@@ -597,7 +599,7 @@ class TestMatrixLight:
|
|
|
597
599
|
fb_index=1, # Write to non-visible buffer
|
|
598
600
|
)
|
|
599
601
|
|
|
600
|
-
# Step 2: Set second 64 zones (rows 4-7) to
|
|
602
|
+
# Step 2: Set second 64 zones (rows 4-7) to lime in frame buffer 1
|
|
601
603
|
await matrix.set64(
|
|
602
604
|
tile_index=0,
|
|
603
605
|
length=1,
|
|
@@ -605,7 +607,7 @@ class TestMatrixLight:
|
|
|
605
607
|
y=4, # Start at row 4 (after first 64 zones)
|
|
606
608
|
width=tile.width,
|
|
607
609
|
duration=0,
|
|
608
|
-
colors=
|
|
610
|
+
colors=lime_colors,
|
|
609
611
|
fb_index=1, # Write to non-visible buffer
|
|
610
612
|
)
|
|
611
613
|
|
|
@@ -628,7 +630,7 @@ class TestMatrixLight:
|
|
|
628
630
|
assert first_64_colors[0].saturation > 0.9
|
|
629
631
|
assert first_64_colors[0].brightness > 0.9
|
|
630
632
|
|
|
631
|
-
# Verify second 64 zones are
|
|
633
|
+
# Verify second 64 zones are lime (hue ~120)
|
|
632
634
|
assert 110 < second_64_colors[0].hue < 130
|
|
633
635
|
assert second_64_colors[0].saturation > 0.9
|
|
634
636
|
assert second_64_colors[0].brightness > 0.9
|
|
@@ -337,7 +337,7 @@ wheels = [
|
|
|
337
337
|
|
|
338
338
|
[[package]]
|
|
339
339
|
name = "lifx-async"
|
|
340
|
-
version = "4.
|
|
340
|
+
version = "4.9.0"
|
|
341
341
|
source = { editable = "." }
|
|
342
342
|
|
|
343
343
|
[package.dev-dependencies]
|
|
@@ -352,6 +352,7 @@ dev = [
|
|
|
352
352
|
{ name = "pytest" },
|
|
353
353
|
{ name = "pytest-asyncio" },
|
|
354
354
|
{ name = "pytest-cov" },
|
|
355
|
+
{ name = "pytest-retry" },
|
|
355
356
|
{ name = "pytest-sugar" },
|
|
356
357
|
{ name = "pyyaml" },
|
|
357
358
|
{ name = "ruff" },
|
|
@@ -371,6 +372,7 @@ dev = [
|
|
|
371
372
|
{ name = "pytest", specifier = ">=8.4.2" },
|
|
372
373
|
{ name = "pytest-asyncio", specifier = ">=0.24.0" },
|
|
373
374
|
{ name = "pytest-cov", specifier = ">=7.0.0" },
|
|
375
|
+
{ name = "pytest-retry", specifier = ">=1.7.0" },
|
|
374
376
|
{ name = "pytest-sugar", specifier = ">=1.1.1" },
|
|
375
377
|
{ name = "pyyaml", specifier = ">=6.0.3" },
|
|
376
378
|
{ name = "ruff", specifier = ">=0.14.2" },
|
|
@@ -933,6 +935,18 @@ wheels = [
|
|
|
933
935
|
{ url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" },
|
|
934
936
|
]
|
|
935
937
|
|
|
938
|
+
[[package]]
|
|
939
|
+
name = "pytest-retry"
|
|
940
|
+
version = "1.7.0"
|
|
941
|
+
source = { registry = "https://pypi.org/simple" }
|
|
942
|
+
dependencies = [
|
|
943
|
+
{ name = "pytest" },
|
|
944
|
+
]
|
|
945
|
+
sdist = { url = "https://files.pythonhosted.org/packages/c5/5b/607b017994cca28de3a1ad22a3eee8418e5d428dcd8ec25b26b18e995a73/pytest_retry-1.7.0.tar.gz", hash = "sha256:f8d52339f01e949df47c11ba9ee8d5b362f5824dff580d3870ec9ae0057df80f", size = 19977, upload-time = "2025-01-19T01:56:13.115Z" }
|
|
946
|
+
wheels = [
|
|
947
|
+
{ url = "https://files.pythonhosted.org/packages/7c/ff/3266c8a73b9b93c4b14160a7e2b31d1e1088e28ed29f4c2d93ae34093bfd/pytest_retry-1.7.0-py3-none-any.whl", hash = "sha256:a2dac85b79a4e2375943f1429479c65beb6c69553e7dae6b8332be47a60954f4", size = 13775, upload-time = "2025-01-19T01:56:11.199Z" },
|
|
948
|
+
]
|
|
949
|
+
|
|
936
950
|
[[package]]
|
|
937
951
|
name = "pytest-sugar"
|
|
938
952
|
version = "1.1.1"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|