rustuya-local 0.1.0.dev4__tar.gz → 0.1.0.dev6__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 (103) hide show
  1. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/.github/workflows/_test.yml +6 -6
  2. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/.github/workflows/release.yml +1 -1
  3. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/CHANGELOG.md +17 -0
  4. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/PKG-INFO +25 -18
  5. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/README.md +22 -15
  6. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/custom_components/rustuya/bridge_supervisor.py +1 -1
  7. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/custom_components/rustuya/const.py +1 -1
  8. rustuya_local-0.1.0.dev6/custom_components/rustuya/manager_registry.py +71 -0
  9. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/custom_components/rustuya/manager_session.py +23 -1
  10. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/custom_components/rustuya/manifest.json +3 -3
  11. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/custom_components/rustuya/panel.py +8 -4
  12. rustuya_local-0.1.0.dev6/custom_components/rustuya/panel_bridge.py +48 -0
  13. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/custom_components/rustuya/www/rustuya-panel.js +86 -34
  14. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/docs/MIGRATING.md +4 -4
  15. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/docs/ROADMAP.md +2 -2
  16. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/pyproject.toml +3 -3
  17. rustuya_local-0.1.0.dev6/release-notes.md +3 -0
  18. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/scripts/build_dropin.py +5 -5
  19. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/src/rustuya_local/__init__.py +2 -2
  20. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/src/rustuya_local/bridge_client.py +31 -4
  21. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/src/rustuya_local/config.py +1 -1
  22. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/src/rustuya_local/host/__init__.py +1 -1
  23. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/src/rustuya_local/host/devices.py +1 -1
  24. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/src/rustuya_local/host/hub.py +9 -2
  25. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/src/rustuya_local/host/il.py +2 -2
  26. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/src/rustuya_local/host/overrides.py +2 -2
  27. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/src/rustuya_local/host/pack.py +27 -4
  28. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/src/rustuya_local/host/rules.py +3 -3
  29. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/src/rustuya_local/host/runner.py +2 -2
  30. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/src/rustuya_local/manager_plugin/__init__.py +1 -1
  31. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/src/rustuya_local/manager_plugin/static/index.js +1 -1
  32. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/src/rustuya_local/service.py +30 -9
  33. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/tests/e2e/devices.py +1 -1
  34. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/tests/e2e/test_bridge_registration.py +70 -0
  35. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/tests/e2e/test_full_stack_devices.py +3 -3
  36. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/tests/e2e/test_manager_plugin.py +6 -6
  37. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/tests/e2e/test_service.py +40 -0
  38. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/tests/ha/fake_manager.py +4 -0
  39. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/tests/ha/test_init.py +2 -2
  40. rustuya_local-0.1.0.dev6/tests/ha/test_manager_registry.py +69 -0
  41. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/tests/ha/test_panel.py +50 -0
  42. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/tests/unit/test_cover_settings.py +3 -2
  43. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/tests/unit/test_dropin.py +9 -9
  44. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/tests/unit/test_host.py +4 -4
  45. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/tests/unit/test_hub.py +2 -2
  46. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/tests/unit/test_il_vectors.py +2 -2
  47. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/tests/unit/test_link_watcher.py +25 -3
  48. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/tests/unit/test_pack.py +37 -4
  49. rustuya_local-0.1.0.dev6/tests/unit/test_panel_js.py +198 -0
  50. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/tests/unit/test_rule_files.py +8 -8
  51. rustuya_local-0.1.0.dev4/release-notes.md +0 -5
  52. rustuya_local-0.1.0.dev4/tests/unit/test_panel_js.py +0 -80
  53. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/.github/dependabot.yml +0 -0
  54. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/.github/workflows/ci.yml +0 -0
  55. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/.github/workflows/hacs.yml +0 -0
  56. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/.github/workflows/hassfest.yml +0 -0
  57. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/.gitignore +0 -0
  58. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/LICENSE +0 -0
  59. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/custom_components/rustuya/__init__.py +0 -0
  60. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/custom_components/rustuya/brand/icon.png +0 -0
  61. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/custom_components/rustuya/brand/icon.svg +0 -0
  62. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/custom_components/rustuya/brand/icon@2x.png +0 -0
  63. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/custom_components/rustuya/brand/logo.png +0 -0
  64. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/custom_components/rustuya/brand/logo.svg +0 -0
  65. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/custom_components/rustuya/brand/logo@2x.png +0 -0
  66. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/custom_components/rustuya/bridge_sync.py +0 -0
  67. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/custom_components/rustuya/broker.py +0 -0
  68. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/custom_components/rustuya/config_flow.py +0 -0
  69. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/custom_components/rustuya/services.yaml +0 -0
  70. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/custom_components/rustuya/strings.json +0 -0
  71. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/custom_components/rustuya/translations/en.json +0 -0
  72. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/custom_components/rustuya/translations/ko.json +0 -0
  73. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/docs/REDESIGN.md +0 -0
  74. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/docs/STATUS.md +0 -0
  75. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/hacs.json +0 -0
  76. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/scripts/release_notes.py +0 -0
  77. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/src/rustuya_local/__main__.py +0 -0
  78. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/src/rustuya_local/async_tools.py +0 -0
  79. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/src/rustuya_local/cli.py +0 -0
  80. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/src/rustuya_local/files.py +0 -0
  81. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/src/rustuya_local/host/memory.py +0 -0
  82. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/src/rustuya_local/host/mqtt.py +0 -0
  83. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/src/rustuya_local/host/transport.py +0 -0
  84. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/src/rustuya_local/manager_plugin/api.py +0 -0
  85. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/src/rustuya_local/validation.py +0 -0
  86. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/tests/e2e/conftest.py +0 -0
  87. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/tests/e2e/test_chain_in_process.py +0 -0
  88. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/tests/e2e/test_chain_mqtt.py +0 -0
  89. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/tests/e2e/test_daemon.py +0 -0
  90. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/tests/e2e/test_full_stack.py +0 -0
  91. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/tests/e2e/test_full_stack_custom_templates.py +0 -0
  92. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/tests/e2e/test_purge.py +0 -0
  93. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/tests/ha/conftest.py +0 -0
  94. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/tests/ha/test_bridge_probe.py +0 -0
  95. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/tests/ha/test_config_flow.py +0 -0
  96. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/tests/ha/test_manager_session.py +0 -0
  97. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/tests/ha/test_migrate.py +0 -0
  98. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/tests/unit/test_config.py +0 -0
  99. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/tests/unit/test_files.py +0 -0
  100. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/tests/unit/test_hazardous.py +0 -0
  101. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/tests/unit/test_plugin_api.py +0 -0
  102. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/tests/unit/test_release_notes.py +0 -0
  103. {rustuya_local-0.1.0.dev4 → rustuya_local-0.1.0.dev6}/tests/unit/tuya_samples.py +0 -0
@@ -38,14 +38,14 @@ jobs:
38
38
  repository: 3735943886/ildevice-homeassistant
39
39
  path: ildevice-homeassistant
40
40
 
41
- # tuya2ildevice is on PyPI (installed below via uv), so this isn't for dependency
41
+ # tuya-interpreter is on PyPI (installed below via uv), so this isn't for dependency
42
42
  # resolution -- it's so tests/e2e/test_full_stack_devices.py can reach its sibling
43
43
  # checkout's tests/golden fixture data, which isn't shipped in the published package.
44
- - name: Check out tuya2ildevice
44
+ - name: Check out tuya-interpreter
45
45
  uses: actions/checkout@v7
46
46
  with:
47
- repository: 3735943886/tuya2ildevice
48
- path: tuya2ildevice
47
+ repository: 3735943886/tuya-interpreter
48
+ path: tuya-interpreter
49
49
 
50
50
  - name: Set up Python ${{ matrix.python-version }}
51
51
  uses: actions/setup-python@v7
@@ -114,7 +114,7 @@ jobs:
114
114
  "$RUNNER_TEMP/smoke/bin/python" -c "
115
115
  import rustuya_local.cli, rustuya_local.manager_plugin, rustuya_local.service
116
116
  from rustuya_local import register
117
- import tuya2ildevice
118
- tuya2ildevice.preload()
117
+ import tuya_interpreter
118
+ tuya_interpreter.preload()
119
119
  print('ok')"
120
120
  "$RUNNER_TEMP/smoke/bin/rustuya-local" --help
@@ -96,7 +96,7 @@ jobs:
96
96
  run: twine check --strict dist/* # --strict: a missing README (long description) fails the release
97
97
 
98
98
  - name: Build the rustuya-manager drop-in zip
99
- # The manager's plugin catalog installs this asset (it cannot pip-install): rustuya_local with the tuya2ildevice
99
+ # The manager's plugin catalog installs this asset (it cannot pip-install): rustuya_local with the tuya-interpreter
100
100
  # the wheel resolves to vendored inside. Kept out of dist/ so it is not uploaded to PyPI.
101
101
  run: |
102
102
  python -m pip install dist/*.whl
@@ -8,6 +8,23 @@ stable release.
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [0.1.0.dev6] — 2026-10-12
12
+
13
+ ### Changed
14
+ - Require tuya-interpreter 0.1.0a1 (tuya2ildevice renamed) in place of tuya2ildevice, in the package, the Home Assistant integration and the manager drop-in archive.
15
+ - Fetch builtin rule updates from tuya-interpreter's `master` branch.
16
+
17
+ ## [0.1.0.dev5] — 2026-10-11
18
+
19
+ ### Fixed
20
+ - Share the running service's complete bridge registry and MQTT command connection with the panel, so panel listing and edits do not open a second registry requester. Require complete contiguous registry pages for standalone setup sessions.
21
+ - Keep explicit offline connection errors from being overwritten by replayed device state snapshots in the panel.
22
+
23
+ ### Changed
24
+ - Split the HA panel synced filter into online and offline, with live sub-device/gateway state and migration of saved filters.
25
+ - Check builtin rules when opening the HA panel; automatically install available updates when enabled, or enable the manual update button with a yellow dot when automatic updates are off.
26
+ - Allow manual builtin rule updates while automatic updates are disabled.
27
+
11
28
  ## [0.1.0.dev4] — 2026-10-11
12
29
 
13
30
  ### Fixed
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: rustuya-local
3
- Version: 0.1.0.dev4
4
- Summary: Local Tuya control: rustuya-bridge -> tuya2ildevice -> IL (ildevice). HA-independent core.
3
+ Version: 0.1.0.dev6
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
7
7
  Project-URL: Issues, https://github.com/3735943886/rustuya-local/issues
@@ -10,7 +10,7 @@ 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: tuya2ildevice>=0.4.0.dev4
13
+ Requires-Dist: tuya-interpreter>=0.1.0a1
14
14
  Provides-Extra: fullstack
15
15
  Requires-Dist: tuyamock; extra == 'fullstack'
16
16
  Provides-Extra: manager
@@ -37,7 +37,7 @@ rustuya-bridge ──MQTT──► rustuya-local ──MQTT (il/…)──► il
37
37
  BridgeClient + rustuya_local.host.Hub
38
38
  ```
39
39
 
40
- - **[tuya2ildevice](https://github.com/3735943886/tuya2ildevice)** is the only place that interprets Tuya: Home Assistant core's `tuya` behaviour and
40
+ - **[tuya-interpreter](https://github.com/3735943886/tuya-interpreter)** is the only place that interprets Tuya: Home Assistant core's `tuya` behaviour and
41
41
  user overrides for non-standard devices, through its Sans-I/O Rust engine and Python wrapper.
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
@@ -62,7 +62,7 @@ Stopping the daemon only marks it `offline`; its devices stay in IL (retained) f
62
62
  and run `rustuya-local purge --config config.json`: the retained descriptors, values and presence of its IL prefix and
63
63
  source are cleared (other sources on the prefix are left alone), so IL consumers drop the devices.
64
64
 
65
- ## tuya2ildevice 0.4 host boundary
65
+ ## tuya-interpreter host boundary
66
66
 
67
67
  The shared Rust core interprets one device and has no MQTT or rule-file loading API.
68
68
  `rustuya_local.host.Hub` owns the device registry and maps `TuyaDriver` outputs to IL MQTT wire data;
@@ -96,20 +96,24 @@ everything on an external bridge, are left as they are.
96
96
  Tuya's cloud schemas are fragmented; fixes go in a `custom_converters/` directory (daemon: `custom_converters` in the
97
97
  config; integration: `<config>/rustuya_converters`; manager plugin: its data dir), followed live:
98
98
 
99
- - `*.json`: tuya2ildevice override blocks by product or device id: rename a dp, define one the schema lacks, map the
99
+ - `*.json`: tuya-interpreter override blocks by product or device id: rename a dp, define one the schema lacks, map the
100
100
  device's words to the standard ones (`remap.alias`), fix labels and classes, define a declarative converter. A cover's
101
101
  direction is not set here but with its settings (below). Each file is a rule object with fixes under `overrides`, for example
102
102
  `{"overrides": {"product_id": {"cover": {"invert_position": true}}}}`.
103
103
  - Converters are JSON programs under `converters.declarative`; Python code plugins are not loaded.
104
104
 
105
- See tuya2ildevice's README ("User overrides") for the format. Official builtin rules, including `00-default.json`,
106
- ship with tuya2ildevice and update when that package is upgraded. They are applied before your own rule files.
105
+ See tuya-interpreter's [device customization guide](https://github.com/3735943886/tuya-interpreter/blob/master/docs/device-customization.md#user-overrides) for the format. Official builtin rules, including `00-default.json`,
106
+ ship with tuya-interpreter and update when that package is upgraded. They are applied before your own rule files.
107
107
 
108
- Online updates fetch the latest [builtin rules](https://github.com/3735943886/tuya2ildevice/tree/feat/rust-rule-engine/rules/builtin)
108
+ Online updates fetch the latest [builtin rules](https://github.com/3735943886/tuya-interpreter/tree/master/rules/builtin)
109
109
  into `rules/builtin/` inside the overrides directory at start and daily. In the Rustuya panel, use *Update builtin rules*
110
- to fetch them now or *Automatically update builtin rules* to control automatic updates. The daemon uses `"pack": false`
111
- to disable online updates; the manager plugin has the same option in its settings. Installed builtin rules remain
112
- available when online updates are off. Updates only replace or remove files they downloaded, preserving user edits.
110
+ to fetch them now or *Automatically update builtin rules* to control automatic updates. Opening the panel checks for
111
+ updates: with automatic updates on, available changes are installed immediately without a yellow dot. With automatic
112
+ updates off, available changes enable the manual update button with a yellow dot. The button is disabled while checking
113
+ or when the installed rules are current. The daemon uses `"pack": false`
114
+ to disable automatic updates; the manager plugin has the same option in its settings. Installed builtin rules remain
115
+ available when automatic updates are off, and the panel still allows manual updates. Updates only replace or remove
116
+ files they downloaded, preserving user edits.
113
117
 
114
118
  ### Cover settings
115
119
 
@@ -136,9 +140,12 @@ Older `cover.infer_motion` and `converters.cover_motion` blocks are upgraded on
136
140
 
137
141
  In Home Assistant the files can be edited from a sidebar panel: the integration's *Configure* → *Add the Rustuya
138
142
  panel to the sidebar* adds it and links to it. The panel is for administrators. It fetches the device list from Tuya Cloud, shows the
139
- bridge's devices against the cloud list as rustuya-manager does (missing, bridge only, mismatch, synced, each a filter you can
143
+ bridge's devices using the running service's complete registry and MQTT connection against the cloud list (missing, bridge only, mismatch, online and offline, each a filter you can
140
144
  turn off; sub-devices under their gateway; each device's bridge connection, live while the panel is open) with add /
141
- update / remove. *Register manually* adds Wi-Fi devices or sub-devices without a cloud login, using the same
145
+ update / remove. Online and offline split devices whose cloud and bridge records match; selecting both shows the
146
+ previous synced view. Sub-devices use their own link state, falling back to their gateway when unavailable; an offline
147
+ gateway makes its sub-devices offline. Unknown state is grouped under offline. A gateway stays visible for context
148
+ when one of its children matches the filters. *Register manually* adds Wi-Fi devices or sub-devices without a cloud login, using the same
142
149
  fields as rustuya-manager. Devices absent from the cloud list appear under *Bridge only*; their removal is not
143
150
  preselected in bulk sync. The panel lists, edits and deletes user converter files (drop JSON rule files on it to copy them in),
144
151
  updates builtin rules on demand, and has the
@@ -151,7 +158,7 @@ language: Korean, or English.
151
158
  ## rustuya-manager plugin
152
159
 
153
160
  Installed next to rustuya-manager, as a pip package (`pip install rustuya-local` in the manager's environment) or as
154
- the drop-in zip each release carries (`rustuya_local-<version>-dropin.zip`, tuya2ildevice binding, native library and builtin rules vendored inside, built by
161
+ the drop-in zip each release carries (`rustuya_local-<version>-dropin.zip`, tuya-interpreter binding, native library and builtin rules vendored inside, built by
155
162
  `scripts/build_dropin.py`; unpack it into the manager's plugin directory, or install it from the manager's plugin
156
163
  catalog once listed there), it appears as a **Tuya (IL)** tab and runs the service under the manager: the manager's broker and bridge root, the manager's device
157
164
  list (followed). Its data dir (`rustuya-local/` next to the manager's plugins) holds `settings.json` (`il`,
@@ -159,7 +166,7 @@ list (followed). Its data dir (`rustuya-local/` next to the manager's plugins) h
159
166
  converter files are picked up while it runs. A settings file it cannot use is shown on the tab until it is fixed.
160
167
 
161
168
  The drop-in zip includes native code for the platform on which it was built; use a matching OS and architecture,
162
- or install with pip to select the appropriate wheel. It freezes the tuya2ildevice it was built with: a tuya2ildevice fix reaches manager users with the next
169
+ or install with pip to select the appropriate wheel. It freezes the tuya-interpreter it was built with: a tuya-interpreter fix reaches manager users with the next
163
170
  rustuya-local release (bump the patch version and tag; the manager's catalog follows the release). Installed both ways
164
171
  (pip and the plugin directory), the plugin runs once.
165
172
 
@@ -171,7 +178,7 @@ rustuya-local release (bump the patch version and tag; the manager's catalog fol
171
178
 
172
179
  Everything here needs the sibling repos and, for the last two groups, `mosquitto`, `pyrustuya-bridge` and `tuyamock`
173
180
  (`pip install -e ".[test,fullstack]"`); those are skipped when missing. Parity with Home Assistant core's fixtures lives in
174
- `tuya2ildevice/tests/chain`, the wire and topic vectors in `ildevice/vectors`.
181
+ `tuya-interpreter/tests/chain`, the wire and topic vectors in `ildevice/vectors`.
175
182
 
176
183
  - `tests/unit/`: the configuration.
177
184
  - `tests/e2e/test_service.py`: the service on in-memory brokers (overrides directory, a failed start).
@@ -181,7 +188,7 @@ Everything here needs the sibling repos and, for the last two groups, `mosquitto
181
188
  - `tests/ha/`: the integration (config flow, setup).
182
189
  - `tests/e2e/test_full_stack*.py`: the real rustuya-bridge (`pyrustuyabridge`, in process) and Tuya device emulators
183
190
  (`tuyamock`) around the daemon: state, commands, rejections, a device dropping off and returning, a daemon restart, and a
184
- differential check that 38 real device shapes come out of the whole chain exactly as `tuya2ildevice` computes them.
191
+ differential check that 38 real device shapes come out of the whole chain exactly as `tuya-interpreter` computes them.
185
192
 
186
193
  ## Status
187
194
 
@@ -11,7 +11,7 @@ rustuya-bridge ──MQTT──► rustuya-local ──MQTT (il/…)──► il
11
11
  BridgeClient + rustuya_local.host.Hub
12
12
  ```
13
13
 
14
- - **[tuya2ildevice](https://github.com/3735943886/tuya2ildevice)** is the only place that interprets Tuya: Home Assistant core's `tuya` behaviour and
14
+ - **[tuya-interpreter](https://github.com/3735943886/tuya-interpreter)** is the only place that interprets Tuya: Home Assistant core's `tuya` behaviour and
15
15
  user overrides for non-standard devices, through its Sans-I/O Rust engine and Python wrapper.
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
@@ -36,7 +36,7 @@ Stopping the daemon only marks it `offline`; its devices stay in IL (retained) f
36
36
  and run `rustuya-local purge --config config.json`: the retained descriptors, values and presence of its IL prefix and
37
37
  source are cleared (other sources on the prefix are left alone), so IL consumers drop the devices.
38
38
 
39
- ## tuya2ildevice 0.4 host boundary
39
+ ## tuya-interpreter host boundary
40
40
 
41
41
  The shared Rust core interprets one device and has no MQTT or rule-file loading API.
42
42
  `rustuya_local.host.Hub` owns the device registry and maps `TuyaDriver` outputs to IL MQTT wire data;
@@ -70,20 +70,24 @@ everything on an external bridge, are left as they are.
70
70
  Tuya's cloud schemas are fragmented; fixes go in a `custom_converters/` directory (daemon: `custom_converters` in the
71
71
  config; integration: `<config>/rustuya_converters`; manager plugin: its data dir), followed live:
72
72
 
73
- - `*.json`: tuya2ildevice override blocks by product or device id: rename a dp, define one the schema lacks, map the
73
+ - `*.json`: tuya-interpreter override blocks by product or device id: rename a dp, define one the schema lacks, map the
74
74
  device's words to the standard ones (`remap.alias`), fix labels and classes, define a declarative converter. A cover's
75
75
  direction is not set here but with its settings (below). Each file is a rule object with fixes under `overrides`, for example
76
76
  `{"overrides": {"product_id": {"cover": {"invert_position": true}}}}`.
77
77
  - Converters are JSON programs under `converters.declarative`; Python code plugins are not loaded.
78
78
 
79
- See tuya2ildevice's README ("User overrides") for the format. Official builtin rules, including `00-default.json`,
80
- ship with tuya2ildevice and update when that package is upgraded. They are applied before your own rule files.
79
+ See tuya-interpreter's [device customization guide](https://github.com/3735943886/tuya-interpreter/blob/master/docs/device-customization.md#user-overrides) for the format. Official builtin rules, including `00-default.json`,
80
+ ship with tuya-interpreter and update when that package is upgraded. They are applied before your own rule files.
81
81
 
82
- Online updates fetch the latest [builtin rules](https://github.com/3735943886/tuya2ildevice/tree/feat/rust-rule-engine/rules/builtin)
82
+ Online updates fetch the latest [builtin rules](https://github.com/3735943886/tuya-interpreter/tree/master/rules/builtin)
83
83
  into `rules/builtin/` inside the overrides directory at start and daily. In the Rustuya panel, use *Update builtin rules*
84
- to fetch them now or *Automatically update builtin rules* to control automatic updates. The daemon uses `"pack": false`
85
- to disable online updates; the manager plugin has the same option in its settings. Installed builtin rules remain
86
- available when online updates are off. Updates only replace or remove files they downloaded, preserving user edits.
84
+ to fetch them now or *Automatically update builtin rules* to control automatic updates. Opening the panel checks for
85
+ updates: with automatic updates on, available changes are installed immediately without a yellow dot. With automatic
86
+ updates off, available changes enable the manual update button with a yellow dot. The button is disabled while checking
87
+ or when the installed rules are current. The daemon uses `"pack": false`
88
+ to disable automatic updates; the manager plugin has the same option in its settings. Installed builtin rules remain
89
+ available when automatic updates are off, and the panel still allows manual updates. Updates only replace or remove
90
+ files they downloaded, preserving user edits.
87
91
 
88
92
  ### Cover settings
89
93
 
@@ -110,9 +114,12 @@ Older `cover.infer_motion` and `converters.cover_motion` blocks are upgraded on
110
114
 
111
115
  In Home Assistant the files can be edited from a sidebar panel: the integration's *Configure* → *Add the Rustuya
112
116
  panel to the sidebar* adds it and links to it. The panel is for administrators. It fetches the device list from Tuya Cloud, shows the
113
- bridge's devices against the cloud list as rustuya-manager does (missing, bridge only, mismatch, synced, each a filter you can
117
+ bridge's devices using the running service's complete registry and MQTT connection against the cloud list (missing, bridge only, mismatch, online and offline, each a filter you can
114
118
  turn off; sub-devices under their gateway; each device's bridge connection, live while the panel is open) with add /
115
- update / remove. *Register manually* adds Wi-Fi devices or sub-devices without a cloud login, using the same
119
+ update / remove. Online and offline split devices whose cloud and bridge records match; selecting both shows the
120
+ previous synced view. Sub-devices use their own link state, falling back to their gateway when unavailable; an offline
121
+ gateway makes its sub-devices offline. Unknown state is grouped under offline. A gateway stays visible for context
122
+ when one of its children matches the filters. *Register manually* adds Wi-Fi devices or sub-devices without a cloud login, using the same
116
123
  fields as rustuya-manager. Devices absent from the cloud list appear under *Bridge only*; their removal is not
117
124
  preselected in bulk sync. The panel lists, edits and deletes user converter files (drop JSON rule files on it to copy them in),
118
125
  updates builtin rules on demand, and has the
@@ -125,7 +132,7 @@ language: Korean, or English.
125
132
  ## rustuya-manager plugin
126
133
 
127
134
  Installed next to rustuya-manager, as a pip package (`pip install rustuya-local` in the manager's environment) or as
128
- the drop-in zip each release carries (`rustuya_local-<version>-dropin.zip`, tuya2ildevice binding, native library and builtin rules vendored inside, built by
135
+ the drop-in zip each release carries (`rustuya_local-<version>-dropin.zip`, tuya-interpreter binding, native library and builtin rules vendored inside, built by
129
136
  `scripts/build_dropin.py`; unpack it into the manager's plugin directory, or install it from the manager's plugin
130
137
  catalog once listed there), it appears as a **Tuya (IL)** tab and runs the service under the manager: the manager's broker and bridge root, the manager's device
131
138
  list (followed). Its data dir (`rustuya-local/` next to the manager's plugins) holds `settings.json` (`il`,
@@ -133,7 +140,7 @@ list (followed). Its data dir (`rustuya-local/` next to the manager's plugins) h
133
140
  converter files are picked up while it runs. A settings file it cannot use is shown on the tab until it is fixed.
134
141
 
135
142
  The drop-in zip includes native code for the platform on which it was built; use a matching OS and architecture,
136
- or install with pip to select the appropriate wheel. It freezes the tuya2ildevice it was built with: a tuya2ildevice fix reaches manager users with the next
143
+ or install with pip to select the appropriate wheel. It freezes the tuya-interpreter it was built with: a tuya-interpreter fix reaches manager users with the next
137
144
  rustuya-local release (bump the patch version and tag; the manager's catalog follows the release). Installed both ways
138
145
  (pip and the plugin directory), the plugin runs once.
139
146
 
@@ -145,7 +152,7 @@ rustuya-local release (bump the patch version and tag; the manager's catalog fol
145
152
 
146
153
  Everything here needs the sibling repos and, for the last two groups, `mosquitto`, `pyrustuya-bridge` and `tuyamock`
147
154
  (`pip install -e ".[test,fullstack]"`); those are skipped when missing. Parity with Home Assistant core's fixtures lives in
148
- `tuya2ildevice/tests/chain`, the wire and topic vectors in `ildevice/vectors`.
155
+ `tuya-interpreter/tests/chain`, the wire and topic vectors in `ildevice/vectors`.
149
156
 
150
157
  - `tests/unit/`: the configuration.
151
158
  - `tests/e2e/test_service.py`: the service on in-memory brokers (overrides directory, a failed start).
@@ -155,7 +162,7 @@ Everything here needs the sibling repos and, for the last two groups, `mosquitto
155
162
  - `tests/ha/`: the integration (config flow, setup).
156
163
  - `tests/e2e/test_full_stack*.py`: the real rustuya-bridge (`pyrustuyabridge`, in process) and Tuya device emulators
157
164
  (`tuyamock`) around the daemon: state, commands, rejections, a device dropping off and returning, a daemon restart, and a
158
- differential check that 38 real device shapes come out of the whole chain exactly as `tuya2ildevice` computes them.
165
+ differential check that 38 real device shapes come out of the whole chain exactly as `tuya-interpreter` computes them.
159
166
 
160
167
  ## Status
161
168
 
@@ -1,6 +1,6 @@
1
1
  """An embedded rustuya-bridge: `pyrustuyabridge.PyBridgeServer` run in a background thread, tied to this config
2
2
  entry's lifecycle instead of a separate rustuya-bridge process. Same class our own end-to-end tests drive
3
- (tuya2ildevice/rustuya-local tests/e2e/test_full_stack.py) — proven against a real broker and real Tuya-protocol
3
+ (tuya-interpreter/rustuya-local tests/e2e/test_full_stack.py) — proven against a real broker and real Tuya-protocol
4
4
  devices, just supervised here instead of by a test fixture.
5
5
 
6
6
  The embedded bridge still needs a real MQTT broker to talk through (it manages its own native MQTT client); it does
@@ -28,7 +28,7 @@ CONF_DEVICES_PATH = "devices_path" # tuyadevices.json; the cloud wi
28
28
  # --- entry options (changed any time through the options flow) -------------------------
29
29
  CONF_ALLOW_HAZARDOUS = "allow_hazardous"
30
30
  CONF_EXPOSE_UNUSED = "expose_unused"
31
- CONF_PACK = "pack" # sync tuya2ildevice's override pack into CONVERTERS_DIR
31
+ CONF_PACK = "pack" # sync tuya-interpreter's override pack into CONVERTERS_DIR
32
32
  CONF_PANEL = "panel" # the sidebar page (panel.py); Configure adds / removes it
33
33
 
34
34
  DEFAULT_OPTIONS = {CONF_ALLOW_HAZARDOUS: False, CONF_EXPOSE_UNUSED: False, CONF_PACK: True}
@@ -0,0 +1,71 @@
1
+ """Per-session compatibility guard for manager versions accepting incomplete status pages.
2
+
3
+ The response topic is broadcast: the service and a short-lived Manager can receive each other's
4
+ pages. Only a contiguous snapshot starting at zero may become the panel/config flow's registry.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import asyncio
9
+ import time
10
+ from typing import Any
11
+
12
+
13
+ async def ready_registry(manager: Any, timeout: float) -> None:
14
+ client = getattr(manager, "client", None)
15
+ if client is None or not hasattr(client, "_handle_status_page"):
16
+ await manager.wait_ready()
17
+ return
18
+
19
+ request = client._request_status
20
+ handle = client._handle_status_page
21
+ offset = 0
22
+ total = None
23
+ ids: set[str] = set()
24
+ complete = False
25
+
26
+ async def request_status() -> None:
27
+ nonlocal offset, total, complete
28
+ if (not client._status_active or client._status_started is None
29
+ or time.monotonic() - client._status_started >= client._STATUS_CYCLE_TIMEOUT):
30
+ offset, total, complete = 0, None, False
31
+ ids.clear()
32
+ client._status_accum = None
33
+ await request()
34
+
35
+ async def handle_page(parsed: dict[str, Any]) -> None:
36
+ nonlocal offset, total, complete
37
+ if not client._status_active:
38
+ return
39
+ devices = parsed.get("devices")
40
+ start = parsed.get("offset", 0)
41
+ returned = parsed.get("returned", len(devices) if isinstance(devices, dict) else 0)
42
+ count = parsed.get("device_count")
43
+ more = parsed.get("has_more", False)
44
+ if (not isinstance(devices, dict) or type(start) is not int or start != offset
45
+ or type(returned) is not int or returned != len(devices)
46
+ or type(more) is not bool or (more and not returned)
47
+ or ids.intersection(devices)):
48
+ return
49
+ if count is not None and (type(count) is not int or count < start + returned
50
+ or more != (start + returned < count)):
51
+ return
52
+ if offset and count != total:
53
+ return
54
+ total = count
55
+ offset += returned
56
+ ids.update(devices)
57
+ complete = not more
58
+ await handle(parsed)
59
+
60
+ client._request_status = request_status
61
+ client._handle_status_page = handle_page
62
+ await client.wait_bootstrap(timeout=timeout)
63
+ if not client._bootstrap_done.is_set():
64
+ raise TimeoutError("bridge subscriptions are not ready")
65
+ # Bootstrap may have received pages before the guard was installed. Start a fresh
66
+ # offset-zero cycle after subscription readiness instead of trusting that snapshot.
67
+ client._status_started = None
68
+ await client._request_status()
69
+ await asyncio.wait_for(client._status_idle.wait(), timeout)
70
+ if not complete:
71
+ raise TimeoutError("incomplete bridge registry")
@@ -68,7 +68,9 @@ async def open_manager(*, broker: str, root: str, devices_path: str, username: s
68
68
  # `sync()`'s diff is only meaningful once the bridge's device list has arrived; without this every cloud
69
69
  # device looks "missing" (an empty bridge side), which is how a whole device list ended up offered for
70
70
  # registration.
71
- await manager.wait_ready()
71
+ from .manager_registry import ready_registry
72
+
73
+ await ready_registry(manager, WAIT)
72
74
  except BaseException:
73
75
  await close_manager(manager)
74
76
  raise
@@ -107,6 +109,26 @@ async def open_for_entry(hass: Any, data: dict[str, Any], *, run_bridge: bool =
107
109
  devices_path=data[CONF_DEVICES_PATH], username=username, password=password, **bridge)
108
110
 
109
111
 
112
+ async def open_panel_for_entry(hass: Any, data: dict[str, Any], service: Any):
113
+ """Share the loaded service's registry and command transport; no Manager MQTT connection."""
114
+ from .const import CONF_DEVICES_PATH
115
+ from .panel_bridge import PanelBridge
116
+
117
+ lock = _lock()
118
+ try:
119
+ await asyncio.wait_for(lock.acquire(), WAIT)
120
+ except TimeoutError as e:
121
+ raise Busy from e
122
+ session = PanelBridge(hass, service, data[CONF_DEVICES_PATH])
123
+ _HOLDERS[session] = lock
124
+ try:
125
+ await session.ready()
126
+ except BaseException:
127
+ await close_manager(session)
128
+ raise
129
+ return session
130
+
131
+
110
132
  async def saved_user_code(hass: Any, data: dict[str, Any]) -> str | None:
111
133
  """The user code of the saved Tuya login (`tuyacreds.json` beside the device file, where a session's wizard keeps
112
134
  it), read without a session: no broker connection and no lock, so the panel can fill it in before a fetch starts."""
@@ -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
- "tuya2ildevice>=0.4.0.dev4",
12
+ "tuya-interpreter>=0.1.0a1",
13
13
  "paho-mqtt>=2.0",
14
14
  "rustuya-manager>=0.2.2",
15
15
  "pyrustuyabridge>=0.4",
16
- "rustuya-local>=0.1.0.dev4"
16
+ "rustuya-local>=0.1.0.dev6"
17
17
  ],
18
18
  "single_config_entry": true,
19
- "version": "0.1.0.dev4"
19
+ "version": "0.1.0.dev6"
20
20
  }
@@ -349,11 +349,15 @@ async def _restart(hass: HomeAssistant, entry: Any, *, data: dict[str, Any] | No
349
349
 
350
350
 
351
351
  class PackView(_View):
352
- """POST: sync the override pack now rather than at the end of its day; `pack` in the list shows the result."""
352
+ """GET checks for updates without writes; POST requests a sync."""
353
353
 
354
354
  url = "/api/rustuya/pack"
355
355
  name = "api:rustuya:pack"
356
356
 
357
+ @_entry_view
358
+ async def get(self, request: web.Request, entry: Any, runtime: Any) -> web.Response:
359
+ return self.json(await runtime.service.check_pack())
360
+
357
361
  @_entry_view
358
362
  async def post(self, request: web.Request, entry: Any, runtime: Any) -> web.Response:
359
363
  return self.json({"started": runtime.service.sync_pack_now()})
@@ -536,8 +540,8 @@ def _manual_device(body: Any) -> dict[str, str]:
536
540
 
537
541
 
538
542
  class BridgeView(_View):
539
- """The cloud list against what the bridge holds, through a rustuya-manager session like the options flow's
540
- `bridge_sync` step (and sharing its lock: one session at a time). GET: every device with its category. POST
543
+ """The cloud list against the running service's complete bridge registry, sharing the management session lock.
544
+ GET: every device with its category. POST
541
545
  `{"add": [...], "update": [...], "remove": [...]}`: those commands (only for ids the diff has in that category),
542
546
  then the list again. PATCH edits an existing device; PUT registers a manual device (id/name, WiFi key/ip/version or SubDevice cid/parent_id).
543
547
  `online` is the running service's link state by id."""
@@ -592,7 +596,7 @@ class BridgeView(_View):
592
596
  if not manager_session.available():
593
597
  return self.json_message("rustuya-manager is not installed", HTTPStatus.NOT_IMPLEMENTED)
594
598
  try:
595
- manager = await manager_session.open_for_entry(request.app["hass"], entry.data)
599
+ manager = await manager_session.open_panel_for_entry(request.app["hass"], entry.data, runtime.service)
596
600
  except manager_session.Busy:
597
601
  return self.json_message("a Tuya login or device sync window is open; close it and try again",
598
602
  HTTPStatus.CONFLICT)
@@ -0,0 +1,48 @@
1
+ """Panel device management through the running service, without another MQTT registry requester."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ from pathlib import Path
6
+ from types import SimpleNamespace
7
+ from typing import Any
8
+
9
+
10
+ class PanelBridge:
11
+ def __init__(self, hass: Any, service: Any, devices_path: str) -> None:
12
+ self.hass = hass
13
+ self.client = getattr(service, "bridge_client", None)
14
+ self.path = Path(devices_path)
15
+ self.state = SimpleNamespace(cloud={}, bridge={})
16
+
17
+ async def ready(self) -> None:
18
+ if self.client is None:
19
+ raise RuntimeError("Rustuya service is not ready")
20
+ await self.sync()
21
+
22
+ async def sync(self):
23
+ from rustuya_manager.diff import diff
24
+ from rustuya_manager.models import Device
25
+
26
+ def cloud():
27
+ if not self.path.exists():
28
+ return {}
29
+ data = json.loads(self.path.read_text("utf-8"))
30
+ records = data if isinstance(data, list) else data.values()
31
+ return {d["id"]: Device.from_dict(d) for d in records if "id" in d}
32
+
33
+ self.state.cloud = await self.hass.async_add_executor_job(cloud)
34
+ records = await self.client.registry()
35
+ self.state.bridge = {id: Device.from_dict({**record, "id": id}) for id, record in records.items()}
36
+ return diff(self.state.cloud, self.state.bridge)
37
+
38
+ async def publish_command(self, action: str, **kw) -> None:
39
+ await self.client.publish_command(action, **kw)
40
+
41
+ async def remove_device(self, target_id: str) -> None:
42
+ await self.publish_command("remove", target_id=target_id)
43
+
44
+ async def wait_registry_refresh(self) -> None:
45
+ await self.client.registry(refresh=True)
46
+
47
+ async def __aexit__(self, *exc) -> None:
48
+ pass