rustuya-local 0.1.0.dev3__tar.gz → 0.1.0.dev5__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.
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/CHANGELOG.md +19 -0
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/PKG-INFO +24 -15
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/README.md +23 -14
- rustuya_local-0.1.0.dev5/custom_components/rustuya/manager_registry.py +71 -0
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/custom_components/rustuya/manager_session.py +23 -1
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/custom_components/rustuya/manifest.json +2 -2
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/custom_components/rustuya/panel.py +8 -4
- rustuya_local-0.1.0.dev5/custom_components/rustuya/panel_bridge.py +48 -0
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/custom_components/rustuya/strings.json +1 -1
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/custom_components/rustuya/translations/en.json +1 -1
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/custom_components/rustuya/translations/ko.json +1 -1
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/custom_components/rustuya/www/rustuya-panel.js +96 -44
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/pyproject.toml +1 -1
- rustuya_local-0.1.0.dev5/release-notes.md +8 -0
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/src/rustuya_local/bridge_client.py +30 -3
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/src/rustuya_local/host/pack.py +25 -2
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/src/rustuya_local/manager_plugin/static/index.js +3 -3
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/src/rustuya_local/service.py +26 -5
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/tests/e2e/test_bridge_registration.py +70 -0
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/tests/e2e/test_service.py +40 -0
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/tests/ha/fake_manager.py +4 -0
- rustuya_local-0.1.0.dev5/tests/ha/test_manager_registry.py +69 -0
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/tests/ha/test_panel.py +50 -0
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/tests/unit/test_cover_settings.py +2 -1
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/tests/unit/test_host.py +2 -2
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/tests/unit/test_link_watcher.py +25 -3
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/tests/unit/test_pack.py +33 -0
- rustuya_local-0.1.0.dev5/tests/unit/test_panel_js.py +198 -0
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/tests/unit/test_rule_files.py +1 -1
- rustuya_local-0.1.0.dev3/release-notes.md +0 -6
- rustuya_local-0.1.0.dev3/tests/unit/test_panel_js.py +0 -80
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/.github/dependabot.yml +0 -0
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/.github/workflows/_test.yml +0 -0
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/.github/workflows/ci.yml +0 -0
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/.github/workflows/hacs.yml +0 -0
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/.github/workflows/hassfest.yml +0 -0
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/.github/workflows/release.yml +0 -0
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/.gitignore +0 -0
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/LICENSE +0 -0
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/custom_components/rustuya/__init__.py +0 -0
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/custom_components/rustuya/brand/icon.png +0 -0
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/custom_components/rustuya/brand/icon.svg +0 -0
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/custom_components/rustuya/brand/icon@2x.png +0 -0
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/custom_components/rustuya/brand/logo.png +0 -0
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/custom_components/rustuya/brand/logo.svg +0 -0
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/custom_components/rustuya/brand/logo@2x.png +0 -0
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/custom_components/rustuya/bridge_supervisor.py +0 -0
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/custom_components/rustuya/bridge_sync.py +0 -0
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/custom_components/rustuya/broker.py +0 -0
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/custom_components/rustuya/config_flow.py +0 -0
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/custom_components/rustuya/const.py +0 -0
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/custom_components/rustuya/services.yaml +0 -0
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/docs/MIGRATING.md +0 -0
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/docs/REDESIGN.md +0 -0
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/docs/ROADMAP.md +0 -0
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/docs/STATUS.md +0 -0
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/hacs.json +0 -0
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/scripts/build_dropin.py +0 -0
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/scripts/release_notes.py +0 -0
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/src/rustuya_local/__init__.py +0 -0
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/src/rustuya_local/__main__.py +0 -0
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/src/rustuya_local/async_tools.py +0 -0
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/src/rustuya_local/cli.py +0 -0
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/src/rustuya_local/config.py +0 -0
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/src/rustuya_local/files.py +0 -0
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/src/rustuya_local/host/__init__.py +0 -0
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/src/rustuya_local/host/devices.py +0 -0
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/src/rustuya_local/host/hub.py +0 -0
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/src/rustuya_local/host/il.py +0 -0
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/src/rustuya_local/host/memory.py +0 -0
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/src/rustuya_local/host/mqtt.py +0 -0
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/src/rustuya_local/host/overrides.py +0 -0
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/src/rustuya_local/host/rules.py +0 -0
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/src/rustuya_local/host/runner.py +0 -0
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/src/rustuya_local/host/transport.py +0 -0
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/src/rustuya_local/manager_plugin/__init__.py +0 -0
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/src/rustuya_local/manager_plugin/api.py +0 -0
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/src/rustuya_local/validation.py +0 -0
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/tests/e2e/conftest.py +0 -0
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/tests/e2e/devices.py +0 -0
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/tests/e2e/test_chain_in_process.py +0 -0
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/tests/e2e/test_chain_mqtt.py +0 -0
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/tests/e2e/test_daemon.py +0 -0
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/tests/e2e/test_full_stack.py +0 -0
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/tests/e2e/test_full_stack_custom_templates.py +0 -0
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/tests/e2e/test_full_stack_devices.py +0 -0
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/tests/e2e/test_manager_plugin.py +0 -0
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/tests/e2e/test_purge.py +0 -0
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/tests/ha/conftest.py +0 -0
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/tests/ha/test_bridge_probe.py +0 -0
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/tests/ha/test_config_flow.py +0 -0
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/tests/ha/test_init.py +0 -0
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/tests/ha/test_manager_session.py +0 -0
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/tests/ha/test_migrate.py +0 -0
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/tests/unit/test_config.py +0 -0
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/tests/unit/test_dropin.py +0 -0
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/tests/unit/test_files.py +0 -0
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/tests/unit/test_hazardous.py +0 -0
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/tests/unit/test_hub.py +0 -0
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/tests/unit/test_il_vectors.py +0 -0
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/tests/unit/test_plugin_api.py +0 -0
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/tests/unit/test_release_notes.py +0 -0
- {rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/tests/unit/tuya_samples.py +0 -0
|
@@ -8,6 +8,25 @@ stable release.
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [0.1.0.dev5] — 2026-10-11
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
- 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.
|
|
15
|
+
- Keep explicit offline connection errors from being overwritten by replayed device state snapshots in the panel.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
- Split the HA panel synced filter into online and offline, with live sub-device/gateway state and migration of saved filters.
|
|
19
|
+
- 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.
|
|
20
|
+
- Allow manual builtin rule updates while automatic updates are disabled.
|
|
21
|
+
|
|
22
|
+
## [0.1.0.dev4] — 2026-10-11
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
- Fetch dev4 builtin rule updates from its development branch; master does not yet contain the new rules directory.
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
- Label official rule updates as builtin rules in the HA panel and manager settings, and clarify that bundled rules remain available when online updates are off.
|
|
29
|
+
|
|
11
30
|
## [0.1.0.dev3] — 2026-10-11
|
|
12
31
|
|
|
13
32
|
### Changed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: rustuya-local
|
|
3
|
-
Version: 0.1.0.
|
|
3
|
+
Version: 0.1.0.dev5
|
|
4
4
|
Summary: Local Tuya control: rustuya-bridge -> tuya2ildevice -> 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
|
|
@@ -66,7 +66,7 @@ source are cleared (other sources on the prefix are left alone), so IL consumers
|
|
|
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;
|
|
69
|
-
`host.rules` discovers and loads the bundled
|
|
69
|
+
`host.rules` discovers and loads the bundled builtin rules and user layers, passing decoded rules to Rust for composition.
|
|
70
70
|
The service preloads native code and rules in a worker thread before starting devices.
|
|
71
71
|
Timers, retained-message policy, bridge `get` requests, presence and settings persistence remain in this host.
|
|
72
72
|
MQTT and in-process transport tests exercise the same adapter and the bidirectional Rust driver.
|
|
@@ -102,12 +102,18 @@ config; integration: `<config>/rustuya_converters`; manager plugin: its data dir
|
|
|
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.
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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.
|
|
107
|
+
|
|
108
|
+
Online updates fetch the latest [builtin rules](https://github.com/3735943886/tuya2ildevice/tree/feat/rust-rule-engine/rules/builtin)
|
|
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. 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.
|
|
111
117
|
|
|
112
118
|
### Cover settings
|
|
113
119
|
|
|
@@ -128,20 +134,23 @@ device settings are enabled, unless an override selects another source. A settin
|
|
|
128
134
|
the setting is saved as the device's block in `zz_settings.json` in the converters directory and applied at once, so it
|
|
129
135
|
survives a restart and can be edited there too. Without a converters directory (a daemon with no
|
|
130
136
|
`custom_converters`) the switches are not offered; `"options": {"device_settings": false}` turns them off.
|
|
131
|
-
A block with the same `cover` keys in your own file or the
|
|
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.
|
|
132
138
|
Older `cover.infer_motion` and `converters.cover_motion` blocks are upgraded on loading to `cover.state_source`
|
|
133
139
|
(`inferred`, or `none` for `infer_motion: false`) and `cover.settle`; see [migration notes](docs/MIGRATING.md).
|
|
134
140
|
|
|
135
141
|
In Home Assistant the files can be edited from a sidebar panel: the integration's *Configure* → *Add the Rustuya
|
|
136
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
|
|
137
|
-
bridge's devices against the cloud list
|
|
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
|
|
138
144
|
turn off; sub-devices under their gateway; each device's bridge connection, live while the panel is open) with add /
|
|
139
|
-
update / remove.
|
|
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
|
|
140
149
|
fields as rustuya-manager. Devices absent from the cloud list appear under *Bridge only*; their removal is not
|
|
141
|
-
preselected in bulk sync. The panel lists
|
|
142
|
-
|
|
150
|
+
preselected in bulk sync. The panel lists, edits and deletes user converter files (drop JSON rule files on it to copy them in),
|
|
151
|
+
updates builtin rules on demand, and has the
|
|
143
152
|
*Options* (remote control of locks, alarms and garage doors; data points Home Assistant core would not classify; the
|
|
144
|
-
|
|
153
|
+
automatic builtin rule updates), and the *Settings* setup leaves at their defaults: the bridge topic root, the IL prefix and source, the device file, and
|
|
145
154
|
the embedded bridge's state file and log level. Saving restarts the integration; moving the IL prefix or source first
|
|
146
155
|
clears what the old one left on the broker. *Hide panel* in its toolbar turns it off again. The integration and the panel follow Home Assistant's
|
|
147
156
|
language: Korean, or English.
|
|
@@ -149,7 +158,7 @@ language: Korean, or English.
|
|
|
149
158
|
## rustuya-manager plugin
|
|
150
159
|
|
|
151
160
|
Installed next to rustuya-manager, as a pip package (`pip install rustuya-local` in the manager's environment) or as
|
|
152
|
-
the drop-in zip each release carries (`rustuya_local-<version>-dropin.zip`, tuya2ildevice binding, native library and
|
|
161
|
+
the drop-in zip each release carries (`rustuya_local-<version>-dropin.zip`, tuya2ildevice binding, native library and builtin rules vendored inside, built by
|
|
153
162
|
`scripts/build_dropin.py`; unpack it into the manager's plugin directory, or install it from the manager's plugin
|
|
154
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
|
|
155
164
|
list (followed). Its data dir (`rustuya-local/` next to the manager's plugins) holds `settings.json` (`il`,
|
|
@@ -40,7 +40,7 @@ source are cleared (other sources on the prefix are left alone), so IL consumers
|
|
|
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;
|
|
43
|
-
`host.rules` discovers and loads the bundled
|
|
43
|
+
`host.rules` discovers and loads the bundled builtin rules and user layers, passing decoded rules to Rust for composition.
|
|
44
44
|
The service preloads native code and rules in a worker thread before starting devices.
|
|
45
45
|
Timers, retained-message policy, bridge `get` requests, presence and settings persistence remain in this host.
|
|
46
46
|
MQTT and in-process transport tests exercise the same adapter and the bidirectional Rust driver.
|
|
@@ -76,12 +76,18 @@ config; integration: `<config>/rustuya_converters`; manager plugin: its data dir
|
|
|
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.
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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.
|
|
81
|
+
|
|
82
|
+
Online updates fetch the latest [builtin rules](https://github.com/3735943886/tuya2ildevice/tree/feat/rust-rule-engine/rules/builtin)
|
|
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. 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.
|
|
85
91
|
|
|
86
92
|
### Cover settings
|
|
87
93
|
|
|
@@ -102,20 +108,23 @@ device settings are enabled, unless an override selects another source. A settin
|
|
|
102
108
|
the setting is saved as the device's block in `zz_settings.json` in the converters directory and applied at once, so it
|
|
103
109
|
survives a restart and can be edited there too. Without a converters directory (a daemon with no
|
|
104
110
|
`custom_converters`) the switches are not offered; `"options": {"device_settings": false}` turns them off.
|
|
105
|
-
A block with the same `cover` keys in your own file or the
|
|
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.
|
|
106
112
|
Older `cover.infer_motion` and `converters.cover_motion` blocks are upgraded on loading to `cover.state_source`
|
|
107
113
|
(`inferred`, or `none` for `infer_motion: false`) and `cover.settle`; see [migration notes](docs/MIGRATING.md).
|
|
108
114
|
|
|
109
115
|
In Home Assistant the files can be edited from a sidebar panel: the integration's *Configure* → *Add the Rustuya
|
|
110
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
|
|
111
|
-
bridge's devices against the cloud list
|
|
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
|
|
112
118
|
turn off; sub-devices under their gateway; each device's bridge connection, live while the panel is open) with add /
|
|
113
|
-
update / remove.
|
|
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
|
|
114
123
|
fields as rustuya-manager. Devices absent from the cloud list appear under *Bridge only*; their removal is not
|
|
115
|
-
preselected in bulk sync. The panel lists
|
|
116
|
-
|
|
124
|
+
preselected in bulk sync. The panel lists, edits and deletes user converter files (drop JSON rule files on it to copy them in),
|
|
125
|
+
updates builtin rules on demand, and has the
|
|
117
126
|
*Options* (remote control of locks, alarms and garage doors; data points Home Assistant core would not classify; the
|
|
118
|
-
|
|
127
|
+
automatic builtin rule updates), and the *Settings* setup leaves at their defaults: the bridge topic root, the IL prefix and source, the device file, and
|
|
119
128
|
the embedded bridge's state file and log level. Saving restarts the integration; moving the IL prefix or source first
|
|
120
129
|
clears what the old one left on the broker. *Hide panel* in its toolbar turns it off again. The integration and the panel follow Home Assistant's
|
|
121
130
|
language: Korean, or English.
|
|
@@ -123,7 +132,7 @@ language: Korean, or English.
|
|
|
123
132
|
## rustuya-manager plugin
|
|
124
133
|
|
|
125
134
|
Installed next to rustuya-manager, as a pip package (`pip install rustuya-local` in the manager's environment) or as
|
|
126
|
-
the drop-in zip each release carries (`rustuya_local-<version>-dropin.zip`, tuya2ildevice binding, native library and
|
|
135
|
+
the drop-in zip each release carries (`rustuya_local-<version>-dropin.zip`, tuya2ildevice binding, native library and builtin rules vendored inside, built by
|
|
127
136
|
`scripts/build_dropin.py`; unpack it into the manager's plugin directory, or install it from the manager's plugin
|
|
128
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
|
|
129
138
|
list (followed). Its data dir (`rustuya-local/` next to the manager's plugins) holds `settings.json` (`il`,
|
|
@@ -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")
|
{rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/custom_components/rustuya/manager_session.py
RENAMED
|
@@ -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
|
-
|
|
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."""
|
|
@@ -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
|
-
"""
|
|
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
|
|
540
|
-
|
|
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.
|
|
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
|
{rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/custom_components/rustuya/strings.json
RENAMED
|
@@ -138,7 +138,7 @@
|
|
|
138
138
|
"wizard_failed": "Fetching from Tuya Cloud failed: {error}",
|
|
139
139
|
"no_devices": "There are no devices in the cloud list or on the bridge.",
|
|
140
140
|
"manager_busy": "Another Tuya Cloud fetch or device sync window is still open. Close it and try again.",
|
|
141
|
-
"panel_shown": "The Rustuya panel was added to the sidebar: [open it]({url}). Its Options hold remote control of locks, alarms and garage doors, unused data points and
|
|
141
|
+
"panel_shown": "The Rustuya panel was added to the sidebar: [open it]({url}). Its Options hold remote control of locks, alarms and garage doors, unused data points and automatic builtin rule updates.",
|
|
142
142
|
"panel_open": "[Open the Rustuya panel]({url})."
|
|
143
143
|
},
|
|
144
144
|
"error": {
|
{rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/custom_components/rustuya/translations/en.json
RENAMED
|
@@ -138,7 +138,7 @@
|
|
|
138
138
|
"wizard_failed": "Fetching from Tuya Cloud failed: {error}",
|
|
139
139
|
"no_devices": "There are no devices in the cloud list or on the bridge.",
|
|
140
140
|
"manager_busy": "Another Tuya Cloud fetch or device sync window is still open. Close it and try again.",
|
|
141
|
-
"panel_shown": "The Rustuya panel was added to the sidebar: [open it]({url}). Its Options hold remote control of locks, alarms and garage doors, unused data points and
|
|
141
|
+
"panel_shown": "The Rustuya panel was added to the sidebar: [open it]({url}). Its Options hold remote control of locks, alarms and garage doors, unused data points and automatic builtin rule updates.",
|
|
142
142
|
"panel_open": "[Open the Rustuya panel]({url})."
|
|
143
143
|
},
|
|
144
144
|
"error": {
|
{rustuya_local-0.1.0.dev3 → rustuya_local-0.1.0.dev5}/custom_components/rustuya/translations/ko.json
RENAMED
|
@@ -138,7 +138,7 @@
|
|
|
138
138
|
"wizard_failed": "Tuya Cloud에서 가져오지 못했습니다: {error}",
|
|
139
139
|
"no_devices": "클라우드 목록에도 브리지에도 기기가 없습니다.",
|
|
140
140
|
"manager_busy": "다른 Tuya Cloud 가져오기 또는 기기 동기화 창이 아직 열려 있습니다. 그 창을 닫고 다시 시도하세요.",
|
|
141
|
-
"panel_shown": "사이드바에 Rustuya 패널이 추가되었습니다: [열기]({url}). 패널의 Options에서 잠금장치·경보·차고문 원격 제어, 분류되지 않는 데이터 포인트,
|
|
141
|
+
"panel_shown": "사이드바에 Rustuya 패널이 추가되었습니다: [열기]({url}). 패널의 Options에서 잠금장치·경보·차고문 원격 제어, 분류되지 않는 데이터 포인트, Builtin 규칙 자동 업데이트를 설정합니다.",
|
|
142
142
|
"panel_open": "[Rustuya 패널 열기]({url})"
|
|
143
143
|
},
|
|
144
144
|
"error": {
|