rustuya-local 0.1.0.dev2__tar.gz → 0.1.0.dev4__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.dev2 → rustuya_local-0.1.0.dev4}/CHANGELOG.md +17 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/PKG-INFO +16 -13
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/README.md +14 -11
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/custom_components/rustuya/manifest.json +3 -3
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/custom_components/rustuya/strings.json +1 -1
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/custom_components/rustuya/translations/en.json +1 -1
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/custom_components/rustuya/translations/ko.json +1 -1
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/custom_components/rustuya/www/rustuya-panel.js +16 -16
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/docs/MIGRATING.md +2 -2
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/pyproject.toml +2 -2
- rustuya_local-0.1.0.dev4/release-notes.md +5 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/scripts/build_dropin.py +4 -1
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/src/rustuya_local/host/overrides.py +2 -2
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/src/rustuya_local/host/pack.py +4 -4
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/src/rustuya_local/host/rules.py +21 -8
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/src/rustuya_local/manager_plugin/static/index.js +3 -3
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/src/rustuya_local/service.py +7 -2
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/tests/e2e/test_service.py +18 -7
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/tests/unit/test_dropin.py +1 -1
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/tests/unit/test_pack.py +1 -1
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/tests/unit/test_rule_files.py +27 -1
- rustuya_local-0.1.0.dev2/release-notes.md +0 -11
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/.github/dependabot.yml +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/.github/workflows/_test.yml +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/.github/workflows/ci.yml +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/.github/workflows/hacs.yml +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/.github/workflows/hassfest.yml +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/.github/workflows/release.yml +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/.gitignore +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/LICENSE +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/custom_components/rustuya/__init__.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/custom_components/rustuya/brand/icon.png +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/custom_components/rustuya/brand/icon.svg +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/custom_components/rustuya/brand/icon@2x.png +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/custom_components/rustuya/brand/logo.png +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/custom_components/rustuya/brand/logo.svg +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/custom_components/rustuya/brand/logo@2x.png +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/custom_components/rustuya/bridge_supervisor.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/custom_components/rustuya/bridge_sync.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/custom_components/rustuya/broker.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/custom_components/rustuya/config_flow.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/custom_components/rustuya/const.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/custom_components/rustuya/manager_session.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/custom_components/rustuya/panel.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/custom_components/rustuya/services.yaml +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/docs/REDESIGN.md +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/docs/ROADMAP.md +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/docs/STATUS.md +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/hacs.json +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/scripts/release_notes.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/src/rustuya_local/__init__.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/src/rustuya_local/__main__.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/src/rustuya_local/async_tools.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/src/rustuya_local/bridge_client.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/src/rustuya_local/cli.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/src/rustuya_local/config.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/src/rustuya_local/files.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/src/rustuya_local/host/__init__.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/src/rustuya_local/host/devices.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/src/rustuya_local/host/hub.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/src/rustuya_local/host/il.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/src/rustuya_local/host/memory.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/src/rustuya_local/host/mqtt.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/src/rustuya_local/host/runner.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/src/rustuya_local/host/transport.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/src/rustuya_local/manager_plugin/__init__.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/src/rustuya_local/manager_plugin/api.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/src/rustuya_local/validation.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/tests/e2e/conftest.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/tests/e2e/devices.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/tests/e2e/test_bridge_registration.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/tests/e2e/test_chain_in_process.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/tests/e2e/test_chain_mqtt.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/tests/e2e/test_daemon.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/tests/e2e/test_full_stack.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/tests/e2e/test_full_stack_custom_templates.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/tests/e2e/test_full_stack_devices.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/tests/e2e/test_manager_plugin.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/tests/e2e/test_purge.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/tests/ha/conftest.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/tests/ha/fake_manager.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/tests/ha/test_bridge_probe.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/tests/ha/test_config_flow.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/tests/ha/test_init.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/tests/ha/test_manager_session.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/tests/ha/test_migrate.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/tests/ha/test_panel.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/tests/unit/test_config.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/tests/unit/test_cover_settings.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/tests/unit/test_files.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/tests/unit/test_hazardous.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/tests/unit/test_host.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/tests/unit/test_hub.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/tests/unit/test_il_vectors.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/tests/unit/test_link_watcher.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/tests/unit/test_panel_js.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/tests/unit/test_plugin_api.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/tests/unit/test_release_notes.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/tests/unit/tuya_samples.py +0 -0
|
@@ -8,6 +8,23 @@ stable release.
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [0.1.0.dev4] — 2026-10-11
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
- Fetch dev4 builtin rule updates from its development branch; master does not yet contain the new rules directory.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
- 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.
|
|
18
|
+
|
|
19
|
+
## [0.1.0.dev3] — 2026-10-11
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
- Require tuya2ildevice 0.4.0.dev4 in the package and Home Assistant integration.
|
|
23
|
+
- Load bundled official rules from `rules/builtin/`, exclude the manifest from rule layers, and apply user files after official rules regardless of filename.
|
|
24
|
+
- Fetch the complete official pack into `rules/builtin/` inside the overrides directory and watch it for live reloads.
|
|
25
|
+
- Retire unedited files owned by the old flat override pack while preserving user edits.
|
|
26
|
+
- Include the official rule layers and manifest in the manager drop-in archive.
|
|
27
|
+
|
|
11
28
|
## [0.1.0.dev2] — 2026-10-11
|
|
12
29
|
|
|
13
30
|
### Fixed
|
|
@@ -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.dev4
|
|
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
|
|
@@ -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.
|
|
13
|
+
Requires-Dist: tuya2ildevice>=0.4.0.dev4
|
|
14
14
|
Provides-Extra: fullstack
|
|
15
15
|
Requires-Dist: tuyamock; extra == 'fullstack'
|
|
16
16
|
Provides-Extra: manager
|
|
@@ -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,11 +102,14 @@ 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
|
-
|
|
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. 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
113
|
|
|
111
114
|
### Cover settings
|
|
112
115
|
|
|
@@ -127,7 +130,7 @@ device settings are enabled, unless an override selects another source. A settin
|
|
|
127
130
|
the setting is saved as the device's block in `zz_settings.json` in the converters directory and applied at once, so it
|
|
128
131
|
survives a restart and can be edited there too. Without a converters directory (a daemon with no
|
|
129
132
|
`custom_converters`) the switches are not offered; `"options": {"device_settings": false}` turns them off.
|
|
130
|
-
A block with the same `cover` keys in your own file or the
|
|
133
|
+
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.
|
|
131
134
|
Older `cover.infer_motion` and `converters.cover_motion` blocks are upgraded on loading to `cover.state_source`
|
|
132
135
|
(`inferred`, or `none` for `infer_motion: false`) and `cover.settle`; see [migration notes](docs/MIGRATING.md).
|
|
133
136
|
|
|
@@ -137,10 +140,10 @@ bridge's devices against the cloud list as rustuya-manager does (missing, bridge
|
|
|
137
140
|
turn off; sub-devices under their gateway; each device's bridge connection, live while the panel is open) with add /
|
|
138
141
|
update / remove. *Register manually* adds Wi-Fi devices or sub-devices without a cloud login, using the same
|
|
139
142
|
fields as rustuya-manager. Devices absent from the cloud list appear under *Bridge only*; their removal is not
|
|
140
|
-
preselected in bulk sync. The panel lists
|
|
141
|
-
|
|
143
|
+
preselected in bulk sync. The panel lists, edits and deletes user converter files (drop JSON rule files on it to copy them in),
|
|
144
|
+
updates builtin rules on demand, and has the
|
|
142
145
|
*Options* (remote control of locks, alarms and garage doors; data points Home Assistant core would not classify; the
|
|
143
|
-
|
|
146
|
+
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
|
|
144
147
|
the embedded bridge's state file and log level. Saving restarts the integration; moving the IL prefix or source first
|
|
145
148
|
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
|
|
146
149
|
language: Korean, or English.
|
|
@@ -148,7 +151,7 @@ language: Korean, or English.
|
|
|
148
151
|
## rustuya-manager plugin
|
|
149
152
|
|
|
150
153
|
Installed next to rustuya-manager, as a pip package (`pip install rustuya-local` in the manager's environment) or as
|
|
151
|
-
the drop-in zip each release carries (`rustuya_local-<version>-dropin.zip`, tuya2ildevice binding, native library and
|
|
154
|
+
the drop-in zip each release carries (`rustuya_local-<version>-dropin.zip`, tuya2ildevice binding, native library and builtin rules vendored inside, built by
|
|
152
155
|
`scripts/build_dropin.py`; unpack it into the manager's plugin directory, or install it from the manager's plugin
|
|
153
156
|
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
|
|
154
157
|
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,11 +76,14 @@ 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
|
-
|
|
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. 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
87
|
|
|
85
88
|
### Cover settings
|
|
86
89
|
|
|
@@ -101,7 +104,7 @@ device settings are enabled, unless an override selects another source. A settin
|
|
|
101
104
|
the setting is saved as the device's block in `zz_settings.json` in the converters directory and applied at once, so it
|
|
102
105
|
survives a restart and can be edited there too. Without a converters directory (a daemon with no
|
|
103
106
|
`custom_converters`) the switches are not offered; `"options": {"device_settings": false}` turns them off.
|
|
104
|
-
A block with the same `cover` keys in your own file or the
|
|
107
|
+
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.
|
|
105
108
|
Older `cover.infer_motion` and `converters.cover_motion` blocks are upgraded on loading to `cover.state_source`
|
|
106
109
|
(`inferred`, or `none` for `infer_motion: false`) and `cover.settle`; see [migration notes](docs/MIGRATING.md).
|
|
107
110
|
|
|
@@ -111,10 +114,10 @@ bridge's devices against the cloud list as rustuya-manager does (missing, bridge
|
|
|
111
114
|
turn off; sub-devices under their gateway; each device's bridge connection, live while the panel is open) with add /
|
|
112
115
|
update / remove. *Register manually* adds Wi-Fi devices or sub-devices without a cloud login, using the same
|
|
113
116
|
fields as rustuya-manager. Devices absent from the cloud list appear under *Bridge only*; their removal is not
|
|
114
|
-
preselected in bulk sync. The panel lists
|
|
115
|
-
|
|
117
|
+
preselected in bulk sync. The panel lists, edits and deletes user converter files (drop JSON rule files on it to copy them in),
|
|
118
|
+
updates builtin rules on demand, and has the
|
|
116
119
|
*Options* (remote control of locks, alarms and garage doors; data points Home Assistant core would not classify; the
|
|
117
|
-
|
|
120
|
+
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
|
|
118
121
|
the embedded bridge's state file and log level. Saving restarts the integration; moving the IL prefix or source first
|
|
119
122
|
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
|
|
120
123
|
language: Korean, or English.
|
|
@@ -122,7 +125,7 @@ language: Korean, or English.
|
|
|
122
125
|
## rustuya-manager plugin
|
|
123
126
|
|
|
124
127
|
Installed next to rustuya-manager, as a pip package (`pip install rustuya-local` in the manager's environment) or as
|
|
125
|
-
the drop-in zip each release carries (`rustuya_local-<version>-dropin.zip`, tuya2ildevice binding, native library and
|
|
128
|
+
the drop-in zip each release carries (`rustuya_local-<version>-dropin.zip`, tuya2ildevice binding, native library and builtin rules vendored inside, built by
|
|
126
129
|
`scripts/build_dropin.py`; unpack it into the manager's plugin directory, or install it from the manager's plugin
|
|
127
130
|
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
|
|
128
131
|
list (followed). Its data dir (`rustuya-local/` next to the manager's plugins) holds `settings.json` (`il`,
|
{rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/custom_components/rustuya/manifest.json
RENAMED
|
@@ -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.
|
|
12
|
+
"tuya2ildevice>=0.4.0.dev4",
|
|
13
13
|
"paho-mqtt>=2.0",
|
|
14
14
|
"rustuya-manager>=0.2.2",
|
|
15
15
|
"pyrustuyabridge>=0.4",
|
|
16
|
-
"rustuya-local>=0.1.0.
|
|
16
|
+
"rustuya-local>=0.1.0.dev4"
|
|
17
17
|
],
|
|
18
18
|
"single_config_entry": true,
|
|
19
|
-
"version": "0.1.0.
|
|
19
|
+
"version": "0.1.0.dev4"
|
|
20
20
|
}
|
{rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/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.dev2 → rustuya_local-0.1.0.dev4}/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.dev2 → rustuya_local-0.1.0.dev4}/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": {
|
{rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/custom_components/rustuya/www/rustuya-panel.js
RENAMED
|
@@ -183,17 +183,17 @@ const I18N = {
|
|
|
183
183
|
opt_allow_hazardous_hint: "Off: their state is shown, but IL and rustuya.send_command refuse writes to them.",
|
|
184
184
|
opt_expose_unused: "Expose data points Home Assistant core would not classify",
|
|
185
185
|
opt_expose_unused_hint: "Every data point the cloud schema lists, not only the ones a standard entity uses.",
|
|
186
|
-
opt_pack: "
|
|
187
|
-
opt_pack_hint: "
|
|
186
|
+
opt_pack: "Automatically update builtin rules",
|
|
187
|
+
opt_pack_hint: "Check for official rule updates at start and daily.",
|
|
188
188
|
save_restart: "Save and restart", options_title: "Options", options_intro: "Saving restarts the integration.",
|
|
189
189
|
options_unreadable: "Cannot read the options: {error}", nothing_changed: "Nothing changed",
|
|
190
190
|
saving_restarting: "Saving; Rustuya is restarting…", not_saved: "Not saved: {error}",
|
|
191
191
|
settings_title: "Settings",
|
|
192
192
|
settings_intro: "Set up with their defaults. Saving restarts the integration; moving the IL prefix or source clears what the old one left on the broker, so IL consumers drop those devices and see them again under the new one.",
|
|
193
193
|
settings_unreadable: "Cannot read the settings: {error}", confirm_settings: "Save {names} and restart Rustuya?",
|
|
194
|
-
sync_now: "
|
|
195
|
-
hide: "Hide panel", pack_title: "
|
|
196
|
-
pack_intro: "
|
|
194
|
+
sync_now: "Update builtin rules", hide_title: "Remove this panel from the sidebar; the integration's Configure adds it back",
|
|
195
|
+
hide: "Hide panel", pack_title: "Builtin rules",
|
|
196
|
+
pack_intro: "Official rules included with tuya2ildevice. Fetch the latest rules without updating the integration.",
|
|
197
197
|
converters_title: "Custom converters",
|
|
198
198
|
converters_intro: "Rule files (*.json: override blocks and declarative converters) in rustuya_converters/. Saved files apply within seconds. See tuya2ildevice's README, \"User overrides\". Drop files here to copy them in.",
|
|
199
199
|
new_file: "New file", save: "Save", delete: "Delete",
|
|
@@ -204,15 +204,15 @@ const I18N = {
|
|
|
204
204
|
confirm_hide_unsaved: "Unsaved changes in {names} will be lost. Remove the Rustuya panel from the sidebar anyway?",
|
|
205
205
|
and: " and ", not_hidden: "Not hidden: {error}", files_unreadable: "Cannot list the files: {error}",
|
|
206
206
|
no_files: "No files yet. Built-in fixes apply without any.",
|
|
207
|
-
pack_syncing: "
|
|
207
|
+
pack_syncing: "Updating…", pack_off: "Online updates are off. Using the installed rules.", pack_never: "Using bundled rules; no online update yet.",
|
|
208
208
|
pack_added: "added", pack_updated: "updated", pack_removed: "removed", pack_kept: "kept",
|
|
209
|
-
pack_failed: "; failed: {list}", pack_error: "
|
|
209
|
+
pack_failed: "; failed: {list}", pack_error: "Update failed ({error}), {when}", pack_synced: "Updated {when}",
|
|
210
210
|
origin_note_pack: "From the override pack: the next sync updates it. Saving an edit makes it yours.",
|
|
211
211
|
origin_note_pack_edited: "An edited pack file: the pack no longer updates it. Delete it to get the pack's copy back at the next sync.",
|
|
212
212
|
name_first: "Name the file first (*.json)",
|
|
213
213
|
confirm_edit_pack: "{name} comes from the override pack. Once edited it is yours: the pack no longer updates or removes it.",
|
|
214
214
|
saved: "Saved {name}", confirm_delete: "Delete {name}?", deleted: "Deleted {name}", not_deleted: "Not deleted: {error}",
|
|
215
|
-
pack_is_off: "
|
|
215
|
+
pack_is_off: "Builtin rule updates are off", cannot_sync: "Cannot update: {error}", pack_changed: "Builtin rules updated",
|
|
216
216
|
},
|
|
217
217
|
ko: {
|
|
218
218
|
origin_pack: "팩", origin_pack_edited: "팩, 수정됨",
|
|
@@ -252,17 +252,17 @@ const I18N = {
|
|
|
252
252
|
opt_allow_hazardous_hint: "끄면 상태는 보이지만 IL과 rustuya.send_command가 쓰기를 거부합니다.",
|
|
253
253
|
opt_expose_unused: "Home Assistant core가 분류하지 않는 데이터 포인트도 노출",
|
|
254
254
|
opt_expose_unused_hint: "표준 엔티티가 쓰는 것만이 아니라 클라우드 스키마의 모든 데이터 포인트.",
|
|
255
|
-
opt_pack: "
|
|
256
|
-
opt_pack_hint: "
|
|
255
|
+
opt_pack: "Builtin 규칙 자동 업데이트",
|
|
256
|
+
opt_pack_hint: "시작할 때와 매일 공식 규칙 업데이트를 확인합니다.",
|
|
257
257
|
save_restart: "저장하고 재시작", options_title: "옵션", options_intro: "저장하면 통합구성요소가 재시작됩니다.",
|
|
258
258
|
options_unreadable: "옵션을 읽을 수 없습니다: {error}", nothing_changed: "바뀐 것이 없습니다",
|
|
259
259
|
saving_restarting: "저장하는 중. Rustuya를 재시작합니다…", not_saved: "저장하지 못했습니다: {error}",
|
|
260
260
|
settings_title: "설정",
|
|
261
261
|
settings_intro: "설정 과정에서 기본값으로 정해진 값들입니다. 저장하면 통합구성요소가 재시작됩니다. IL prefix나 source를 옮기면 예전 값으로 브로커에 남은 것을 지워서, IL 소비자가 그 기기를 지우고 새 값으로 다시 보게 됩니다.",
|
|
262
262
|
settings_unreadable: "설정을 읽을 수 없습니다: {error}", confirm_settings: "{names}을(를) 저장하고 Rustuya를 재시작할까요?",
|
|
263
|
-
sync_now: "
|
|
264
|
-
hide: "패널 숨기기", pack_title: "
|
|
265
|
-
pack_intro: "
|
|
263
|
+
sync_now: "Builtin 규칙 업데이트", hide_title: "사이드바에서 이 패널을 없앱니다. 통합구성요소의 구성(Configure)에서 다시 추가할 수 있습니다",
|
|
264
|
+
hide: "패널 숨기기", pack_title: "Builtin 규칙",
|
|
265
|
+
pack_intro: "tuya2ildevice에 포함된 공식 규칙입니다. 통합구성요소 업데이트 없이 최신 규칙을 가져옵니다.",
|
|
266
266
|
converters_title: "커스텀 컨버터",
|
|
267
267
|
converters_intro: "rustuya_converters/ 안의 규칙 파일(*.json: 오버라이드 블록과 선언형 컨버터). 저장한 파일은 몇 초 안에 적용됩니다. tuya2ildevice README의 \"User overrides\"를 참고하세요. 여기에 파일을 끌어다 놓으면 복사됩니다.",
|
|
268
268
|
new_file: "새 파일", save: "저장", delete: "삭제",
|
|
@@ -273,15 +273,15 @@ const I18N = {
|
|
|
273
273
|
confirm_hide_unsaved: "{names}의 저장하지 않은 변경이 사라집니다. 그래도 사이드바에서 Rustuya 패널을 없앨까요?",
|
|
274
274
|
and: ", ", not_hidden: "숨기지 못했습니다: {error}", files_unreadable: "파일 목록을 읽을 수 없습니다: {error}",
|
|
275
275
|
no_files: "아직 파일이 없습니다. 기본 수정은 파일 없이도 적용됩니다.",
|
|
276
|
-
pack_syncing: "
|
|
276
|
+
pack_syncing: "업데이트하는 중…", pack_off: "온라인 업데이트가 꺼져 있습니다. 설치된 규칙을 사용합니다.", pack_never: "패키지에 포함된 규칙 사용 중; 아직 온라인 업데이트하지 않았습니다.",
|
|
277
277
|
pack_added: "추가", pack_updated: "갱신", pack_removed: "삭제", pack_kept: "유지",
|
|
278
|
-
pack_failed: "; 실패: {list}", pack_error: "
|
|
278
|
+
pack_failed: "; 실패: {list}", pack_error: "업데이트 실패 ({error}), {when}", pack_synced: "{when}에 업데이트함",
|
|
279
279
|
origin_note_pack: "오버라이드 팩의 파일입니다. 다음 동기화 때 갱신됩니다. 수정해서 저장하면 내 파일이 됩니다.",
|
|
280
280
|
origin_note_pack_edited: "수정한 팩 파일입니다. 팩이 더 이상 갱신하지 않습니다. 지우면 다음 동기화 때 팩의 파일이 다시 들어옵니다.",
|
|
281
281
|
name_first: "먼저 파일 이름을 정하세요 (*.json)",
|
|
282
282
|
confirm_edit_pack: "{name}은(는) 오버라이드 팩의 파일입니다. 수정하면 내 파일이 되어 팩이 더 이상 갱신하거나 지우지 않습니다.",
|
|
283
283
|
saved: "{name} 저장함", confirm_delete: "{name}을(를) 삭제할까요?", deleted: "{name} 삭제함", not_deleted: "삭제하지 못했습니다: {error}",
|
|
284
|
-
pack_is_off: "
|
|
284
|
+
pack_is_off: "Builtin 규칙 업데이트가 꺼져 있습니다", cannot_sync: "업데이트할 수 없습니다: {error}", pack_changed: "Builtin 규칙이 업데이트되었습니다",
|
|
285
285
|
},
|
|
286
286
|
};
|
|
287
287
|
|
|
@@ -69,8 +69,8 @@ rustuya-manager plugin. It is retired. Its replacement is two pieces:
|
|
|
69
69
|
- `converters.cover_motion` and a cover's `infer_motion` are read as the cover's `state_source` (`inferred`, or
|
|
70
70
|
`none` for `infer_motion: false`), and a `cover_motion` `settle` as the cover's `settle`; its other keys are
|
|
71
71
|
dropped with a warning. `zz_settings.json` is rewritten in the new form the next time a setting is saved.
|
|
72
|
-
- Leave the old pack (`00_default.json`, `00_curtain.py`, `.rustuya_pack.json`) out.
|
|
73
|
-
files
|
|
72
|
+
- Leave the old pack (`00_default.json`, `00_curtain.py`, `.rustuya_pack.json`) out. Official rules ship with tuya2ildevice; online updates arrive under
|
|
73
|
+
`rules/builtin/` automatically. User files are applied after all official rules.
|
|
74
74
|
|
|
75
75
|
Changes to the directory are picked up while running, no restart needed.
|
|
76
76
|
|
|
@@ -4,12 +4,12 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "rustuya-local"
|
|
7
|
-
version = "0.1.0.
|
|
7
|
+
version = "0.1.0.dev4"
|
|
8
8
|
description = "Local Tuya control: rustuya-bridge -> tuya2ildevice -> IL (ildevice). HA-independent core."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.13"
|
|
11
11
|
license = "MIT"
|
|
12
|
-
dependencies = ["tuya2ildevice>=0.4.0.
|
|
12
|
+
dependencies = ["tuya2ildevice>=0.4.0.dev4", "pyrustuyabridge>=0.4", "paho-mqtt>=2.0"]
|
|
13
13
|
# rustuya-manager (custom_components/rustuya's config flow) is optional at the package level (the HA manifest
|
|
14
14
|
# pulls it in); declared here too so the venv used for this repo's tests has it. pyrustuyabridge is a base dependency
|
|
15
15
|
# now (not just for the embedded bridge): rustuya-local's own bridge_client uses it to interpret rustuya-bridge's
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
### Fixed
|
|
2
|
+
- Fetch dev4 builtin rule updates from its development branch; master does not yet contain the new rules directory.
|
|
3
|
+
|
|
4
|
+
### Changed
|
|
5
|
+
- 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.
|
|
@@ -58,7 +58,10 @@ def _members() -> list[tuple[str, Path]]:
|
|
|
58
58
|
members = dict(out)
|
|
59
59
|
members.setdefault(f"{prefix}/{native.name}", native)
|
|
60
60
|
for layer in rule_files(rule_paths()[:1]): # the rule layers a wheel bundles, nothing else
|
|
61
|
-
members.setdefault(f"{prefix}/rules/{layer.name}", layer)
|
|
61
|
+
members.setdefault(f"{prefix}/rules/builtin/{layer.name}", layer)
|
|
62
|
+
manifest = rule_paths()[0] / 'manifest.json'
|
|
63
|
+
if manifest.is_file():
|
|
64
|
+
members.setdefault(f"{prefix}/rules/builtin/manifest.json", manifest)
|
|
62
65
|
out = list(members.items())
|
|
63
66
|
return sorted(out)
|
|
64
67
|
|
|
@@ -95,12 +95,12 @@ class OverrideWatcher:
|
|
|
95
95
|
|
|
96
96
|
def _stat(self) -> Any:
|
|
97
97
|
stamp = []
|
|
98
|
-
for p in rule_files([self.path]):
|
|
98
|
+
for p in rule_files([self.path, self.path / 'rules' / 'builtin']):
|
|
99
99
|
try:
|
|
100
100
|
st = p.stat()
|
|
101
101
|
except OSError:
|
|
102
102
|
continue
|
|
103
|
-
stamp.append((p
|
|
103
|
+
stamp.append((str(p), st.st_mtime_ns, st.st_size))
|
|
104
104
|
return tuple(stamp)
|
|
105
105
|
|
|
106
106
|
def check(self) -> bool:
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
"""The override pack: fixes for non-standard devices published on tuya2ildevice's `
|
|
1
|
+
"""The override pack: fixes for non-standard devices published on tuya2ildevice's `feat/rust-rule-engine` branch (`rules/builtin/`) and copied into
|
|
2
2
|
the host's rule directory, so a fix reaches users without a release. `OverrideWatcher` then loads the copied files
|
|
3
3
|
like the user's own. The files are rule layers (JSON); fetching them is the host's, the engine only merges them.
|
|
4
4
|
|
|
5
|
-
`
|
|
5
|
+
`builtin/manifest.json` lists the files with their SHA-256 (built by `scripts/build_pack.py`):
|
|
6
6
|
|
|
7
7
|
{"version": 1, "files": [{"name": "00_pack_abc.json", "sha256": "...", "requires": 1, "until": 3}]}
|
|
8
8
|
|
|
@@ -38,7 +38,7 @@ from .overrides import SETTINGS_FILE
|
|
|
38
38
|
_LOGGER = logging.getLogger(__name__)
|
|
39
39
|
|
|
40
40
|
LEVEL = 4 # 2: `cover` settings; 3: `delta`; 4: rule-layer files (tuya2ildevice 0.4)
|
|
41
|
-
BASE_URL = "https://raw.githubusercontent.com/3735943886/tuya2ildevice/
|
|
41
|
+
BASE_URL = "https://raw.githubusercontent.com/3735943886/tuya2ildevice/feat/rust-rule-engine/rules/builtin/"
|
|
42
42
|
MANIFEST = "manifest.json"
|
|
43
43
|
LEDGER = ".tuya2ildevice_pack.json"
|
|
44
44
|
MAX_BYTES = 1024 * 1024
|
|
@@ -129,7 +129,7 @@ def _wanted(doc: dict[str, Any]) -> dict[str, str]:
|
|
|
129
129
|
continue
|
|
130
130
|
if entry.get("requires", 0) > LEVEL or entry.get("until", LEVEL + 1) <= LEVEL:
|
|
131
131
|
continue
|
|
132
|
-
if name
|
|
132
|
+
if name in (SETTINGS_FILE, MANIFEST): # the host's own settings: never the pack's
|
|
133
133
|
_LOGGER.warning("pack: %s is the host's settings file, not taken from the pack", name)
|
|
134
134
|
continue
|
|
135
135
|
out[name] = sha.lower()
|
|
@@ -64,8 +64,8 @@ def _upgrade_layer(data, where):
|
|
|
64
64
|
|
|
65
65
|
def rule_paths(paths=(), *, bundled_path=None):
|
|
66
66
|
package = Path(tuya2ildevice.__file__).resolve().parent
|
|
67
|
-
bundled = package / 'rules'
|
|
68
|
-
checkout = package.parents[1] / 'rules'
|
|
67
|
+
bundled = package / 'rules' / 'builtin'
|
|
68
|
+
checkout = package.parents[1] / 'rules' / 'builtin'
|
|
69
69
|
location = bundled_path or (bundled if bundled.is_dir() else checkout)
|
|
70
70
|
return [Path(location), *map(Path, paths)]
|
|
71
71
|
|
|
@@ -73,14 +73,16 @@ def rule_paths(paths=(), *, bundled_path=None):
|
|
|
73
73
|
def rule_files(locations):
|
|
74
74
|
files = []
|
|
75
75
|
for location in map(Path, locations):
|
|
76
|
-
if location.is_file():
|
|
76
|
+
if location.is_file() and location.name != 'manifest.json':
|
|
77
77
|
files.append(location)
|
|
78
78
|
elif location.is_dir():
|
|
79
|
-
files.extend(p for p in location.iterdir()
|
|
79
|
+
files.extend(p for p in location.iterdir()
|
|
80
|
+
if p.is_file() and p.suffix == '.json' and p.name[0] not in '._'
|
|
81
|
+
and p.name != 'manifest.json')
|
|
80
82
|
return sorted(files, key=lambda p: os.fsencode(p.name))
|
|
81
83
|
|
|
82
84
|
|
|
83
|
-
def merge_rule_files(locations, *, tolerant=False):
|
|
85
|
+
def merge_rule_files(locations, *, tolerant=False, base=None):
|
|
84
86
|
layers, warnings = [], []
|
|
85
87
|
files = rule_files(locations)
|
|
86
88
|
for path in files:
|
|
@@ -91,7 +93,7 @@ def merge_rule_files(locations, *, tolerant=False):
|
|
|
91
93
|
if not tolerant:
|
|
92
94
|
raise ValueError(f'{path}: {error}') from error
|
|
93
95
|
warnings.append(f'{path.name}: {error}')
|
|
94
|
-
result = merge_rules(layers, tolerant=tolerant)
|
|
96
|
+
result = merge_rules(layers, base=base, tolerant=tolerant)
|
|
95
97
|
names = {str(p): p.name for p in files}
|
|
96
98
|
for warning in result['warnings']:
|
|
97
99
|
source, _, message = warning.partition(': ')
|
|
@@ -99,12 +101,23 @@ def merge_rule_files(locations, *, tolerant=False):
|
|
|
99
101
|
return {**result, 'warnings': warnings}
|
|
100
102
|
|
|
101
103
|
|
|
104
|
+
def _load_rules(paths=(), *, bundled_path=None, tolerant=False):
|
|
105
|
+
locations = rule_paths(paths, bundled_path=bundled_path)
|
|
106
|
+
official = merge_rule_files(locations[:1])
|
|
107
|
+
# Downloaded official rules refine the bundled pack; user files always run last.
|
|
108
|
+
downloaded = [Path(p) / 'rules' / 'builtin' for p in paths if Path(p).is_dir()]
|
|
109
|
+
official = merge_rule_files(downloaded, base=official['rules'], tolerant=tolerant)
|
|
110
|
+
result = merge_rule_files(locations[1:], base=official['rules'], tolerant=tolerant)
|
|
111
|
+
result['warnings'] = official['warnings'] + result['warnings']
|
|
112
|
+
return result
|
|
113
|
+
|
|
114
|
+
|
|
102
115
|
def load_rules(paths=(), *, bundled_path=None):
|
|
103
|
-
return
|
|
116
|
+
return _load_rules(paths, bundled_path=bundled_path)['rules']
|
|
104
117
|
|
|
105
118
|
|
|
106
119
|
def load_rules_tolerant(paths=(), *, bundled_path=None):
|
|
107
|
-
return
|
|
120
|
+
return _load_rules(paths, bundled_path=bundled_path, tolerant=True)
|
|
108
121
|
|
|
109
122
|
|
|
110
123
|
@functools.cache
|
|
@@ -56,8 +56,8 @@ function paintStatus(box, data) {
|
|
|
56
56
|
const p = data.pack;
|
|
57
57
|
if (p) {
|
|
58
58
|
const when = new Date(p.at * 1000).toLocaleString();
|
|
59
|
-
const text = p.error ? `
|
|
60
|
-
: `
|
|
59
|
+
const text = p.error ? `Builtin rules: update failed (${p.error}), ${when}`
|
|
60
|
+
: `Builtin rules: updated ${when}` + PACK_CHANGES
|
|
61
61
|
.filter((k) => p[k] && p[k].length).map((k) => `; ${k} ${p[k].join(", ")}`).join("");
|
|
62
62
|
box.append(el("p", { class: `text-sm mt-2 ${p.error || (p.failed && p.failed.length) ? "text-amber-600" : "text-gray-500"}` }, text));
|
|
63
63
|
for (const f of p.failed || []) box.append(el("p", { class: "text-sm text-amber-600" }, f));
|
|
@@ -70,7 +70,7 @@ const OPTIONS = [
|
|
|
70
70
|
["use_quirks", "Apply the built-in fixes for known non-standard devices"],
|
|
71
71
|
["expose_unused", "Show dps no entity uses, as plain properties"],
|
|
72
72
|
["allow_hazardous", "Allow controlling hazardous covers (garage doors, gates)"],
|
|
73
|
-
["pack", "
|
|
73
|
+
["pack", "Automatically update builtin rules at start and daily"],
|
|
74
74
|
];
|
|
75
75
|
|
|
76
76
|
async function settingsBox(ctx) {
|
|
@@ -16,7 +16,7 @@ integration, the rustuya-manager plugin or a daemon) serves them, since every de
|
|
|
16
16
|
`online` that nobody answers for (a Last Will lost with the broker, or a producer on tuya2ildevice < 0.3.5) does not
|
|
17
17
|
stop the start.
|
|
18
18
|
|
|
19
|
-
With `pack` on, the override pack (tuya2ildevice's `rules/
|
|
19
|
+
With `pack` on, the override pack (tuya2ildevice's `rules/builtin/`) is copied into the overrides directory in the background at
|
|
20
20
|
start and every `pack_interval` seconds; the directory watcher loads what it brings.
|
|
21
21
|
|
|
22
22
|
A device's settings (a cover's direction toggles, tuya2ildevice's `device_settings`) are IL properties like any other;
|
|
@@ -249,7 +249,12 @@ class Service:
|
|
|
249
249
|
while True:
|
|
250
250
|
self._pack_now.clear()
|
|
251
251
|
try:
|
|
252
|
-
|
|
252
|
+
# Retire unedited files owned by the old flat pack, preserving user files.
|
|
253
|
+
retired = await run_in_thread(pack.sync, directory, manifest={'files': []})
|
|
254
|
+
result = await run_in_thread(pack.sync, directory / 'rules' / 'builtin', base_url=url)
|
|
255
|
+
result.removed.extend(retired.removed)
|
|
256
|
+
result.kept.extend(retired.kept)
|
|
257
|
+
result.failed.extend(retired.failed)
|
|
253
258
|
self.pack_status = {"at": time.time(), **result.as_dict()}
|
|
254
259
|
except pack.PackError as e:
|
|
255
260
|
_LOGGER.warning("override pack: %s", e)
|
|
@@ -201,7 +201,14 @@ async def test_the_override_pack_is_synced_in_the_background(tmp_path, monkeypat
|
|
|
201
201
|
monkeypatch.setattr(bc.BridgeClient, "start", lambda self, timeout=0.05: orig(self, timeout))
|
|
202
202
|
served, conv = tmp_path / "served", tmp_path / "conv"
|
|
203
203
|
served.mkdir()
|
|
204
|
-
|
|
204
|
+
conv.mkdir()
|
|
205
|
+
(conv / '00_pack_old.json').write_text('{}')
|
|
206
|
+
(conv / '00_pack_edited.json').write_text('{"overrides": {}}')
|
|
207
|
+
(conv / '.tuya2ildevice_pack.json').write_text(json.dumps({'files': {
|
|
208
|
+
'00_pack_old.json': hashlib.sha256(b'{}').hexdigest(),
|
|
209
|
+
'00_pack_edited.json': hashlib.sha256(b'{}').hexdigest(),
|
|
210
|
+
}}))
|
|
211
|
+
fix = json.dumps({"overrides": {"lamp1": {"device": {"label": "Packed"}}}})
|
|
205
212
|
(served / "00_pack_lamp.json").write_text(fix)
|
|
206
213
|
(served / "manifest.json").write_text(json.dumps({"version": 1, "files": [
|
|
207
214
|
{"name": "00_pack_lamp.json", "sha256": hashlib.sha256(fix.encode()).hexdigest()}]}))
|
|
@@ -212,8 +219,10 @@ async def test_the_override_pack_is_synced_in_the_background(tmp_path, monkeypat
|
|
|
212
219
|
if service.pack_status:
|
|
213
220
|
break
|
|
214
221
|
await asyncio.sleep(0.02)
|
|
215
|
-
assert service.pack_status["added"] == ["00_pack_lamp.json"] and (conv / "00_pack_lamp.json").is_file()
|
|
216
|
-
(conv /
|
|
222
|
+
assert service.pack_status["added"] == ["00_pack_lamp.json"] and (conv / "rules" / "builtin" / "00_pack_lamp.json").is_file()
|
|
223
|
+
assert not (conv / '00_pack_old.json').exists()
|
|
224
|
+
assert (conv / '00_pack_edited.json').read_text() == '{"overrides": {}}'
|
|
225
|
+
(conv / "rules" / "builtin" / "00_pack_lamp.json").unlink()
|
|
217
226
|
first, service.pack_status = service.pack_status, None
|
|
218
227
|
assert service.sync_pack_now() # not a day later: now
|
|
219
228
|
for _ in range(100):
|
|
@@ -230,7 +239,7 @@ async def test_the_override_pack_is_synced_in_the_background(tmp_path, monkeypat
|
|
|
230
239
|
if service.pack_status:
|
|
231
240
|
break
|
|
232
241
|
await asyncio.sleep(0.02)
|
|
233
|
-
assert "error" in service.pack_status and (conv / "00_pack_lamp.json").is_file()
|
|
242
|
+
assert "error" in service.pack_status and (conv / "rules" / "builtin" / "00_pack_lamp.json").is_file()
|
|
234
243
|
await service.stop()
|
|
235
244
|
|
|
236
245
|
|
|
@@ -333,9 +342,11 @@ async def test_stop_waits_for_an_override_pack_write_in_progress(tmp_path, monke
|
|
|
333
342
|
entered = asyncio.Event()
|
|
334
343
|
release = threading.Event()
|
|
335
344
|
loop = asyncio.get_running_loop()
|
|
336
|
-
path = tmp_path / 'conv' / '00_pack_test.json'
|
|
345
|
+
path = tmp_path / 'conv' / 'rules' / 'builtin' / '00_pack_test.json'
|
|
337
346
|
|
|
338
|
-
def blocked_sync(directory, *, base_url):
|
|
347
|
+
def blocked_sync(directory, *, base_url=pack.BASE_URL, manifest=None):
|
|
348
|
+
if manifest is not None:
|
|
349
|
+
return pack.SyncResult()
|
|
339
350
|
loop.call_soon_threadsafe(entered.set)
|
|
340
351
|
if not release.wait(5):
|
|
341
352
|
raise TimeoutError('test did not release pack sync')
|
|
@@ -346,7 +357,7 @@ async def test_stop_waits_for_an_override_pack_write_in_progress(tmp_path, monke
|
|
|
346
357
|
monkeypatch.setattr(pack, 'sync', blocked_sync)
|
|
347
358
|
t = Transports()
|
|
348
359
|
await t.bridge.publish('rustuya/bridge/config', '{}', 1, True)
|
|
349
|
-
service = _service(t, tmp_path, overrides_path=
|
|
360
|
+
service = _service(t, tmp_path, overrides_path=tmp_path / 'conv', pack=True)
|
|
350
361
|
await service.start()
|
|
351
362
|
stopping = None
|
|
352
363
|
try:
|
|
@@ -21,7 +21,7 @@ def test_one_package_with_tuya2ildevice_inside(tmp_path):
|
|
|
21
21
|
for needed in ("rustuya_local/__init__.py", "rustuya_local/manager_plugin/static/index.js",
|
|
22
22
|
"rustuya_local/_vendor/tuya2ildevice/__init__.py", "rustuya_local/_vendor/tuya2ildevice/native.py"):
|
|
23
23
|
assert needed in names
|
|
24
|
-
assert "rustuya_local/_vendor/tuya2ildevice/rules/00-default.json" in names
|
|
24
|
+
assert "rustuya_local/_vendor/tuya2ildevice/rules/builtin/00-default.json" in names
|
|
25
25
|
assert any(n.startswith("rustuya_local/_vendor/tuya2ildevice/") and n.endswith((".so", ".dll", ".dylib"))
|
|
26
26
|
for n in names)
|
|
27
27
|
assert not any("__pycache__" in n or n.startswith("rustuya_local/_vendor/paho") for n in names)
|
|
@@ -10,7 +10,7 @@ import pytest
|
|
|
10
10
|
from rustuya_local.host import load_overrides, pack
|
|
11
11
|
from rustuya_local.host.pack import LEDGER, PackError, read_ledger, sync
|
|
12
12
|
|
|
13
|
-
PUBLISHED = Path(__file__).resolve().parents[3] / "tuya2ildevice" / "rules" / "
|
|
13
|
+
PUBLISHED = Path(__file__).resolve().parents[3] / "tuya2ildevice" / "rules" / "builtin" # the sibling checkout
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
def sha(text: str) -> str:
|
|
@@ -53,7 +53,7 @@ def test_watcher_reloads_global_rules_and_restores_defaults_after_removal(tmp_pa
|
|
|
53
53
|
|
|
54
54
|
|
|
55
55
|
|
|
56
|
-
def
|
|
56
|
+
def test_host_applies_user_layers_after_bundled_rules(tmp_path):
|
|
57
57
|
write(tmp_path / '000_before_base.json', {'maps': {'switch_on': {'probe': True}}})
|
|
58
58
|
write(tmp_path / '10_after_base.json', {'overrides': {'p': {'device': {'label': 'user'}}}})
|
|
59
59
|
loaded = load_overrides(tmp_path)
|
|
@@ -168,3 +168,29 @@ def test_failed_adoption_releases_the_candidate_and_keeps_the_existing_drivers(m
|
|
|
168
168
|
assert candidate not in _pack_users and not unregister_rules(candidate)
|
|
169
169
|
finally:
|
|
170
170
|
hub.close()
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def test_official_manifest_is_not_a_rule_and_user_files_run_last(tmp_path):
|
|
174
|
+
builtin = tmp_path / 'rules' / 'builtin'
|
|
175
|
+
builtin.mkdir(parents=True)
|
|
176
|
+
write(builtin / 'manifest.json', {'version': 1, 'files': []})
|
|
177
|
+
write(builtin / '99_official.json', {'overrides': {'p': {'device': {'label': 'official'}}}})
|
|
178
|
+
write(tmp_path / '000_user.json', {'overrides': {'p': {'device': {'label': 'user'}}}})
|
|
179
|
+
loaded = load_overrides(tmp_path)
|
|
180
|
+
assert not loaded.warnings
|
|
181
|
+
assert TuyaDriver(DEVICE, rules=loaded.rules).descriptor['label'] == 'user'
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def test_watcher_follows_downloaded_official_rules(tmp_path):
|
|
185
|
+
builtin = tmp_path / 'rules' / 'builtin'
|
|
186
|
+
builtin.mkdir(parents=True)
|
|
187
|
+
watcher = OverrideWatcher(tmp_path, None)
|
|
188
|
+
assert watcher.check()
|
|
189
|
+
path = builtin / '10_official.json'
|
|
190
|
+
write(path, {'overrides': {'p': {'device': {'label': 'downloaded'}}}})
|
|
191
|
+
assert watcher.check()
|
|
192
|
+
assert watcher.load().overrides['p']['device']['label'] == 'downloaded'
|
|
193
|
+
assert not watcher.check()
|
|
194
|
+
path.unlink()
|
|
195
|
+
assert watcher.check()
|
|
196
|
+
assert watcher.load().overrides.get('p', {}).get('device', {}).get('label') != 'downloaded'
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
### Fixed
|
|
2
|
-
- Serialize device setting saves and override reloads so simultaneous commands preserve every device's settings,
|
|
3
|
-
including when a save is cancelled during file work.
|
|
4
|
-
- Release a Hub's native rule pack on service shutdown, failed construction, and failed rule replacement; retain
|
|
5
|
-
shared packs until their last owner releases them.
|
|
6
|
-
- Remove Runner reconnect callbacks and subscriptions on shutdown or failed startup, and await background cleanup.
|
|
7
|
-
- Retry transient override-loading failures and read watched device files off the event loop.
|
|
8
|
-
|
|
9
|
-
### Changed
|
|
10
|
-
- Share atomic file replacement across settings, converter files and the override pack, using unique temporary files.
|
|
11
|
-
- Refresh the roadmap and document the cover motion-source selector and position-source settings.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/custom_components/rustuya/brand/icon.png
RENAMED
|
File without changes
|
{rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/custom_components/rustuya/brand/icon.svg
RENAMED
|
File without changes
|
{rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/custom_components/rustuya/brand/icon@2x.png
RENAMED
|
File without changes
|
{rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/custom_components/rustuya/brand/logo.png
RENAMED
|
File without changes
|
{rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/custom_components/rustuya/brand/logo.svg
RENAMED
|
File without changes
|
{rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/custom_components/rustuya/brand/logo@2x.png
RENAMED
|
File without changes
|
{rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/custom_components/rustuya/bridge_supervisor.py
RENAMED
|
File without changes
|
{rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/custom_components/rustuya/bridge_sync.py
RENAMED
|
File without changes
|
|
File without changes
|
{rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/custom_components/rustuya/config_flow.py
RENAMED
|
File without changes
|
|
File without changes
|
{rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/custom_components/rustuya/manager_session.py
RENAMED
|
File without changes
|
|
File without changes
|
{rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/custom_components/rustuya/services.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/src/rustuya_local/manager_plugin/__init__.py
RENAMED
|
File without changes
|
{rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/src/rustuya_local/manager_plugin/api.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev4}/tests/e2e/test_full_stack_custom_templates.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|