rustuya-local 0.1.0.dev2__tar.gz → 0.1.0.dev3__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.dev3}/CHANGELOG.md +9 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/PKG-INFO +6 -5
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/README.md +4 -3
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/custom_components/rustuya/manifest.json +3 -3
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/docs/MIGRATING.md +2 -2
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/pyproject.toml +2 -2
- rustuya_local-0.1.0.dev3/release-notes.md +6 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/scripts/build_dropin.py +4 -1
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/src/rustuya_local/host/overrides.py +2 -2
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/src/rustuya_local/host/pack.py +4 -4
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/src/rustuya_local/host/rules.py +21 -8
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/src/rustuya_local/service.py +7 -2
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/tests/e2e/test_service.py +18 -7
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/tests/unit/test_dropin.py +1 -1
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/tests/unit/test_pack.py +1 -1
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/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.dev3}/.github/dependabot.yml +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/.github/workflows/_test.yml +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/.github/workflows/ci.yml +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/.github/workflows/hacs.yml +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/.github/workflows/hassfest.yml +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/.github/workflows/release.yml +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/.gitignore +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/LICENSE +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/custom_components/rustuya/__init__.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/custom_components/rustuya/brand/icon.png +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/custom_components/rustuya/brand/icon.svg +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/custom_components/rustuya/brand/icon@2x.png +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/custom_components/rustuya/brand/logo.png +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/custom_components/rustuya/brand/logo.svg +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/custom_components/rustuya/brand/logo@2x.png +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/custom_components/rustuya/bridge_supervisor.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/custom_components/rustuya/bridge_sync.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/custom_components/rustuya/broker.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/custom_components/rustuya/config_flow.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/custom_components/rustuya/const.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/custom_components/rustuya/manager_session.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/custom_components/rustuya/panel.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/custom_components/rustuya/services.yaml +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/custom_components/rustuya/strings.json +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/custom_components/rustuya/translations/en.json +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/custom_components/rustuya/translations/ko.json +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/custom_components/rustuya/www/rustuya-panel.js +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/docs/REDESIGN.md +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/docs/ROADMAP.md +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/docs/STATUS.md +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/hacs.json +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/scripts/release_notes.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/src/rustuya_local/__init__.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/src/rustuya_local/__main__.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/src/rustuya_local/async_tools.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/src/rustuya_local/bridge_client.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/src/rustuya_local/cli.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/src/rustuya_local/config.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/src/rustuya_local/files.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/src/rustuya_local/host/__init__.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/src/rustuya_local/host/devices.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/src/rustuya_local/host/hub.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/src/rustuya_local/host/il.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/src/rustuya_local/host/memory.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/src/rustuya_local/host/mqtt.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/src/rustuya_local/host/runner.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/src/rustuya_local/host/transport.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/src/rustuya_local/manager_plugin/__init__.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/src/rustuya_local/manager_plugin/api.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/src/rustuya_local/manager_plugin/static/index.js +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/src/rustuya_local/validation.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/tests/e2e/conftest.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/tests/e2e/devices.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/tests/e2e/test_bridge_registration.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/tests/e2e/test_chain_in_process.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/tests/e2e/test_chain_mqtt.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/tests/e2e/test_daemon.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/tests/e2e/test_full_stack.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/tests/e2e/test_full_stack_custom_templates.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/tests/e2e/test_full_stack_devices.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/tests/e2e/test_manager_plugin.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/tests/e2e/test_purge.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/tests/ha/conftest.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/tests/ha/fake_manager.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/tests/ha/test_bridge_probe.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/tests/ha/test_config_flow.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/tests/ha/test_init.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/tests/ha/test_manager_session.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/tests/ha/test_migrate.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/tests/ha/test_panel.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/tests/unit/test_config.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/tests/unit/test_cover_settings.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/tests/unit/test_files.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/tests/unit/test_hazardous.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/tests/unit/test_host.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/tests/unit/test_hub.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/tests/unit/test_il_vectors.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/tests/unit/test_link_watcher.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/tests/unit/test_panel_js.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/tests/unit/test_plugin_api.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/tests/unit/test_release_notes.py +0 -0
- {rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/tests/unit/tuya_samples.py +0 -0
|
@@ -8,6 +8,15 @@ stable release.
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [0.1.0.dev3] — 2026-10-11
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
- Require tuya2ildevice 0.4.0.dev4 in the package and Home Assistant integration.
|
|
15
|
+
- Load bundled official rules from `rules/builtin/`, exclude the manifest from rule layers, and apply user files after official rules regardless of filename.
|
|
16
|
+
- Fetch the complete official pack into `rules/builtin/` inside the overrides directory and watch it for live reloads.
|
|
17
|
+
- Retire unedited files owned by the old flat override pack while preserving user edits.
|
|
18
|
+
- Include the official rule layers and manifest in the manager drop-in archive.
|
|
19
|
+
|
|
11
20
|
## [0.1.0.dev2] — 2026-10-11
|
|
12
21
|
|
|
13
22
|
### 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.dev3
|
|
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
|
|
@@ -103,9 +103,10 @@ config; integration: `<config>/rustuya_converters`; manager plugin: its data dir
|
|
|
103
103
|
- Converters are JSON programs under `converters.declarative`; Python code plugins are not loaded.
|
|
104
104
|
|
|
105
105
|
See tuya2ildevice's README ("User overrides") for the format. A curated set ships in tuya2ildevice, and fixes published
|
|
106
|
-
between its releases (its [override pack](https://github.com/3735943886/tuya2ildevice/tree/master/rules/
|
|
107
|
-
into the directory at start and daily
|
|
108
|
-
|
|
106
|
+
between its releases (its [override pack](https://github.com/3735943886/tuya2ildevice/tree/master/rules/builtin)) are copied
|
|
107
|
+
into `rules/builtin/` inside the overrides directory at start and daily. These include `00-default.json`; your own
|
|
108
|
+
files are applied after all official rules. The bundled defaults update with tuya2ildevice, even when pack sync is off.
|
|
109
|
+
Turn online updates off with `"pack": false` (daemon), the Rustuya panel's *Options* (integration), or the plugin tab. The pack only replaces or removes the files it
|
|
109
110
|
put there, and leaves a file alone once you edit it.
|
|
110
111
|
|
|
111
112
|
### Cover settings
|
|
@@ -77,9 +77,10 @@ config; integration: `<config>/rustuya_converters`; manager plugin: its data dir
|
|
|
77
77
|
- Converters are JSON programs under `converters.declarative`; Python code plugins are not loaded.
|
|
78
78
|
|
|
79
79
|
See tuya2ildevice's README ("User overrides") for the format. A curated set ships in tuya2ildevice, and fixes published
|
|
80
|
-
between its releases (its [override pack](https://github.com/3735943886/tuya2ildevice/tree/master/rules/
|
|
81
|
-
into the directory at start and daily
|
|
82
|
-
|
|
80
|
+
between its releases (its [override pack](https://github.com/3735943886/tuya2ildevice/tree/master/rules/builtin)) are copied
|
|
81
|
+
into `rules/builtin/` inside the overrides directory at start and daily. These include `00-default.json`; your own
|
|
82
|
+
files are applied after all official rules. The bundled defaults update with tuya2ildevice, even when pack sync is off.
|
|
83
|
+
Turn online updates off with `"pack": false` (daemon), the Rustuya panel's *Options* (integration), or the plugin tab. The pack only replaces or removes the files it
|
|
83
84
|
put there, and leaves a file alone once you edit it.
|
|
84
85
|
|
|
85
86
|
### Cover settings
|
{rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/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.dev3"
|
|
17
17
|
],
|
|
18
18
|
"single_config_entry": true,
|
|
19
|
-
"version": "0.1.0.
|
|
19
|
+
"version": "0.1.0.dev3"
|
|
20
20
|
}
|
|
@@ -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.dev3"
|
|
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,6 @@
|
|
|
1
|
+
### Changed
|
|
2
|
+
- Require tuya2ildevice 0.4.0.dev4 in the package and Home Assistant integration.
|
|
3
|
+
- Load bundled official rules from `rules/builtin/`, exclude the manifest from rule layers, and apply user files after official rules regardless of filename.
|
|
4
|
+
- Fetch the complete official pack into `rules/builtin/` inside the overrides directory and watch it for live reloads.
|
|
5
|
+
- Retire unedited files owned by the old flat override pack while preserving user edits.
|
|
6
|
+
- Include the official rule layers and manifest in the manager drop-in archive.
|
|
@@ -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 `master` (`rules/
|
|
1
|
+
"""The override pack: fixes for non-standard devices published on tuya2ildevice's `master` (`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/master/rules/
|
|
41
|
+
BASE_URL = "https://raw.githubusercontent.com/3735943886/tuya2ildevice/master/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
|
|
@@ -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.dev3}/custom_components/rustuya/brand/icon.png
RENAMED
|
File without changes
|
{rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/custom_components/rustuya/brand/icon.svg
RENAMED
|
File without changes
|
{rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/custom_components/rustuya/brand/icon@2x.png
RENAMED
|
File without changes
|
{rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/custom_components/rustuya/brand/logo.png
RENAMED
|
File without changes
|
{rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/custom_components/rustuya/brand/logo.svg
RENAMED
|
File without changes
|
{rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/custom_components/rustuya/brand/logo@2x.png
RENAMED
|
File without changes
|
{rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/custom_components/rustuya/bridge_supervisor.py
RENAMED
|
File without changes
|
{rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/custom_components/rustuya/bridge_sync.py
RENAMED
|
File without changes
|
|
File without changes
|
{rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/custom_components/rustuya/config_flow.py
RENAMED
|
File without changes
|
|
File without changes
|
{rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/custom_components/rustuya/manager_session.py
RENAMED
|
File without changes
|
|
File without changes
|
{rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/custom_components/rustuya/services.yaml
RENAMED
|
File without changes
|
{rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/custom_components/rustuya/strings.json
RENAMED
|
File without changes
|
{rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/custom_components/rustuya/translations/en.json
RENAMED
|
File without changes
|
{rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/custom_components/rustuya/translations/ko.json
RENAMED
|
File without changes
|
{rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/custom_components/rustuya/www/rustuya-panel.js
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.dev3}/src/rustuya_local/manager_plugin/__init__.py
RENAMED
|
File without changes
|
{rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/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
|
|
File without changes
|
{rustuya_local-0.1.0.dev2 → rustuya_local-0.1.0.dev3}/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
|