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.
Files changed (168) hide show
  1. {lifx_async-4.7.4 → lifx_async-4.8.0}/.github/workflows/ci.yml +4 -4
  2. {lifx_async-4.7.4 → lifx_async-4.8.0}/.github/workflows/docs.yml +3 -3
  3. {lifx_async-4.7.4 → lifx_async-4.8.0}/CLAUDE.md +19 -4
  4. {lifx_async-4.7.4 → lifx_async-4.8.0}/PKG-INFO +1 -1
  5. {lifx_async-4.7.4 → lifx_async-4.8.0}/docs/api/index.md +12 -4
  6. {lifx_async-4.7.4 → lifx_async-4.8.0}/docs/changelog.md +16 -0
  7. {lifx_async-4.7.4 → lifx_async-4.8.0}/docs/getting-started/quickstart.md +21 -0
  8. {lifx_async-4.7.4 → lifx_async-4.8.0}/docs/user-guide/advanced-usage.md +71 -0
  9. lifx_async-4.8.0/examples/14_mdns_discovery.py +76 -0
  10. {lifx_async-4.7.4 → lifx_async-4.8.0}/pyproject.toml +1 -1
  11. lifx_async-4.8.0/scripts/mdns_probe.py +660 -0
  12. {lifx_async-4.7.4 → lifx_async-4.8.0}/src/lifx/__init__.py +6 -0
  13. {lifx_async-4.7.4 → lifx_async-4.8.0}/src/lifx/api.py +54 -0
  14. {lifx_async-4.7.4 → lifx_async-4.8.0}/src/lifx/const.py +13 -0
  15. {lifx_async-4.7.4 → lifx_async-4.8.0}/src/lifx/devices/ceiling.py +75 -17
  16. lifx_async-4.8.0/src/lifx/network/mdns/__init__.py +58 -0
  17. lifx_async-4.8.0/src/lifx/network/mdns/discovery.py +403 -0
  18. lifx_async-4.8.0/src/lifx/network/mdns/dns.py +356 -0
  19. lifx_async-4.8.0/src/lifx/network/mdns/transport.py +313 -0
  20. lifx_async-4.8.0/src/lifx/network/mdns/types.py +35 -0
  21. {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_api/test_api_discovery.py +81 -1
  22. {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_devices/test_ceiling.py +32 -6
  23. lifx_async-4.8.0/tests/test_network/test_mdns/__init__.py +1 -0
  24. lifx_async-4.8.0/tests/test_network/test_mdns/conftest.py +152 -0
  25. lifx_async-4.8.0/tests/test_network/test_mdns/test_discovery.py +733 -0
  26. lifx_async-4.8.0/tests/test_network/test_mdns/test_dns.py +398 -0
  27. lifx_async-4.8.0/tests/test_network/test_mdns/test_transport.py +433 -0
  28. {lifx_async-4.7.4 → lifx_async-4.8.0}/uv.lock +1 -1
  29. {lifx_async-4.7.4 → lifx_async-4.8.0}/.claude/settings.json +0 -0
  30. {lifx_async-4.7.4 → lifx_async-4.8.0}/.github/dependabot.yml +0 -0
  31. {lifx_async-4.7.4 → lifx_async-4.8.0}/.github/labeler.yml +0 -0
  32. {lifx_async-4.7.4 → lifx_async-4.8.0}/.github/workflows/pr-automation.yml +0 -0
  33. {lifx_async-4.7.4 → lifx_async-4.8.0}/.gitignore +0 -0
  34. {lifx_async-4.7.4 → lifx_async-4.8.0}/.pre-commit-config.yaml +0 -0
  35. {lifx_async-4.7.4 → lifx_async-4.8.0}/LICENSE +0 -0
  36. {lifx_async-4.7.4 → lifx_async-4.8.0}/README.md +0 -0
  37. {lifx_async-4.7.4 → lifx_async-4.8.0}/context7.json +0 -0
  38. {lifx_async-4.7.4 → lifx_async-4.8.0}/docs/api/colors.md +0 -0
  39. {lifx_async-4.7.4 → lifx_async-4.8.0}/docs/api/devices.md +0 -0
  40. {lifx_async-4.7.4 → lifx_async-4.8.0}/docs/api/effects.md +0 -0
  41. {lifx_async-4.7.4 → lifx_async-4.8.0}/docs/api/exceptions.md +0 -0
  42. {lifx_async-4.7.4 → lifx_async-4.8.0}/docs/api/high-level.md +0 -0
  43. {lifx_async-4.7.4 → lifx_async-4.8.0}/docs/api/network.md +0 -0
  44. {lifx_async-4.7.4 → lifx_async-4.8.0}/docs/api/protocol.md +0 -0
  45. {lifx_async-4.7.4 → lifx_async-4.8.0}/docs/api/themes.md +0 -0
  46. {lifx_async-4.7.4 → lifx_async-4.8.0}/docs/architecture/effects-architecture.md +0 -0
  47. {lifx_async-4.7.4 → lifx_async-4.8.0}/docs/architecture/overview.md +0 -0
  48. {lifx_async-4.7.4 → lifx_async-4.8.0}/docs/faq.md +0 -0
  49. {lifx_async-4.7.4 → lifx_async-4.8.0}/docs/getting-started/effects.md +0 -0
  50. {lifx_async-4.7.4 → lifx_async-4.8.0}/docs/getting-started/installation.md +0 -0
  51. {lifx_async-4.7.4 → lifx_async-4.8.0}/docs/getting-started/themes.md +0 -0
  52. {lifx_async-4.7.4 → lifx_async-4.8.0}/docs/index.md +0 -0
  53. {lifx_async-4.7.4 → lifx_async-4.8.0}/docs/migration/effect-api-changes.md +0 -0
  54. {lifx_async-4.7.4 → lifx_async-4.8.0}/docs/stylesheets/extra.css +0 -0
  55. {lifx_async-4.7.4 → lifx_async-4.8.0}/docs/user-guide/ceiling-lights.md +0 -0
  56. {lifx_async-4.7.4 → lifx_async-4.8.0}/docs/user-guide/effects-custom.md +0 -0
  57. {lifx_async-4.7.4 → lifx_async-4.8.0}/docs/user-guide/effects-troubleshooting.md +0 -0
  58. {lifx_async-4.7.4 → lifx_async-4.8.0}/docs/user-guide/protocol-deep-dive.md +0 -0
  59. {lifx_async-4.7.4 → lifx_async-4.8.0}/docs/user-guide/themes.md +0 -0
  60. {lifx_async-4.7.4 → lifx_async-4.8.0}/docs/user-guide/troubleshooting.md +0 -0
  61. {lifx_async-4.7.4 → lifx_async-4.8.0}/examples/01_simple_discovery.py +0 -0
  62. {lifx_async-4.7.4 → lifx_async-4.8.0}/examples/02_simple_control.py +0 -0
  63. {lifx_async-4.7.4 → lifx_async-4.8.0}/examples/03_waveforms.py +0 -0
  64. {lifx_async-4.7.4 → lifx_async-4.8.0}/examples/04_logging.py +0 -0
  65. {lifx_async-4.7.4 → lifx_async-4.8.0}/examples/06_pulse_effect.py +0 -0
  66. {lifx_async-4.7.4 → lifx_async-4.8.0}/examples/07_colorloop_effect.py +0 -0
  67. {lifx_async-4.7.4 → lifx_async-4.8.0}/examples/08_custom_effect.py +0 -0
  68. {lifx_async-4.7.4 → lifx_async-4.8.0}/examples/09_background_effect.py +0 -0
  69. {lifx_async-4.7.4 → lifx_async-4.8.0}/examples/10_find_specific_devices.py +0 -0
  70. {lifx_async-4.7.4 → lifx_async-4.8.0}/examples/11_matrix_basic.py +0 -0
  71. {lifx_async-4.7.4 → lifx_async-4.8.0}/examples/12_matrix_effects.py +0 -0
  72. {lifx_async-4.7.4 → lifx_async-4.8.0}/examples/13_matrix_large.py +0 -0
  73. {lifx_async-4.7.4 → lifx_async-4.8.0}/mkdocs.yml +0 -0
  74. {lifx_async-4.7.4 → lifx_async-4.8.0}/renovate.json +0 -0
  75. {lifx_async-4.7.4 → lifx_async-4.8.0}/src/lifx/color.py +0 -0
  76. {lifx_async-4.7.4 → lifx_async-4.8.0}/src/lifx/devices/__init__.py +0 -0
  77. {lifx_async-4.7.4 → lifx_async-4.8.0}/src/lifx/devices/base.py +0 -0
  78. {lifx_async-4.7.4 → lifx_async-4.8.0}/src/lifx/devices/hev.py +0 -0
  79. {lifx_async-4.7.4 → lifx_async-4.8.0}/src/lifx/devices/infrared.py +0 -0
  80. {lifx_async-4.7.4 → lifx_async-4.8.0}/src/lifx/devices/light.py +0 -0
  81. {lifx_async-4.7.4 → lifx_async-4.8.0}/src/lifx/devices/matrix.py +0 -0
  82. {lifx_async-4.7.4 → lifx_async-4.8.0}/src/lifx/devices/multizone.py +0 -0
  83. {lifx_async-4.7.4 → lifx_async-4.8.0}/src/lifx/effects/__init__.py +0 -0
  84. {lifx_async-4.7.4 → lifx_async-4.8.0}/src/lifx/effects/base.py +0 -0
  85. {lifx_async-4.7.4 → lifx_async-4.8.0}/src/lifx/effects/colorloop.py +0 -0
  86. {lifx_async-4.7.4 → lifx_async-4.8.0}/src/lifx/effects/conductor.py +0 -0
  87. {lifx_async-4.7.4 → lifx_async-4.8.0}/src/lifx/effects/const.py +0 -0
  88. {lifx_async-4.7.4 → lifx_async-4.8.0}/src/lifx/effects/models.py +0 -0
  89. {lifx_async-4.7.4 → lifx_async-4.8.0}/src/lifx/effects/pulse.py +0 -0
  90. {lifx_async-4.7.4 → lifx_async-4.8.0}/src/lifx/effects/state_manager.py +0 -0
  91. {lifx_async-4.7.4 → lifx_async-4.8.0}/src/lifx/exceptions.py +0 -0
  92. {lifx_async-4.7.4 → lifx_async-4.8.0}/src/lifx/network/__init__.py +0 -0
  93. {lifx_async-4.7.4 → lifx_async-4.8.0}/src/lifx/network/connection.py +0 -0
  94. {lifx_async-4.7.4 → lifx_async-4.8.0}/src/lifx/network/discovery.py +0 -0
  95. {lifx_async-4.7.4 → lifx_async-4.8.0}/src/lifx/network/message.py +0 -0
  96. {lifx_async-4.7.4 → lifx_async-4.8.0}/src/lifx/network/transport.py +0 -0
  97. {lifx_async-4.7.4 → lifx_async-4.8.0}/src/lifx/products/__init__.py +0 -0
  98. {lifx_async-4.7.4 → lifx_async-4.8.0}/src/lifx/products/generator.py +0 -0
  99. {lifx_async-4.7.4 → lifx_async-4.8.0}/src/lifx/products/quirks.py +0 -0
  100. {lifx_async-4.7.4 → lifx_async-4.8.0}/src/lifx/products/registry.py +0 -0
  101. {lifx_async-4.7.4 → lifx_async-4.8.0}/src/lifx/protocol/__init__.py +0 -0
  102. {lifx_async-4.7.4 → lifx_async-4.8.0}/src/lifx/protocol/base.py +0 -0
  103. {lifx_async-4.7.4 → lifx_async-4.8.0}/src/lifx/protocol/generator.py +0 -0
  104. {lifx_async-4.7.4 → lifx_async-4.8.0}/src/lifx/protocol/header.py +0 -0
  105. {lifx_async-4.7.4 → lifx_async-4.8.0}/src/lifx/protocol/models.py +0 -0
  106. {lifx_async-4.7.4 → lifx_async-4.8.0}/src/lifx/protocol/packets.py +0 -0
  107. {lifx_async-4.7.4 → lifx_async-4.8.0}/src/lifx/protocol/protocol_types.py +0 -0
  108. {lifx_async-4.7.4 → lifx_async-4.8.0}/src/lifx/protocol/serializer.py +0 -0
  109. {lifx_async-4.7.4 → lifx_async-4.8.0}/src/lifx/py.typed +0 -0
  110. {lifx_async-4.7.4 → lifx_async-4.8.0}/src/lifx/theme/__init__.py +0 -0
  111. {lifx_async-4.7.4 → lifx_async-4.8.0}/src/lifx/theme/canvas.py +0 -0
  112. {lifx_async-4.7.4 → lifx_async-4.8.0}/src/lifx/theme/generators.py +0 -0
  113. {lifx_async-4.7.4 → lifx_async-4.8.0}/src/lifx/theme/library.py +0 -0
  114. {lifx_async-4.7.4 → lifx_async-4.8.0}/src/lifx/theme/theme.py +0 -0
  115. {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/__init__.py +0 -0
  116. {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/conftest.py +0 -0
  117. {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_api/__init__.py +0 -0
  118. {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_api/test_api_apply_theme.py +0 -0
  119. {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_api/test_api_batch_errors.py +0 -0
  120. {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_api/test_api_batch_operations.py +0 -0
  121. {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_api/test_api_organization.py +0 -0
  122. {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_color.py +0 -0
  123. {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_devices/__init__.py +0 -0
  124. {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_devices/conftest.py +0 -0
  125. {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_devices/test_base.py +0 -0
  126. {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_devices/test_hev.py +0 -0
  127. {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_devices/test_infrared.py +0 -0
  128. {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_devices/test_light.py +0 -0
  129. {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_devices/test_mac_address.py +0 -0
  130. {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_devices/test_matrix.py +0 -0
  131. {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_devices/test_multizone.py +0 -0
  132. {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_devices/test_state_ceiling.py +0 -0
  133. {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_devices/test_state_hev.py +0 -0
  134. {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_devices/test_state_infrared.py +0 -0
  135. {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_devices/test_state_light.py +0 -0
  136. {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_devices/test_state_management.py +0 -0
  137. {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_devices/test_state_matrix.py +0 -0
  138. {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_devices/test_state_multizone.py +0 -0
  139. {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_effects/__init__.py +0 -0
  140. {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_effects/test_base.py +0 -0
  141. {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_effects/test_capability_filtering.py +0 -0
  142. {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_effects/test_colorloop.py +0 -0
  143. {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_effects/test_integration.py +0 -0
  144. {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_effects/test_models.py +0 -0
  145. {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_effects/test_pulse.py +0 -0
  146. {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_effects/test_state_manager.py +0 -0
  147. {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_network/__init__.py +0 -0
  148. {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_network/test_concurrent_requests.py +0 -0
  149. {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_network/test_connection.py +0 -0
  150. {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_network/test_discovery_devices.py +0 -0
  151. {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_network/test_discovery_errors.py +0 -0
  152. {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_network/test_message.py +0 -0
  153. {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_network/test_message_advanced.py +0 -0
  154. {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_network/test_transport.py +0 -0
  155. {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_products/test_product_generator.py +0 -0
  156. {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_products/test_registry.py +0 -0
  157. {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_protocol/test_generated.py +0 -0
  158. {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_protocol/test_header.py +0 -0
  159. {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_protocol/test_protocol_generator.py +0 -0
  160. {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_protocol/test_serializer.py +0 -0
  161. {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_theme/__init__.py +0 -0
  162. {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_theme/conftest.py +0 -0
  163. {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_theme/test_apply_theme.py +0 -0
  164. {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_theme/test_canvas.py +0 -0
  165. {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_theme/test_generators.py +0 -0
  166. {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_theme/test_library.py +0 -0
  167. {lifx_async-4.7.4 → lifx_async-4.8.0}/tests/test_theme/test_theme.py +0 -0
  168. {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@ed21f2f24f8dd64503750218de024bcf64c7250a # v7
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@ed21f2f24f8dd64503750218de024bcf64c7250a # v7
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@ed21f2f24f8dd64503750218de024bcf64c7250a # v7
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@ed21f2f24f8dd64503750218de024bcf64c7250a # v7
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@ed21f2f24f8dd64503750218de024bcf64c7250a # v7
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@ed21f2f24f8dd64503750218de024bcf64c7250a # v7
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@ed21f2f24f8dd64503750218de024bcf64c7250a # v7
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 as they're discovered
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
- - **768 tests total** (comprehensive coverage across all layers)
625
+ - **1075+ tests total** (comprehensive coverage across all layers)
620
626
  - **Protocol Layer**: 136 tests (serialization, header, packets, generator validation)
621
- - **Network Layer**: 86 tests (transport, discovery, connection, message, async generator requests)
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
- └── test_concurrent_requests.py # Concurrent request tests
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lifx-async
3
- Version: 4.7.4
3
+ Version: 4.8.0
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>
@@ -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
- └── transport.py # UDP transport
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) - Simple device discovery
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())
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "lifx-async"
3
- version = "4.7.4"
3
+ version = "4.8.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"