lifx-async 4.7.4__tar.gz → 4.8.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.7.4 → lifx_async-4.8.0}/.github/workflows/ci.yml +4 -4
- {lifx_async-4.7.4 → lifx_async-4.8.0}/.github/workflows/docs.yml +3 -3
- {lifx_async-4.7.4 → lifx_async-4.8.0}/CLAUDE.md +19 -4
- {lifx_async-4.7.4 → lifx_async-4.8.0}/PKG-INFO +1 -1
- {lifx_async-4.7.4 → lifx_async-4.8.0}/docs/api/index.md +12 -4
- {lifx_async-4.7.4 → lifx_async-4.8.0}/docs/changelog.md +16 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/docs/getting-started/quickstart.md +21 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/docs/user-guide/advanced-usage.md +71 -0
- lifx_async-4.8.0/examples/14_mdns_discovery.py +76 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/pyproject.toml +1 -1
- lifx_async-4.8.0/scripts/mdns_probe.py +660 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/src/lifx/__init__.py +6 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/src/lifx/api.py +54 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/src/lifx/const.py +13 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/src/lifx/devices/ceiling.py +75 -17
- lifx_async-4.8.0/src/lifx/network/mdns/__init__.py +58 -0
- lifx_async-4.8.0/src/lifx/network/mdns/discovery.py +403 -0
- lifx_async-4.8.0/src/lifx/network/mdns/dns.py +356 -0
- lifx_async-4.8.0/src/lifx/network/mdns/transport.py +313 -0
- lifx_async-4.8.0/src/lifx/network/mdns/types.py +35 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_api/test_api_discovery.py +81 -1
- {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_devices/test_ceiling.py +32 -6
- lifx_async-4.8.0/tests/test_network/test_mdns/__init__.py +1 -0
- lifx_async-4.8.0/tests/test_network/test_mdns/conftest.py +152 -0
- lifx_async-4.8.0/tests/test_network/test_mdns/test_discovery.py +733 -0
- lifx_async-4.8.0/tests/test_network/test_mdns/test_dns.py +398 -0
- lifx_async-4.8.0/tests/test_network/test_mdns/test_transport.py +433 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/uv.lock +1 -1
- {lifx_async-4.7.4 → lifx_async-4.8.0}/.claude/settings.json +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/.github/dependabot.yml +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/.github/labeler.yml +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/.github/workflows/pr-automation.yml +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/.gitignore +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/.pre-commit-config.yaml +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/LICENSE +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/README.md +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/context7.json +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/docs/api/colors.md +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/docs/api/devices.md +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/docs/api/effects.md +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/docs/api/exceptions.md +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/docs/api/high-level.md +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/docs/api/network.md +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/docs/api/protocol.md +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/docs/api/themes.md +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/docs/architecture/effects-architecture.md +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/docs/architecture/overview.md +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/docs/faq.md +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/docs/getting-started/effects.md +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/docs/getting-started/installation.md +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/docs/getting-started/themes.md +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/docs/index.md +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/docs/migration/effect-api-changes.md +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/docs/stylesheets/extra.css +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/docs/user-guide/ceiling-lights.md +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/docs/user-guide/effects-custom.md +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/docs/user-guide/effects-troubleshooting.md +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/docs/user-guide/protocol-deep-dive.md +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/docs/user-guide/themes.md +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/docs/user-guide/troubleshooting.md +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/examples/01_simple_discovery.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/examples/02_simple_control.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/examples/03_waveforms.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/examples/04_logging.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/examples/06_pulse_effect.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/examples/07_colorloop_effect.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/examples/08_custom_effect.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/examples/09_background_effect.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/examples/10_find_specific_devices.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/examples/11_matrix_basic.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/examples/12_matrix_effects.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/examples/13_matrix_large.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/mkdocs.yml +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/renovate.json +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/src/lifx/color.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/src/lifx/devices/__init__.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/src/lifx/devices/base.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/src/lifx/devices/hev.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/src/lifx/devices/infrared.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/src/lifx/devices/light.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/src/lifx/devices/matrix.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/src/lifx/devices/multizone.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/src/lifx/effects/__init__.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/src/lifx/effects/base.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/src/lifx/effects/colorloop.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/src/lifx/effects/conductor.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/src/lifx/effects/const.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/src/lifx/effects/models.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/src/lifx/effects/pulse.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/src/lifx/effects/state_manager.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/src/lifx/exceptions.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/src/lifx/network/__init__.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/src/lifx/network/connection.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/src/lifx/network/discovery.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/src/lifx/network/message.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/src/lifx/network/transport.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/src/lifx/products/__init__.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/src/lifx/products/generator.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/src/lifx/products/quirks.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/src/lifx/products/registry.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/src/lifx/protocol/__init__.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/src/lifx/protocol/base.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/src/lifx/protocol/generator.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/src/lifx/protocol/header.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/src/lifx/protocol/models.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/src/lifx/protocol/packets.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/src/lifx/protocol/protocol_types.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/src/lifx/protocol/serializer.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/src/lifx/py.typed +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/src/lifx/theme/__init__.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/src/lifx/theme/canvas.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/src/lifx/theme/generators.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/src/lifx/theme/library.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/src/lifx/theme/theme.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/__init__.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/conftest.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_api/__init__.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_api/test_api_apply_theme.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_api/test_api_batch_errors.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_api/test_api_batch_operations.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_api/test_api_organization.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_color.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_devices/__init__.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_devices/conftest.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_devices/test_base.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_devices/test_hev.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_devices/test_infrared.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_devices/test_light.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_devices/test_mac_address.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_devices/test_matrix.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_devices/test_multizone.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_devices/test_state_ceiling.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_devices/test_state_hev.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_devices/test_state_infrared.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_devices/test_state_light.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_devices/test_state_management.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_devices/test_state_matrix.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_devices/test_state_multizone.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_effects/__init__.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_effects/test_base.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_effects/test_capability_filtering.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_effects/test_colorloop.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_effects/test_integration.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_effects/test_models.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_effects/test_pulse.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_effects/test_state_manager.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_network/__init__.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_network/test_concurrent_requests.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_network/test_connection.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_network/test_discovery_devices.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_network/test_discovery_errors.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_network/test_message.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_network/test_message_advanced.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_network/test_transport.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_products/test_product_generator.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_products/test_registry.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_protocol/test_generated.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_protocol/test_header.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_protocol/test_protocol_generator.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_protocol/test_serializer.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_theme/__init__.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_theme/conftest.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_theme/test_apply_theme.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_theme/test_canvas.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_theme/test_generators.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_theme/test_library.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_theme/test_theme.py +0 -0
- {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_utils.py +0 -0
|
@@ -40,7 +40,7 @@ jobs:
|
|
|
40
40
|
python-version: ${{ env.PYTHON_VERSION }}
|
|
41
41
|
|
|
42
42
|
- name: Install uv
|
|
43
|
-
uses: astral-sh/setup-uv@
|
|
43
|
+
uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7
|
|
44
44
|
with:
|
|
45
45
|
version: ${{ env.UV_VERSION }}
|
|
46
46
|
python-version: ${{ env.PYTHON_VERSION }}
|
|
@@ -82,7 +82,7 @@ jobs:
|
|
|
82
82
|
python-version: ${{ matrix.python-version }}
|
|
83
83
|
|
|
84
84
|
- name: Install uv
|
|
85
|
-
uses: astral-sh/setup-uv@
|
|
85
|
+
uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7
|
|
86
86
|
with:
|
|
87
87
|
version: ${{ env.UV_VERSION }}
|
|
88
88
|
python-version: ${{ matrix.python-version }}
|
|
@@ -134,7 +134,7 @@ jobs:
|
|
|
134
134
|
python-version: ${{ env.PYTHON_VERSION }}
|
|
135
135
|
|
|
136
136
|
- name: Install uv
|
|
137
|
-
uses: astral-sh/setup-uv@
|
|
137
|
+
uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7
|
|
138
138
|
with:
|
|
139
139
|
version: ${{ env.UV_VERSION }}
|
|
140
140
|
python-version: ${{ env.PYTHON_VERSION }}
|
|
@@ -198,7 +198,7 @@ jobs:
|
|
|
198
198
|
python-version: ${{ env.PYTHON_VERSION }}
|
|
199
199
|
|
|
200
200
|
- name: Install uv
|
|
201
|
-
uses: astral-sh/setup-uv@
|
|
201
|
+
uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7
|
|
202
202
|
with:
|
|
203
203
|
version: ${{ env.UV_VERSION }}
|
|
204
204
|
python-version: ${{ env.PYTHON_VERSION }}
|
|
@@ -35,7 +35,7 @@ jobs:
|
|
|
35
35
|
python-version: ${{ env.PYTHON_VERSION }}
|
|
36
36
|
|
|
37
37
|
- name: Install uv
|
|
38
|
-
uses: astral-sh/setup-uv@
|
|
38
|
+
uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7
|
|
39
39
|
with:
|
|
40
40
|
version: ${{ env.UV_VERSION }}
|
|
41
41
|
python-version: ${{ env.PYTHON_VERSION }}
|
|
@@ -69,7 +69,7 @@ jobs:
|
|
|
69
69
|
python-version: ${{ env.PYTHON_VERSION }}
|
|
70
70
|
|
|
71
71
|
- name: Install uv
|
|
72
|
-
uses: astral-sh/setup-uv@
|
|
72
|
+
uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7
|
|
73
73
|
with:
|
|
74
74
|
version: ${{ env.UV_VERSION }}
|
|
75
75
|
python-version: ${{ env.PYTHON_VERSION }}
|
|
@@ -97,7 +97,7 @@ jobs:
|
|
|
97
97
|
python-version: ${{ env.PYTHON_VERSION }}
|
|
98
98
|
|
|
99
99
|
- name: Install uv
|
|
100
|
-
uses: astral-sh/setup-uv@
|
|
100
|
+
uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7
|
|
101
101
|
with:
|
|
102
102
|
version: ${{ env.UV_VERSION }}
|
|
103
103
|
python-version: ${{ env.PYTHON_VERSION }}
|
|
@@ -122,6 +122,11 @@ uv run mkdocs gh-deploy
|
|
|
122
122
|
- `discovery.py`: Device discovery via broadcast with `DiscoveredDevice` dataclass
|
|
123
123
|
- `connection.py`: Device connection with retry logic and lazy opening
|
|
124
124
|
- `message.py`: Message building and parsing with `MessageBuilder`
|
|
125
|
+
- `mdns/`: mDNS/DNS-SD discovery module (zero-dependency, stdlib only)
|
|
126
|
+
- `discovery.py`: `discover_lifx_services()` and `discover_devices_mdns()`
|
|
127
|
+
- `dns.py`: DNS wire format parser for PTR, SRV, A, TXT records
|
|
128
|
+
- `transport.py`: `MdnsTransport` class for multicast UDP
|
|
129
|
+
- `types.py`: `LifxServiceRecord` dataclass
|
|
125
130
|
- Lazy connection opening (auto-opens on first request)
|
|
126
131
|
|
|
127
132
|
3. **Device Layer** (`src/lifx/devices/`)
|
|
@@ -136,7 +141,8 @@ uv run mkdocs gh-deploy
|
|
|
136
141
|
|
|
137
142
|
4. **High-Level API** (`src/lifx/api.py`)
|
|
138
143
|
|
|
139
|
-
- `discover()`: Async generator yielding devices
|
|
144
|
+
- `discover()`: Async generator yielding devices via UDP broadcast
|
|
145
|
+
- `discover_mdns()`: Async generator yielding devices via mDNS (faster, single query)
|
|
140
146
|
- `find_by_serial()`: Find specific device by serial number
|
|
141
147
|
- `find_by_label()`: Async generator yielding devices matching label (exact or substring)
|
|
142
148
|
- `find_by_ip()`: Find device by IP address using targeted broadcast
|
|
@@ -616,9 +622,9 @@ The `discover_devices()` function implements DoS protection through:
|
|
|
616
622
|
|
|
617
623
|
## Testing Strategy
|
|
618
624
|
|
|
619
|
-
- **
|
|
625
|
+
- **1075+ tests total** (comprehensive coverage across all layers)
|
|
620
626
|
- **Protocol Layer**: 136 tests (serialization, header, packets, generator validation)
|
|
621
|
-
- **Network Layer**:
|
|
627
|
+
- **Network Layer**: 149 tests (transport, discovery, connection, message, mDNS, async generator requests)
|
|
622
628
|
- **Device Layer**: 157 tests (base, light, hev, infrared, multizone, tile)
|
|
623
629
|
- **API Layer**: 60 tests (discovery, batch operations, organization, themes, error handling)
|
|
624
630
|
- **Utilities**: 329 tests (color conversion, product registry, RGB roundtrip, effects, themes)
|
|
@@ -675,7 +681,11 @@ tests/
|
|
|
675
681
|
│ ├── test_discovery_errors.py # Discovery error handling tests
|
|
676
682
|
│ ├── test_connection.py # Connection management tests
|
|
677
683
|
│ ├── test_message.py # Message building/parsing tests
|
|
678
|
-
│
|
|
684
|
+
│ ├── test_concurrent_requests.py # Concurrent request tests
|
|
685
|
+
│ └── test_mdns/ # mDNS discovery tests
|
|
686
|
+
│ ├── test_dns.py # DNS parser tests
|
|
687
|
+
│ ├── test_transport.py # mDNS transport tests
|
|
688
|
+
│ └── test_discovery.py # mDNS discovery tests
|
|
679
689
|
├── test_devices/
|
|
680
690
|
│ ├── test_base.py # Base device tests
|
|
681
691
|
│ ├── test_light.py # Light device tests
|
|
@@ -792,6 +802,11 @@ Critical constants are defined in `src/lifx/const.py`:
|
|
|
792
802
|
- `MAX_RESPONSE_TIME`: Maximum response time for local network devices (0.5s)
|
|
793
803
|
- `IDLE_TIMEOUT_MULTIPLIER`: Idle timeout after last response (4.0)
|
|
794
804
|
|
|
805
|
+
**mDNS Constants:**
|
|
806
|
+
- `MDNS_ADDRESS`: Multicast address for mDNS (224.0.0.251)
|
|
807
|
+
- `MDNS_PORT`: mDNS port (5353)
|
|
808
|
+
- `LIFX_MDNS_SERVICE`: LIFX service type (_lifx._udp.local)
|
|
809
|
+
|
|
795
810
|
**UUID Namespaces:**
|
|
796
811
|
- `LIFX_LOCATION_NAMESPACE`: UUID namespace for generating location UUIDs
|
|
797
812
|
- `LIFX_GROUP_NAMESPACE`: UUID namespace for generating group UUIDs
|
|
@@ -20,9 +20,14 @@ lifx/
|
|
|
20
20
|
│ └── matrix.py # MatrixLight (2D matrix devices: tiles, candle, path)
|
|
21
21
|
├── network/ # Network layer
|
|
22
22
|
│ ├── connection.py # Device connections with lazy opening
|
|
23
|
-
│ ├── discovery.py # Network device discovery
|
|
23
|
+
│ ├── discovery.py # Network device discovery (UDP broadcast)
|
|
24
24
|
│ ├── message.py # Message building and parsing
|
|
25
|
-
│
|
|
25
|
+
│ ├── transport.py # UDP transport
|
|
26
|
+
│ └── mdns/ # mDNS/DNS-SD discovery
|
|
27
|
+
│ ├── discovery.py # mDNS discovery functions
|
|
28
|
+
│ ├── dns.py # DNS wire format parser
|
|
29
|
+
│ ├── transport.py # Multicast UDP transport
|
|
30
|
+
│ └── types.py # LifxServiceRecord dataclass
|
|
26
31
|
├── products/ # Product registry
|
|
27
32
|
│ ├── registry.py # Auto-generated product database
|
|
28
33
|
│ ├── generator.py # Generator to download/parse products.json
|
|
@@ -43,7 +48,8 @@ lifx/
|
|
|
43
48
|
|
|
44
49
|
Main entry points for most users:
|
|
45
50
|
|
|
46
|
-
- [`discover()`](high-level.md#lifx.api.discover) -
|
|
51
|
+
- [`discover()`](high-level.md#lifx.api.discover) - Device discovery via UDP broadcast
|
|
52
|
+
- [`discover_mdns()`](high-level.md#lifx.api.discover_mdns) - Device discovery via mDNS (faster)
|
|
47
53
|
- [`find_by_serial()`](high-level.md#lifx.api.find_by_serial) - Find device by serial number
|
|
48
54
|
- [`find_by_label()`](high-level.md#lifx.api.find_by_label) - Find devices by label (exact or substring)
|
|
49
55
|
- [`find_by_ip()`](high-level.md#lifx.api.find_by_ip) - Find device by IP address
|
|
@@ -71,7 +77,9 @@ Work with colors:
|
|
|
71
77
|
|
|
72
78
|
Low-level network operations:
|
|
73
79
|
|
|
74
|
-
- [`discover_devices()`](network.md#lifx.network.discovery.discover_devices) - Low-level discovery
|
|
80
|
+
- [`discover_devices()`](network.md#lifx.network.discovery.discover_devices) - Low-level UDP discovery
|
|
81
|
+
- [`discover_lifx_services()`](network.md#lifx.network.mdns.discover_lifx_services) - Low-level mDNS discovery
|
|
82
|
+
- [`LifxServiceRecord`](network.md#lifx.network.mdns.LifxServiceRecord) - mDNS service record
|
|
75
83
|
- [`DeviceConnection`](network.md#lifx.network.connection.DeviceConnection) - Device connections
|
|
76
84
|
|
|
77
85
|
### Products Registry
|
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
<!-- version list -->
|
|
4
4
|
|
|
5
|
+
## v4.8.0 (2025-12-20)
|
|
6
|
+
|
|
7
|
+
### Features
|
|
8
|
+
|
|
9
|
+
- **network**: Add mDNS/DNS-SD discovery for LIFX devices
|
|
10
|
+
([`f25987d`](https://github.com/Djelibeybi/lifx-async/commit/f25987d9357d395209dd7d346787671d85bf1371))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
## v4.7.5 (2025-12-16)
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
- **devices**: Override set_color in CeilingLight to track component state
|
|
18
|
+
([`0d20563`](https://github.com/Djelibeybi/lifx-async/commit/0d20563c170363229ab17620398283bd85ee7829))
|
|
19
|
+
|
|
20
|
+
|
|
5
21
|
## v4.7.4 (2025-12-16)
|
|
6
22
|
|
|
7
23
|
### Performance Improvements
|
|
@@ -24,6 +24,27 @@ async def main():
|
|
|
24
24
|
asyncio.run(main())
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
+
**Alternative: mDNS Discovery**
|
|
28
|
+
|
|
29
|
+
For faster discovery with device type detection in a single query:
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
import asyncio
|
|
33
|
+
from lifx import discover_mdns
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
async def main():
|
|
37
|
+
async for device in discover_mdns():
|
|
38
|
+
async with device:
|
|
39
|
+
color, power, label = await device.get_color()
|
|
40
|
+
print(f"{label}: {type(device).__name__}")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
asyncio.run(main())
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
mDNS discovery is faster because it gets device type information directly from the mDNS response, eliminating extra network queries.
|
|
47
|
+
|
|
27
48
|
### 2. Control a Light
|
|
28
49
|
|
|
29
50
|
Turn on the first discovered light, then change its color:
|
|
@@ -4,6 +4,7 @@ This guide covers advanced lifx patterns and techniques for building robust LIFX
|
|
|
4
4
|
|
|
5
5
|
## Table of Contents
|
|
6
6
|
|
|
7
|
+
- [Discovery Methods](#discovery-methods)
|
|
7
8
|
- [Storing State](#storing-state)
|
|
8
9
|
- [Connection Management](#connection-management)
|
|
9
10
|
- [Concurrency Patterns](#concurrency-patterns)
|
|
@@ -12,6 +13,76 @@ This guide covers advanced lifx patterns and techniques for building robust LIFX
|
|
|
12
13
|
- [Custom Effects](#custom-effects)
|
|
13
14
|
- [Performance Optimization](#performance-optimization)
|
|
14
15
|
|
|
16
|
+
## Discovery Methods
|
|
17
|
+
|
|
18
|
+
lifx-async provides two discovery methods with different trade-offs:
|
|
19
|
+
|
|
20
|
+
### UDP Broadcast Discovery
|
|
21
|
+
|
|
22
|
+
The traditional discovery method broadcasts to all devices on the network:
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
from lifx import discover
|
|
26
|
+
|
|
27
|
+
async def broadcast_discovery():
|
|
28
|
+
async for device in discover(timeout=5.0):
|
|
29
|
+
async with device:
|
|
30
|
+
color, power, label = await device.get_color()
|
|
31
|
+
print(f"Found: {label} ({type(device).__name__})")
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
**Characteristics:**
|
|
35
|
+
|
|
36
|
+
- Sends 1 broadcast + N queries (one per device for type detection)
|
|
37
|
+
- Works on any local network
|
|
38
|
+
- May miss devices on other subnets
|
|
39
|
+
|
|
40
|
+
### mDNS Discovery
|
|
41
|
+
|
|
42
|
+
mDNS discovery uses DNS-SD to find devices with a single multicast query:
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from lifx import discover_mdns
|
|
46
|
+
|
|
47
|
+
async def mdns_discovery():
|
|
48
|
+
async for device in discover_mdns(timeout=5.0):
|
|
49
|
+
async with device:
|
|
50
|
+
color, power, label = await device.get_color()
|
|
51
|
+
print(f"Found: {label} ({type(device).__name__})")
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**Characteristics:**
|
|
55
|
+
|
|
56
|
+
- Single network query (device type in TXT record)
|
|
57
|
+
- Faster discovery with immediate type detection
|
|
58
|
+
- Can work across subnets with an mDNS reflector
|
|
59
|
+
- Zero dependencies (uses Python stdlib only)
|
|
60
|
+
|
|
61
|
+
### Low-Level mDNS API
|
|
62
|
+
|
|
63
|
+
For raw mDNS data without device instantiation:
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
from lifx import discover_lifx_services
|
|
67
|
+
|
|
68
|
+
async def raw_mdns_discovery():
|
|
69
|
+
async for record in discover_lifx_services(timeout=5.0):
|
|
70
|
+
print(f"Serial: {record.serial}")
|
|
71
|
+
print(f"IP: {record.ip}:{record.port}")
|
|
72
|
+
print(f"Product ID: {record.product_id}")
|
|
73
|
+
print(f"Firmware: {record.firmware}")
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Choosing a Discovery Method
|
|
77
|
+
|
|
78
|
+
| Scenario | Recommended Method |
|
|
79
|
+
|----------|-------------------|
|
|
80
|
+
| General use | `discover()` or `discover_mdns()` |
|
|
81
|
+
| Fastest discovery | `discover_mdns()` |
|
|
82
|
+
| Cross-subnet (with reflector) | `discover_mdns()` |
|
|
83
|
+
| Maximum compatibility | `discover()` |
|
|
84
|
+
| Raw device data | `discover_lifx_services()` |
|
|
85
|
+
|
|
15
86
|
## Storing State
|
|
16
87
|
|
|
17
88
|
Device properties return cached values that were last retrieved from the device.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Example: Discover LIFX devices using mDNS.
|
|
3
|
+
|
|
4
|
+
This example demonstrates how to discover LIFX devices on your local network
|
|
5
|
+
using mDNS/DNS-SD instead of UDP broadcast. mDNS discovery has several advantages:
|
|
6
|
+
|
|
7
|
+
- Single network query (vs 1+N for broadcast discovery)
|
|
8
|
+
- Device type detection without extra queries (from TXT record)
|
|
9
|
+
- Can work across subnets with an mDNS reflector
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
uv run python examples/14_mdns_discovery.py
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import asyncio
|
|
18
|
+
|
|
19
|
+
import lifx
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
async def discover_with_mdns() -> None:
|
|
23
|
+
"""Discover devices using mDNS and print their info."""
|
|
24
|
+
print("Discovering LIFX devices via mDNS...")
|
|
25
|
+
print("-" * 60)
|
|
26
|
+
|
|
27
|
+
device_count = 0
|
|
28
|
+
async for device in lifx.discover_mdns(timeout=5.0):
|
|
29
|
+
device_count += 1
|
|
30
|
+
async with device:
|
|
31
|
+
# get_color() returns color, power, and label in a single request
|
|
32
|
+
color, power, label = await device.get_color()
|
|
33
|
+
power_str = "ON" if power > 0 else "OFF"
|
|
34
|
+
|
|
35
|
+
print(f"\nDevice #{device_count}")
|
|
36
|
+
print(f" Type: {type(device).__name__}")
|
|
37
|
+
print(f" Label: {label}")
|
|
38
|
+
print(f" Serial: {device.serial}")
|
|
39
|
+
print(f" IP: {device.ip}:{device.port}")
|
|
40
|
+
print(f" Power: {power_str}")
|
|
41
|
+
print(
|
|
42
|
+
f" Color: H={color.hue:.0f} S={color.saturation:.0%} "
|
|
43
|
+
f"B={color.brightness:.0%} K={color.kelvin}"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
print("-" * 60)
|
|
47
|
+
if device_count == 0:
|
|
48
|
+
print("No devices found. Make sure LIFX devices are on your network.")
|
|
49
|
+
else:
|
|
50
|
+
print(f"Found {device_count} device(s)")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
async def discover_raw_records() -> None:
|
|
54
|
+
"""Discover devices using low-level mDNS API (raw service records)."""
|
|
55
|
+
print("\nLow-level mDNS discovery (raw service records):")
|
|
56
|
+
print("-" * 60)
|
|
57
|
+
|
|
58
|
+
async for record in lifx.discover_lifx_services(timeout=3.0):
|
|
59
|
+
print(f" Serial: {record.serial}")
|
|
60
|
+
print(f" IP: {record.ip}:{record.port}")
|
|
61
|
+
print(f" Product ID: {record.product_id}")
|
|
62
|
+
print(f" Firmware: {record.firmware}")
|
|
63
|
+
print()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
async def main() -> None:
|
|
67
|
+
"""Run both discovery methods."""
|
|
68
|
+
# High-level API - yields device instances (Light, MatrixLight, etc.)
|
|
69
|
+
await discover_with_mdns()
|
|
70
|
+
|
|
71
|
+
# Low-level API - yields raw mDNS service records
|
|
72
|
+
await discover_raw_records()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
if __name__ == "__main__":
|
|
76
|
+
asyncio.run(main())
|