rustuya-local 0.1.0.dev6__tar.gz → 0.1.0.dev8__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 (109) hide show
  1. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/.github/workflows/_test.yml +4 -4
  2. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/CHANGELOG.md +23 -2
  3. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/PKG-INFO +53 -20
  4. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/README.md +50 -17
  5. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/custom_components/rustuya/__init__.py +14 -8
  6. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/custom_components/rustuya/manifest.json +3 -3
  7. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/custom_components/rustuya/panel.py +56 -50
  8. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/custom_components/rustuya/strings.json +3 -3
  9. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/custom_components/rustuya/translations/en.json +3 -3
  10. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/custom_components/rustuya/translations/ko.json +3 -3
  11. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/custom_components/rustuya/www/rustuya-panel.js +2 -2
  12. rustuya_local-0.1.0.dev8/docs/ARCHITECTURE.md +78 -0
  13. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/docs/MIGRATING.md +16 -9
  14. rustuya_local-0.1.0.dev8/docs/REDESIGN.md +385 -0
  15. rustuya_local-0.1.0.dev8/docs/REVIEW.md +108 -0
  16. rustuya_local-0.1.0.dev8/docs/ROADMAP.md +38 -0
  17. rustuya_local-0.1.0.dev8/docs/STATUS.md +41 -0
  18. rustuya_local-0.1.0.dev8/docs/history/ROADMAP-2026-10-12.md +111 -0
  19. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/pyproject.toml +3 -3
  20. rustuya_local-0.1.0.dev8/release-notes.md +12 -0
  21. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/src/rustuya_local/__init__.py +1 -1
  22. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/src/rustuya_local/host/mqtt.py +49 -3
  23. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/src/rustuya_local/host/pack.py +27 -5
  24. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/src/rustuya_local/host/runner.py +1 -1
  25. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/src/rustuya_local/host/transport.py +10 -1
  26. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/src/rustuya_local/manager_plugin/__init__.py +1 -1
  27. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/src/rustuya_local/manager_plugin/static/index.js +2 -2
  28. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/tests/e2e/test_chain_in_process.py +1 -1
  29. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/tests/e2e/test_chain_mqtt.py +1 -1
  30. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/tests/ha/test_init.py +2 -2
  31. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/tests/ha/test_panel.py +72 -0
  32. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/tests/unit/test_host.py +4 -2
  33. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/tests/unit/test_hub.py +1 -1
  34. rustuya_local-0.1.0.dev8/tests/unit/test_mqtt_lifecycle.py +110 -0
  35. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/tests/unit/test_pack.py +50 -0
  36. rustuya_local-0.1.0.dev6/docs/REDESIGN.md +0 -327
  37. rustuya_local-0.1.0.dev6/docs/ROADMAP.md +0 -102
  38. rustuya_local-0.1.0.dev6/docs/STATUS.md +0 -6
  39. rustuya_local-0.1.0.dev6/release-notes.md +0 -3
  40. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/.github/dependabot.yml +0 -0
  41. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/.github/workflows/ci.yml +0 -0
  42. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/.github/workflows/hacs.yml +0 -0
  43. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/.github/workflows/hassfest.yml +0 -0
  44. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/.github/workflows/release.yml +0 -0
  45. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/.gitignore +0 -0
  46. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/LICENSE +0 -0
  47. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/custom_components/rustuya/brand/icon.png +0 -0
  48. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/custom_components/rustuya/brand/icon.svg +0 -0
  49. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/custom_components/rustuya/brand/icon@2x.png +0 -0
  50. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/custom_components/rustuya/brand/logo.png +0 -0
  51. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/custom_components/rustuya/brand/logo.svg +0 -0
  52. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/custom_components/rustuya/brand/logo@2x.png +0 -0
  53. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/custom_components/rustuya/bridge_supervisor.py +0 -0
  54. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/custom_components/rustuya/bridge_sync.py +0 -0
  55. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/custom_components/rustuya/broker.py +0 -0
  56. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/custom_components/rustuya/config_flow.py +0 -0
  57. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/custom_components/rustuya/const.py +0 -0
  58. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/custom_components/rustuya/manager_registry.py +0 -0
  59. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/custom_components/rustuya/manager_session.py +0 -0
  60. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/custom_components/rustuya/panel_bridge.py +0 -0
  61. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/custom_components/rustuya/services.yaml +0 -0
  62. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/hacs.json +0 -0
  63. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/scripts/build_dropin.py +0 -0
  64. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/scripts/release_notes.py +0 -0
  65. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/src/rustuya_local/__main__.py +0 -0
  66. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/src/rustuya_local/async_tools.py +0 -0
  67. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/src/rustuya_local/bridge_client.py +0 -0
  68. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/src/rustuya_local/cli.py +0 -0
  69. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/src/rustuya_local/config.py +0 -0
  70. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/src/rustuya_local/files.py +0 -0
  71. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/src/rustuya_local/host/__init__.py +0 -0
  72. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/src/rustuya_local/host/devices.py +0 -0
  73. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/src/rustuya_local/host/hub.py +0 -0
  74. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/src/rustuya_local/host/il.py +0 -0
  75. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/src/rustuya_local/host/memory.py +0 -0
  76. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/src/rustuya_local/host/overrides.py +0 -0
  77. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/src/rustuya_local/host/rules.py +0 -0
  78. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/src/rustuya_local/manager_plugin/api.py +0 -0
  79. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/src/rustuya_local/service.py +0 -0
  80. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/src/rustuya_local/validation.py +0 -0
  81. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/tests/e2e/conftest.py +0 -0
  82. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/tests/e2e/devices.py +0 -0
  83. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/tests/e2e/test_bridge_registration.py +0 -0
  84. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/tests/e2e/test_daemon.py +0 -0
  85. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/tests/e2e/test_full_stack.py +0 -0
  86. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/tests/e2e/test_full_stack_custom_templates.py +0 -0
  87. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/tests/e2e/test_full_stack_devices.py +0 -0
  88. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/tests/e2e/test_manager_plugin.py +0 -0
  89. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/tests/e2e/test_purge.py +0 -0
  90. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/tests/e2e/test_service.py +0 -0
  91. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/tests/ha/conftest.py +0 -0
  92. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/tests/ha/fake_manager.py +0 -0
  93. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/tests/ha/test_bridge_probe.py +0 -0
  94. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/tests/ha/test_config_flow.py +0 -0
  95. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/tests/ha/test_manager_registry.py +0 -0
  96. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/tests/ha/test_manager_session.py +0 -0
  97. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/tests/ha/test_migrate.py +0 -0
  98. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/tests/unit/test_config.py +0 -0
  99. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/tests/unit/test_cover_settings.py +0 -0
  100. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/tests/unit/test_dropin.py +0 -0
  101. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/tests/unit/test_files.py +0 -0
  102. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/tests/unit/test_hazardous.py +0 -0
  103. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/tests/unit/test_il_vectors.py +0 -0
  104. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/tests/unit/test_link_watcher.py +0 -0
  105. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/tests/unit/test_panel_js.py +0 -0
  106. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/tests/unit/test_plugin_api.py +0 -0
  107. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/tests/unit/test_release_notes.py +0 -0
  108. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/tests/unit/test_rule_files.py +0 -0
  109. {rustuya_local-0.1.0.dev6 → rustuya_local-0.1.0.dev8}/tests/unit/tuya_samples.py +0 -0
@@ -3,7 +3,7 @@ name: Test (reusable)
3
3
  # Single source of truth for rustuya-local's pre-merge / pre-publish checks. Called from ci.yml
4
4
  # on push/PR and as a gate from release.yml, so a commit that fails these can't reach PyPI.
5
5
  #
6
- # il-ha is not on PyPI yet, so check out ildevice-homeassistant and explicitly
6
+ # ildevice-homeassistant is not on PyPI yet, so check out ildevice-homeassistant and explicitly
7
7
  # install that checkout together with rustuya-local's test extras.
8
8
  # Other package dependencies are installed from PyPI.
9
9
 
@@ -20,7 +20,7 @@ jobs:
20
20
  strategy:
21
21
  fail-fast: false
22
22
  matrix:
23
- # il-ha (the test/fullstack sibling dep) requires Python>=3.14; rustuya-local's own
23
+ # ildevice-homeassistant (the test/fullstack sibling dep) requires Python>=3.14; rustuya-local's own
24
24
  # requires-python floor (3.13, in pyproject.toml) stays lower since only the test extras
25
25
  # need 3.14, not the base package.
26
26
  python-version: ["3.14"]
@@ -31,7 +31,7 @@ jobs:
31
31
  with:
32
32
  path: rustuya-local
33
33
 
34
- # The repository publishes its Python package under the name il-ha.
34
+ # Install the integration checkout as the test consumer package.
35
35
  - name: Check out ildevice-homeassistant
36
36
  uses: actions/checkout@v7
37
37
  with:
@@ -91,7 +91,7 @@ jobs:
91
91
  run: pytest -q
92
92
 
93
93
  package:
94
- # The test job uses an il-ha checkout; this one checks what a user gets: the wheel in a fresh
94
+ # The test job uses an ildevice-homeassistant checkout; this one checks what a user gets: the wheel in a fresh
95
95
  # environment, every dependency from PyPI, importing and starting the CLI.
96
96
  name: installs from PyPI alone
97
97
  runs-on: ubuntu-latest
@@ -3,11 +3,32 @@
3
3
  All notable changes to this project are documented in this file. A release's GitHub Release notes are its section
4
4
  here (the release workflow reads it, and refuses to publish a version without one).
5
5
 
6
- The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versions are `0.0.x` until the first
7
- stable release.
6
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). The current development series uses
7
+ `0.1.0.devN`; the earlier `0.0.x` releases remain below as history.
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [0.1.0.dev8] — 2026-10-13
12
+
13
+ ### Fixed
14
+ - Always stop the embedded bridge during Home Assistant unload, including service-stop failure or cancellation.
15
+ - Close MQTT callback tasks, ignore queued dispatch after shutdown, retrieve asynchronous handler errors and stop
16
+ the paho thread outside the event loop, including when a connection attempt fails.
17
+ - Preserve concurrent partial Home Assistant settings/options saves by locking reads, merges and restart together.
18
+ - Reject unsafe pack ownership paths, malformed hashes and bounds; preserve host settings and symlink targets.
19
+
20
+ ### Documentation
21
+ - Separate current status, architecture and outstanding work from the historical redesign record.
22
+ - Document the 2026-10-13 code review, verification results and remaining reliability issues.
23
+ - Correct daemon setup, test prerequisites and saved cover settings precedence.
24
+ - Translate all Korean Markdown documents into English, including archived design and roadmap history.
25
+
26
+ ## [0.1.0.dev7] — 2026-10-12
27
+
28
+ ### Changed
29
+ - Use ildevice-homeassistant consistently in documentation, UI text and code comments.
30
+ - Require tuya-interpreter 0.1.0a2 in the package and the Home Assistant integration.
31
+
11
32
  ## [0.1.0.dev6] — 2026-10-12
12
33
 
13
34
  ### Changed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: rustuya-local
3
- Version: 0.1.0.dev6
3
+ Version: 0.1.0.dev8
4
4
  Summary: Local Tuya control: rustuya-bridge -> tuya-interpreter -> IL (ildevice). HA-independent core.
5
5
  Project-URL: Homepage, https://github.com/3735943886/rustuya-local
6
6
  Project-URL: Repository, https://github.com/3735943886/rustuya-local
@@ -10,14 +10,14 @@ License-File: LICENSE
10
10
  Requires-Python: >=3.13
11
11
  Requires-Dist: paho-mqtt>=2.0
12
12
  Requires-Dist: pyrustuyabridge>=0.4
13
- Requires-Dist: tuya-interpreter>=0.1.0a1
13
+ Requires-Dist: tuya-interpreter>=0.1.0a2
14
14
  Provides-Extra: fullstack
15
15
  Requires-Dist: tuyamock; extra == 'fullstack'
16
16
  Provides-Extra: manager
17
17
  Requires-Dist: rustuya-manager[web]>=0.2.2; extra == 'manager'
18
18
  Provides-Extra: test
19
19
  Requires-Dist: httpx2; extra == 'test'
20
- Requires-Dist: il-ha; extra == 'test'
20
+ Requires-Dist: ildevice-homeassistant; extra == 'test'
21
21
  Requires-Dist: pytest; extra == 'test'
22
22
  Requires-Dist: pytest-asyncio; extra == 'test'
23
23
  Provides-Extra: test-ha
@@ -32,7 +32,7 @@ Local control of Tuya devices through [rustuya-bridge](https://github.com/373594
32
32
  ([ildevice](https://github.com/3735943886/ildevice)) devices, and in Home Assistant. It replaces rustuya-homeassistant.
33
33
 
34
34
  ```
35
- rustuya-bridge ──MQTT──► rustuya-local ──MQTT (il/…)──► il-ha (Home Assistant integration), or any IL consumer
35
+ rustuya-bridge ──MQTT──► rustuya-local ──MQTT (il/…)──► ildevice-homeassistant (Home Assistant integration), or any IL consumer
36
36
  raw LAN dps Service:
37
37
  BridgeClient + rustuya_local.host.Hub
38
38
  ```
@@ -42,15 +42,33 @@ rustuya-bridge ──MQTT──► rustuya-local ──MQTT (il/…)──► il
42
42
  - **rustuya-local** is one `Service` (`src/rustuya_local/service.py`) and three thin shells that run it:
43
43
  the `rustuya-local run` daemon, the `custom_components/rustuya` Home Assistant integration, and a rustuya-manager
44
44
  plugin. Its `host` package owns the runner, transports and file watchers. It only produces IL and knows nothing of Home Assistant entities.
45
- - **[il-ha](https://github.com/3735943886/ildevice-homeassistant)** turns IL descriptors into Home Assistant entities.
45
+ - **[ildevice-homeassistant](https://github.com/3735943886/ildevice-homeassistant)** turns IL descriptors into Home Assistant entities.
46
46
 
47
47
  ## Daemon
48
48
 
49
+ ```bash
50
+ uv venv --python 3.13
51
+ uv pip install -e .
52
+ .venv/bin/rustuya-local run --config config.json
49
53
  ```
50
- uv venv && uv pip install -e .
51
- rustuya-local run --config config.json
54
+
55
+ Create `config.json` beside the device file before running:
56
+
57
+ ```json
58
+ {
59
+ "bridge": {"host": "localhost", "port": 1883, "root": "rustuya"},
60
+ "il": {"host": "localhost", "port": 1883, "prefix": "il", "source": "tuya"},
61
+ "devices": "tuyadevices.json",
62
+ "custom_converters": "custom_converters",
63
+ "pack": true,
64
+ "watch_interval": 5,
65
+ "options": {"allow_hazardous": false, "expose_unused": false}
66
+ }
52
67
  ```
53
68
 
69
+ The package requires Python 3.13 or newer. Device and converter paths are relative to the configuration file.
70
+ `watch_interval: 0` disables polling; use a positive interval to apply file edits and downloaded rules while running.
71
+
54
72
  `config.json` (see `src/rustuya_local/config.py`): the bridge and IL brokers, the IL prefix, the device list, a
55
73
  `custom_converters` directory, and options for the Hub (`allow_hazardous`, `expose_unused`, `overrides`).
56
74
  The device list is the `tuyadevices.json` that [rustuya-manager](https://github.com/3735943886/rustuya-manager) keeps (its QR-login wizard writes
@@ -75,7 +93,7 @@ MQTT and in-process transport tests exercise the same adapter and the bidirectio
75
93
 
76
94
  Coming from rustuya-homeassistant: [docs/MIGRATING.md](docs/MIGRATING.md).
77
95
 
78
- Install il-ha, and run one IL producer per bridge: the daemon, the integration below, or the manager plugin. A second
96
+ Install ildevice-homeassistant, and run one IL producer per bridge: the daemon, the integration below, or the manager plugin. A second
79
97
  one with the same IL prefix and source refuses to start while the first runs (every device would show twice): the
80
98
  daemon exits, the integration is retried by Home Assistant, the plugin's tab shows it and retries. To move from one to
81
99
  another, stop the old one first.
@@ -87,7 +105,7 @@ bridge mode, the broker and the IL prefix (it checks an external bridge answers
87
105
  is not on the default one), then offers to fetch the devices from Tuya Cloud (a saved login is reused; a QR login
88
106
  only when there is none), which closing its window skips (it is in *Configure* later, and in the panel); the rest
89
107
  starts at its defaults and is changed in the panel's *Settings*. Deleting the integration takes its devices out of IL
90
- (so il-ha removes them); with the embedded bridge it also clears that bridge's retained topics and its state file.
108
+ (so ildevice-homeassistant removes them); with the embedded bridge it also clears that bridge's retained topics and its state file.
91
109
  The device list and Tuya login it kept under `.storage/rustuya/` are deleted; a device file you pointed elsewhere, and
92
110
  everything on an external bridge, are left as they are.
93
111
 
@@ -134,7 +152,9 @@ device settings are enabled, unless an override selects another source. A settin
134
152
  the setting is saved as the device's block in `zz_settings.json` in the converters directory and applied at once, so it
135
153
  survives a restart and can be edited there too. Without a converters directory (a daemon with no
136
154
  `custom_converters`) the switches are not offered; `"options": {"device_settings": false}` turns them off.
137
- A block with the same `cover` keys in your own file or the builtin rules sets a model's starting point; the saved settings win.
155
+ A block with the same `cover` keys in your own file or the builtin rules sets a model's starting point.
156
+ User files, including `zz_settings.json`, are applied in filename order; a later file can override saved settings.
157
+ The daemon's inline `options.overrides` is applied after file layers.
138
158
  Older `cover.infer_motion` and `converters.cover_motion` blocks are upgraded on loading to `cover.state_source`
139
159
  (`inferred`, or `none` for `infer_motion: false`) and `cover.settle`; see [migration notes](docs/MIGRATING.md).
140
160
 
@@ -172,17 +192,24 @@ rustuya-local release (bump the patch version and tag; the manager's catalog fol
172
192
 
173
193
  ## Tests
174
194
 
175
- ```
176
- .venv/bin/python -m pytest
195
+ ```bash
196
+ uv venv --python 3.14
197
+ uv pip install -e ../ildevice-homeassistant -e ".[test,test-ha,fullstack,manager]"
198
+ .venv/bin/python -m pytest -q
199
+ ruff check src tests custom_components scripts
177
200
  ```
178
201
 
179
- Everything here needs the sibling repos and, for the last two groups, `mosquitto`, `pyrustuya-bridge` and `tuyamock`
180
- (`pip install -e ".[test,fullstack]"`); those are skipped when missing. Parity with Home Assistant core's fixtures lives in
181
- `tuya-interpreter/tests/chain`, the wire and topic vectors in `ildevice/vectors`.
202
+ The complete suite uses Python 3.14 because the consumer and Home Assistant test dependencies require it.
203
+ Install `mosquitto` for broker tests and Node.js for panel JavaScript tests. The command above installs the consumer
204
+ from a sibling checkout and other dependencies from the package index; CI follows the same approach.
205
+ Missing base or test dependencies can fail collection; optional broker, emulator and fixture tests skip when their
206
+ prerequisites are unavailable. The full-stack device comparison uses `../tuya-interpreter/tests/golden`; IL wire and
207
+ topic vectors use `../ildevice/vectors` (or the `ILDEVICE` environment variable). Keep those checkouts for these checks.
182
208
 
183
- - `tests/unit/`: the configuration.
209
+ - `tests/unit/`: configuration validation, host lifecycle, IL vectors, rule loading, settings persistence,
210
+ pack ownership, converter APIs, panel JavaScript and release packaging.
184
211
  - `tests/e2e/test_service.py`: the service on in-memory brokers (overrides directory, a failed start).
185
- - `tests/e2e/test_chain_*.py`, `test_daemon.py`: the runner with il-ha's consumer model on the other side, in one process
212
+ - `tests/e2e/test_chain_*.py`, `test_daemon.py`: the runner with ildevice-homeassistant's consumer model on the other side, in one process
186
213
  and over a real broker; the daemon as a subprocess.
187
214
  - `tests/e2e/test_manager_plugin.py`: the plugin under rustuya-manager's real plugin host (skipped without it).
188
215
  - `tests/ha/`: the integration (config flow, setup).
@@ -190,8 +217,14 @@ Everything here needs the sibling repos and, for the last two groups, `mosquitto
190
217
  (`tuyamock`) around the daemon: state, commands, rejections, a device dropping off and returning, a daemon restart, and a
191
218
  differential check that 38 real device shapes come out of the whole chain exactly as `tuya-interpreter` computes them.
192
219
 
193
- ## Status
220
+ ## Documentation
194
221
 
195
- Planning, progress and open decisions: `docs/REDESIGN.md`.
222
+ - [Current status](docs/STATUS.md): version, document index and verification boundaries.
223
+ - [Architecture](docs/ARCHITECTURE.md): current components, lifecycle and rule precedence.
224
+ - [Migration](docs/MIGRATING.md): upgrading old converters and rustuya-homeassistant installations.
225
+ - [Code review](docs/REVIEW.md): findings and local verification from 2026-10-13.
226
+ - [Roadmap](docs/ROADMAP.md): outstanding work and real-device checks.
227
+ - [Design history](docs/REDESIGN.md): the September redesign record; historical APIs and versions.
228
+ - [Changelog](CHANGELOG.md): changes grouped by release.
196
229
 
197
- Licence: `LICENSE` (MIT).
230
+ Licence: [MIT](LICENSE).
@@ -6,7 +6,7 @@ Local control of Tuya devices through [rustuya-bridge](https://github.com/373594
6
6
  ([ildevice](https://github.com/3735943886/ildevice)) devices, and in Home Assistant. It replaces rustuya-homeassistant.
7
7
 
8
8
  ```
9
- rustuya-bridge ──MQTT──► rustuya-local ──MQTT (il/…)──► il-ha (Home Assistant integration), or any IL consumer
9
+ rustuya-bridge ──MQTT──► rustuya-local ──MQTT (il/…)──► ildevice-homeassistant (Home Assistant integration), or any IL consumer
10
10
  raw LAN dps Service:
11
11
  BridgeClient + rustuya_local.host.Hub
12
12
  ```
@@ -16,15 +16,33 @@ rustuya-bridge ──MQTT──► rustuya-local ──MQTT (il/…)──► il
16
16
  - **rustuya-local** is one `Service` (`src/rustuya_local/service.py`) and three thin shells that run it:
17
17
  the `rustuya-local run` daemon, the `custom_components/rustuya` Home Assistant integration, and a rustuya-manager
18
18
  plugin. Its `host` package owns the runner, transports and file watchers. It only produces IL and knows nothing of Home Assistant entities.
19
- - **[il-ha](https://github.com/3735943886/ildevice-homeassistant)** turns IL descriptors into Home Assistant entities.
19
+ - **[ildevice-homeassistant](https://github.com/3735943886/ildevice-homeassistant)** turns IL descriptors into Home Assistant entities.
20
20
 
21
21
  ## Daemon
22
22
 
23
+ ```bash
24
+ uv venv --python 3.13
25
+ uv pip install -e .
26
+ .venv/bin/rustuya-local run --config config.json
23
27
  ```
24
- uv venv && uv pip install -e .
25
- rustuya-local run --config config.json
28
+
29
+ Create `config.json` beside the device file before running:
30
+
31
+ ```json
32
+ {
33
+ "bridge": {"host": "localhost", "port": 1883, "root": "rustuya"},
34
+ "il": {"host": "localhost", "port": 1883, "prefix": "il", "source": "tuya"},
35
+ "devices": "tuyadevices.json",
36
+ "custom_converters": "custom_converters",
37
+ "pack": true,
38
+ "watch_interval": 5,
39
+ "options": {"allow_hazardous": false, "expose_unused": false}
40
+ }
26
41
  ```
27
42
 
43
+ The package requires Python 3.13 or newer. Device and converter paths are relative to the configuration file.
44
+ `watch_interval: 0` disables polling; use a positive interval to apply file edits and downloaded rules while running.
45
+
28
46
  `config.json` (see `src/rustuya_local/config.py`): the bridge and IL brokers, the IL prefix, the device list, a
29
47
  `custom_converters` directory, and options for the Hub (`allow_hazardous`, `expose_unused`, `overrides`).
30
48
  The device list is the `tuyadevices.json` that [rustuya-manager](https://github.com/3735943886/rustuya-manager) keeps (its QR-login wizard writes
@@ -49,7 +67,7 @@ MQTT and in-process transport tests exercise the same adapter and the bidirectio
49
67
 
50
68
  Coming from rustuya-homeassistant: [docs/MIGRATING.md](docs/MIGRATING.md).
51
69
 
52
- Install il-ha, and run one IL producer per bridge: the daemon, the integration below, or the manager plugin. A second
70
+ Install ildevice-homeassistant, and run one IL producer per bridge: the daemon, the integration below, or the manager plugin. A second
53
71
  one with the same IL prefix and source refuses to start while the first runs (every device would show twice): the
54
72
  daemon exits, the integration is retried by Home Assistant, the plugin's tab shows it and retries. To move from one to
55
73
  another, stop the old one first.
@@ -61,7 +79,7 @@ bridge mode, the broker and the IL prefix (it checks an external bridge answers
61
79
  is not on the default one), then offers to fetch the devices from Tuya Cloud (a saved login is reused; a QR login
62
80
  only when there is none), which closing its window skips (it is in *Configure* later, and in the panel); the rest
63
81
  starts at its defaults and is changed in the panel's *Settings*. Deleting the integration takes its devices out of IL
64
- (so il-ha removes them); with the embedded bridge it also clears that bridge's retained topics and its state file.
82
+ (so ildevice-homeassistant removes them); with the embedded bridge it also clears that bridge's retained topics and its state file.
65
83
  The device list and Tuya login it kept under `.storage/rustuya/` are deleted; a device file you pointed elsewhere, and
66
84
  everything on an external bridge, are left as they are.
67
85
 
@@ -108,7 +126,9 @@ device settings are enabled, unless an override selects another source. A settin
108
126
  the setting is saved as the device's block in `zz_settings.json` in the converters directory and applied at once, so it
109
127
  survives a restart and can be edited there too. Without a converters directory (a daemon with no
110
128
  `custom_converters`) the switches are not offered; `"options": {"device_settings": false}` turns them off.
111
- A block with the same `cover` keys in your own file or the builtin rules sets a model's starting point; the saved settings win.
129
+ A block with the same `cover` keys in your own file or the builtin rules sets a model's starting point.
130
+ User files, including `zz_settings.json`, are applied in filename order; a later file can override saved settings.
131
+ The daemon's inline `options.overrides` is applied after file layers.
112
132
  Older `cover.infer_motion` and `converters.cover_motion` blocks are upgraded on loading to `cover.state_source`
113
133
  (`inferred`, or `none` for `infer_motion: false`) and `cover.settle`; see [migration notes](docs/MIGRATING.md).
114
134
 
@@ -146,17 +166,24 @@ rustuya-local release (bump the patch version and tag; the manager's catalog fol
146
166
 
147
167
  ## Tests
148
168
 
149
- ```
150
- .venv/bin/python -m pytest
169
+ ```bash
170
+ uv venv --python 3.14
171
+ uv pip install -e ../ildevice-homeassistant -e ".[test,test-ha,fullstack,manager]"
172
+ .venv/bin/python -m pytest -q
173
+ ruff check src tests custom_components scripts
151
174
  ```
152
175
 
153
- Everything here needs the sibling repos and, for the last two groups, `mosquitto`, `pyrustuya-bridge` and `tuyamock`
154
- (`pip install -e ".[test,fullstack]"`); those are skipped when missing. Parity with Home Assistant core's fixtures lives in
155
- `tuya-interpreter/tests/chain`, the wire and topic vectors in `ildevice/vectors`.
176
+ The complete suite uses Python 3.14 because the consumer and Home Assistant test dependencies require it.
177
+ Install `mosquitto` for broker tests and Node.js for panel JavaScript tests. The command above installs the consumer
178
+ from a sibling checkout and other dependencies from the package index; CI follows the same approach.
179
+ Missing base or test dependencies can fail collection; optional broker, emulator and fixture tests skip when their
180
+ prerequisites are unavailable. The full-stack device comparison uses `../tuya-interpreter/tests/golden`; IL wire and
181
+ topic vectors use `../ildevice/vectors` (or the `ILDEVICE` environment variable). Keep those checkouts for these checks.
156
182
 
157
- - `tests/unit/`: the configuration.
183
+ - `tests/unit/`: configuration validation, host lifecycle, IL vectors, rule loading, settings persistence,
184
+ pack ownership, converter APIs, panel JavaScript and release packaging.
158
185
  - `tests/e2e/test_service.py`: the service on in-memory brokers (overrides directory, a failed start).
159
- - `tests/e2e/test_chain_*.py`, `test_daemon.py`: the runner with il-ha's consumer model on the other side, in one process
186
+ - `tests/e2e/test_chain_*.py`, `test_daemon.py`: the runner with ildevice-homeassistant's consumer model on the other side, in one process
160
187
  and over a real broker; the daemon as a subprocess.
161
188
  - `tests/e2e/test_manager_plugin.py`: the plugin under rustuya-manager's real plugin host (skipped without it).
162
189
  - `tests/ha/`: the integration (config flow, setup).
@@ -164,8 +191,14 @@ Everything here needs the sibling repos and, for the last two groups, `mosquitto
164
191
  (`tuyamock`) around the daemon: state, commands, rejections, a device dropping off and returning, a daemon restart, and a
165
192
  differential check that 38 real device shapes come out of the whole chain exactly as `tuya-interpreter` computes them.
166
193
 
167
- ## Status
194
+ ## Documentation
168
195
 
169
- Planning, progress and open decisions: `docs/REDESIGN.md`.
196
+ - [Current status](docs/STATUS.md): version, document index and verification boundaries.
197
+ - [Architecture](docs/ARCHITECTURE.md): current components, lifecycle and rule precedence.
198
+ - [Migration](docs/MIGRATING.md): upgrading old converters and rustuya-homeassistant installations.
199
+ - [Code review](docs/REVIEW.md): findings and local verification from 2026-10-13.
200
+ - [Roadmap](docs/ROADMAP.md): outstanding work and real-device checks.
201
+ - [Design history](docs/REDESIGN.md): the September redesign record; historical APIs and versions.
202
+ - [Changelog](CHANGELOG.md): changes grouped by release.
170
203
 
171
- Licence: `LICENSE` (MIT).
204
+ Licence: [MIT](LICENSE).
@@ -1,7 +1,7 @@
1
1
  """Rustuya as a Home Assistant integration: runs the same `rustuya_local.service.Service` the standalone
2
2
  `rustuya-local` daemon does, tied to this config entry, with an optional embedded rustuya-bridge (`pyrustuyabridge`)
3
- and user overrides from `<config>/rustuya_converters`. It creates no entities and does not depend on il-ha: it is only an IL
4
- *producer* — install il-ha (or any IL consumer) separately to turn its devices into entities.
3
+ and user overrides from `<config>/rustuya_converters`. It creates no entities and does not depend on ildevice-homeassistant: it is only an IL
4
+ *producer* — install ildevice-homeassistant (or any IL consumer) separately to turn its devices into entities.
5
5
  """
6
6
 
7
7
  from __future__ import annotations
@@ -105,7 +105,7 @@ async def async_setup(hass: HomeAssistant, config: dict) -> bool:
105
105
 
106
106
  def _tuya_id(hass: HomeAssistant, service: Any, device_id: str) -> str:
107
107
  """`device_id` as the Tuya device id: as given when the service drives it, else a Home Assistant device's. An IL
108
- consumer registers a device with the IL id (the Tuya id) among its identifiers — il-ha as `("ildevice", id)`, under
108
+ consumer registers a device with the IL id (the Tuya id) among its identifiers — ildevice-homeassistant as `("ildevice", id)`, under
109
109
  whatever platform name it runs as — so any identifier value the service drives is taken, when there is exactly one.
110
110
  Unresolved, it is returned as given (and refused as an unknown device)."""
111
111
  from homeassistant.helpers import device_registry as dr
@@ -252,19 +252,25 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
252
252
  # setup that follows puts it back anyway; only disabling the entry (or deleting it, below) removes it
253
253
  if entry.disabled_by is not None:
254
254
  panel.async_remove(hass)
255
- runtime: RuntimeData = hass.data[DOMAIN].pop(entry.entry_id)
255
+ runtime: RuntimeData = hass.data[DOMAIN][entry.entry_id]
256
256
  restarting = getattr(runtime, "restarting", False) and entry.disabled_by is None
257
- await runtime.service.stop(offline=not restarting)
257
+ try:
258
+ await runtime.service.stop(offline=not restarting)
259
+ finally:
260
+ try:
261
+ if runtime.embedded_bridge:
262
+ await runtime.embedded_bridge.stop()
263
+ finally:
264
+ # A stopped service cannot be reused, even when draining its commands failed.
265
+ hass.data[DOMAIN].pop(entry.entry_id, None)
258
266
  if restarting:
259
267
  hass.data[RESUME] = True
260
- if runtime.embedded_bridge:
261
- await runtime.embedded_bridge.stop()
262
268
  return True
263
269
 
264
270
 
265
271
  async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
266
272
  """Deleted, not just unloaded (which only says `offline`, keeping everything for the next start). Clears what this
267
- entry leaves behind: its IL devices (retained descriptors and values, so il-ha and other IL consumers drop them),
273
+ entry leaves behind: its IL devices (retained descriptors and values, so ildevice-homeassistant and other IL consumers drop them),
268
274
  with the embedded bridge also that bridge's retained topics and its state file, and the files it kept in its own
269
275
  storage directory (the device list and the Tuya login). A path the user pointed elsewhere is left alone, as is
270
276
  everything on an external bridge. A broker that cannot be reached only leaves the retained topics behind; the
@@ -9,12 +9,12 @@
9
9
  "iot_class": "local_push",
10
10
  "issue_tracker": "https://github.com/3735943886/rustuya-local/issues",
11
11
  "requirements": [
12
- "tuya-interpreter>=0.1.0a1",
12
+ "tuya-interpreter>=0.1.0a2",
13
13
  "paho-mqtt>=2.0",
14
14
  "rustuya-manager>=0.2.2",
15
15
  "pyrustuyabridge>=0.4",
16
- "rustuya-local>=0.1.0.dev6"
16
+ "rustuya-local>=0.1.0.dev8"
17
17
  ],
18
18
  "single_config_entry": true,
19
- "version": "0.1.0.dev6"
19
+ "version": "0.1.0.dev8"
20
20
  }
@@ -221,11 +221,12 @@ class OptionsView(_View):
221
221
  or not all(isinstance(v, bool) for v in body.values())):
222
222
  return self.json_message(f"the options are {sorted(DEFAULT_OPTIONS)}, each true or false",
223
223
  HTTPStatus.BAD_REQUEST)
224
- options = {**entry.options, **body}
225
- if options == dict(entry.options):
226
- return self.json({"restarting": False})
227
- await _restart(request.app["hass"], entry, options=options) # answered once it has restarted
228
- return self.json({"restarting": True})
224
+ async with _settings_lock(request.app["hass"]):
225
+ options = {**entry.options, **body}
226
+ if options == dict(entry.options):
227
+ return self.json({"restarting": False})
228
+ await _restart(request.app["hass"], entry, options=options) # answered once it has restarted
229
+ return self.json({"restarting": True})
229
230
 
230
231
 
231
232
  def settings_of(data: Any) -> dict[str, Any]:
@@ -275,24 +276,30 @@ class SettingsView(_View):
275
276
  @_entry_view
276
277
  async def put(self, request: web.Request, entry: Any, runtime: Any) -> web.Response:
277
278
  hass = request.app["hass"]
278
- try:
279
- new = validate_settings(hass, entry.data, await request.json())
280
- except ValueError as e:
281
- return self.json_message(str(e), HTTPStatus.BAD_REQUEST)
282
- old = dict(entry.data)
283
- if new == settings_of(old):
284
- return self.json({"restarting": False})
285
- external = old.get(CONF_BRIDGE_MODE) != BRIDGE_EMBEDDED
286
- if external and new[CONF_BRIDGE_ROOT] != old[CONF_BRIDGE_ROOT]:
287
- from .config_flow import _probe_bridge
288
-
289
- error = await _probe_bridge(hass, {**old, **new})
290
- if error is not None:
291
- return self.json_message(
292
- f"no rustuya-bridge answers on {new[CONF_BRIDGE_ROOT]}" if error == "bridge_not_found"
293
- else "cannot connect to the broker", HTTPStatus.BAD_REQUEST)
294
- await _apply(hass, entry, old, {**old, **new}) # answered once it has restarted
295
- return self.json({"restarting": True})
279
+ body = await _json_body(request)
280
+ async with _settings_lock(hass):
281
+ try:
282
+ new = validate_settings(hass, entry.data, body)
283
+ except ValueError as e:
284
+ return self.json_message(str(e), HTTPStatus.BAD_REQUEST)
285
+ old = dict(entry.data)
286
+ if new == settings_of(old):
287
+ return self.json({"restarting": False})
288
+ external = old.get(CONF_BRIDGE_MODE) != BRIDGE_EMBEDDED
289
+ if external and new[CONF_BRIDGE_ROOT] != old[CONF_BRIDGE_ROOT]:
290
+ from .config_flow import _probe_bridge
291
+
292
+ error = await _probe_bridge(hass, {**old, **new})
293
+ if error is not None:
294
+ return self.json_message(
295
+ f"no rustuya-bridge answers on {new[CONF_BRIDGE_ROOT]}" if error == "bridge_not_found"
296
+ else "cannot connect to the broker", HTTPStatus.BAD_REQUEST)
297
+ await _apply(hass, entry, old, {**old, **new}) # answered once it has restarted
298
+ return self.json({"restarting": True})
299
+
300
+
301
+ def _settings_lock(hass: HomeAssistant) -> asyncio.Lock:
302
+ return hass.data.setdefault(f"{DOMAIN}_settings_lock", asyncio.Lock())
296
303
 
297
304
 
298
305
  async def _apply(hass: HomeAssistant, entry: Any, old: dict[str, Any], data: dict[str, Any]) -> None:
@@ -318,34 +325,33 @@ async def _restart(hass: HomeAssistant, entry: Any, *, data: dict[str, Any] | No
318
325
  """Stop the entry, run `between`, store the new data / options, start it again — all awaited, so the panel's save
319
326
  answers once the restart is over (the page then reloads onto the restarted entry, not onto one about to go).
320
327
  Stopped, the entry has no update listener, so storing does not start a second reload. An options-only restart
321
- keeps the IL presence online across it (`mark_restart`); new data may move the broker or the IL topics."""
322
- lock = hass.data.setdefault(f"{DOMAIN}_settings_lock", asyncio.Lock())
323
- async with lock:
324
- old_data, old_options = dict(entry.data), dict(entry.options)
325
- if data is None:
326
- from . import mark_restart
327
-
328
- mark_restart(hass, entry)
329
- if not await hass.config_entries.async_unload(entry.entry_id):
330
- raise RestartFailed("Could not stop the integration; settings were not changed")
328
+ keeps the IL presence online across it (`mark_restart`); new data may move the broker or the IL topics.
329
+ The request handler holds `_settings_lock` from reading the current entry through this restart."""
330
+ old_data, old_options = dict(entry.data), dict(entry.options)
331
+ if data is None:
332
+ from . import mark_restart
333
+
334
+ mark_restart(hass, entry)
335
+ if not await hass.config_entries.async_unload(entry.entry_id):
336
+ raise RestartFailed("Could not stop the integration; settings were not changed")
337
+ try:
338
+ if between is not None:
339
+ await between()
340
+ changes = {k: v for k, v in (("data", data), ("options", options)) if v is not None}
341
+ hass.config_entries.async_update_entry(entry, **changes)
342
+ if not await hass.config_entries.async_setup(entry.entry_id):
343
+ raise RestartFailed("The integration could not start with these settings")
344
+ except Exception as e:
345
+ # Failed setup leaves SETUP_ERROR/SETUP_RETRY, which cannot be set up
346
+ # directly. Unload also cancels any scheduled retry with the bad data.
347
+ await hass.config_entries.async_unload(entry.entry_id)
348
+ hass.config_entries.async_update_entry(entry, data=old_data, options=old_options)
331
349
  try:
332
- if between is not None:
333
- await between()
334
- changes = {k: v for k, v in (("data", data), ("options", options)) if v is not None}
335
- hass.config_entries.async_update_entry(entry, **changes)
336
- if not await hass.config_entries.async_setup(entry.entry_id):
337
- raise RestartFailed("The integration could not start with these settings")
338
- except Exception as e:
339
- # Failed setup leaves SETUP_ERROR/SETUP_RETRY, which cannot be set up
340
- # directly. Unload also cancels any scheduled retry with the bad data.
341
- await hass.config_entries.async_unload(entry.entry_id)
342
- hass.config_entries.async_update_entry(entry, data=old_data, options=old_options)
343
- try:
344
- restored = await hass.config_entries.async_setup(entry.entry_id)
345
- except Exception: # noqa: BLE001 -- keep the recovery API usable even if rollback startup fails
346
- restored = False
347
- suffix = "Previous settings restored" if restored else "Previous settings restored, but startup still fails"
348
- raise RestartFailed(f"{e}. {suffix}; settings remain editable") from e
350
+ restored = await hass.config_entries.async_setup(entry.entry_id)
351
+ except Exception: # noqa: BLE001 -- keep the recovery API usable even if rollback startup fails
352
+ restored = False
353
+ suffix = "Previous settings restored" if restored else "Previous settings restored, but startup still fails"
354
+ raise RestartFailed(f"{e}. {suffix}; settings remain editable") from e
349
355
 
350
356
 
351
357
  class PackView(_View):
@@ -19,7 +19,7 @@
19
19
  "il_prefix": "IL topic prefix"
20
20
  },
21
21
  "data_description": {
22
- "il_prefix": "Where the devices are published for il-ha (or any IL consumer); il-ha reads il by default."
22
+ "il_prefix": "Where the devices are published for ildevice-homeassistant (or any IL consumer); ildevice-homeassistant reads il by default."
23
23
  }
24
24
  },
25
25
  "embedded": {
@@ -33,7 +33,7 @@
33
33
  "il_prefix": "IL topic prefix"
34
34
  },
35
35
  "data_description": {
36
- "il_prefix": "Where the devices are published for il-ha (or any IL consumer); il-ha reads il by default."
36
+ "il_prefix": "Where the devices are published for ildevice-homeassistant (or any IL consumer); ildevice-homeassistant reads il by default."
37
37
  }
38
38
  },
39
39
  "onboard": {
@@ -152,7 +152,7 @@
152
152
  "fields": {
153
153
  "device_id": {
154
154
  "name": "Device ID",
155
- "description": "The Tuya device ID (as in the device list), or the Home Assistant device an IL consumer such as il-ha made of it."
155
+ "description": "The Tuya device ID (as in the device list), or the Home Assistant device an IL consumer such as ildevice-homeassistant made of it."
156
156
  },
157
157
  "dp": {
158
158
  "name": "DP",
@@ -19,7 +19,7 @@
19
19
  "il_prefix": "IL topic prefix"
20
20
  },
21
21
  "data_description": {
22
- "il_prefix": "Where the devices are published for il-ha (or any IL consumer); il-ha reads il by default."
22
+ "il_prefix": "Where the devices are published for ildevice-homeassistant (or any IL consumer); ildevice-homeassistant reads il by default."
23
23
  }
24
24
  },
25
25
  "embedded": {
@@ -33,7 +33,7 @@
33
33
  "il_prefix": "IL topic prefix"
34
34
  },
35
35
  "data_description": {
36
- "il_prefix": "Where the devices are published for il-ha (or any IL consumer); il-ha reads il by default."
36
+ "il_prefix": "Where the devices are published for ildevice-homeassistant (or any IL consumer); ildevice-homeassistant reads il by default."
37
37
  }
38
38
  },
39
39
  "onboard": {
@@ -152,7 +152,7 @@
152
152
  "fields": {
153
153
  "device_id": {
154
154
  "name": "Device ID",
155
- "description": "The Tuya device ID (as in the device list), or the Home Assistant device an IL consumer such as il-ha made of it."
155
+ "description": "The Tuya device ID (as in the device list), or the Home Assistant device an IL consumer such as ildevice-homeassistant made of it."
156
156
  },
157
157
  "dp": {
158
158
  "name": "DP",
@@ -19,7 +19,7 @@
19
19
  "il_prefix": "IL 토픽 prefix"
20
20
  },
21
21
  "data_description": {
22
- "il_prefix": "il-ha(또는 다른 IL 소비자)가 읽을 기기를 게시하는 곳입니다. il-ha는 기본으로 il을 읽습니다."
22
+ "il_prefix": "ildevice-homeassistant(또는 다른 IL 소비자)가 읽을 기기를 게시하는 곳입니다. ildevice-homeassistant는 기본으로 il을 읽습니다."
23
23
  }
24
24
  },
25
25
  "embedded": {
@@ -33,7 +33,7 @@
33
33
  "il_prefix": "IL 토픽 prefix"
34
34
  },
35
35
  "data_description": {
36
- "il_prefix": "il-ha(또는 다른 IL 소비자)가 읽을 기기를 게시하는 곳입니다. il-ha는 기본으로 il을 읽습니다."
36
+ "il_prefix": "ildevice-homeassistant(또는 다른 IL 소비자)가 읽을 기기를 게시하는 곳입니다. ildevice-homeassistant는 기본으로 il을 읽습니다."
37
37
  }
38
38
  },
39
39
  "onboard": {
@@ -152,7 +152,7 @@
152
152
  "fields": {
153
153
  "device_id": {
154
154
  "name": "기기 ID",
155
- "description": "Tuya 기기 ID (기기 목록의 것), 또는 il-ha 같은 IL consumer가 만든 Home Assistant 기기 ID."
155
+ "description": "Tuya 기기 ID (기기 목록의 것), 또는 ildevice-homeassistant 같은 IL consumer가 만든 Home Assistant 기기 ID."
156
156
  },
157
157
  "dp": {
158
158
  "name": "DP",