rustuya-local 0.1.0.dev7__tar.gz → 0.1.0.dev8__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/CHANGELOG.md +17 -2
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/PKG-INFO +46 -13
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/README.md +45 -12
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/custom_components/rustuya/__init__.py +10 -4
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/custom_components/rustuya/manifest.json +2 -2
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/custom_components/rustuya/panel.py +56 -50
- rustuya_local-0.1.0.dev8/docs/ARCHITECTURE.md +78 -0
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/docs/MIGRATING.md +14 -7
- rustuya_local-0.1.0.dev8/docs/REDESIGN.md +385 -0
- rustuya_local-0.1.0.dev8/docs/REVIEW.md +108 -0
- rustuya_local-0.1.0.dev8/docs/ROADMAP.md +38 -0
- rustuya_local-0.1.0.dev8/docs/STATUS.md +41 -0
- rustuya_local-0.1.0.dev8/docs/history/ROADMAP-2026-10-12.md +111 -0
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/pyproject.toml +1 -1
- rustuya_local-0.1.0.dev8/release-notes.md +12 -0
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/src/rustuya_local/host/mqtt.py +49 -3
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/src/rustuya_local/host/pack.py +27 -5
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/src/rustuya_local/host/transport.py +10 -1
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/tests/ha/test_panel.py +72 -0
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/tests/unit/test_host.py +4 -2
- rustuya_local-0.1.0.dev8/tests/unit/test_mqtt_lifecycle.py +110 -0
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/tests/unit/test_pack.py +50 -0
- rustuya_local-0.1.0.dev7/docs/REDESIGN.md +0 -327
- rustuya_local-0.1.0.dev7/docs/ROADMAP.md +0 -102
- rustuya_local-0.1.0.dev7/docs/STATUS.md +0 -6
- rustuya_local-0.1.0.dev7/release-notes.md +0 -3
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/.github/dependabot.yml +0 -0
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/.github/workflows/_test.yml +0 -0
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/.github/workflows/ci.yml +0 -0
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/.github/workflows/hacs.yml +0 -0
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/.github/workflows/hassfest.yml +0 -0
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/.github/workflows/release.yml +0 -0
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/.gitignore +0 -0
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/LICENSE +0 -0
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/custom_components/rustuya/brand/icon.png +0 -0
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/custom_components/rustuya/brand/icon.svg +0 -0
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/custom_components/rustuya/brand/icon@2x.png +0 -0
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/custom_components/rustuya/brand/logo.png +0 -0
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/custom_components/rustuya/brand/logo.svg +0 -0
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/custom_components/rustuya/brand/logo@2x.png +0 -0
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/custom_components/rustuya/bridge_supervisor.py +0 -0
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/custom_components/rustuya/bridge_sync.py +0 -0
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/custom_components/rustuya/broker.py +0 -0
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/custom_components/rustuya/config_flow.py +0 -0
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/custom_components/rustuya/const.py +0 -0
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/custom_components/rustuya/manager_registry.py +0 -0
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/custom_components/rustuya/manager_session.py +0 -0
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/custom_components/rustuya/panel_bridge.py +0 -0
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/custom_components/rustuya/services.yaml +0 -0
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/custom_components/rustuya/strings.json +0 -0
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/custom_components/rustuya/translations/en.json +0 -0
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/custom_components/rustuya/translations/ko.json +0 -0
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/custom_components/rustuya/www/rustuya-panel.js +0 -0
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/hacs.json +0 -0
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/scripts/build_dropin.py +0 -0
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/scripts/release_notes.py +0 -0
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/src/rustuya_local/__init__.py +0 -0
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/src/rustuya_local/__main__.py +0 -0
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/src/rustuya_local/async_tools.py +0 -0
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/src/rustuya_local/bridge_client.py +0 -0
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/src/rustuya_local/cli.py +0 -0
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/src/rustuya_local/config.py +0 -0
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/src/rustuya_local/files.py +0 -0
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/src/rustuya_local/host/__init__.py +0 -0
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/src/rustuya_local/host/devices.py +0 -0
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/src/rustuya_local/host/hub.py +0 -0
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/src/rustuya_local/host/il.py +0 -0
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/src/rustuya_local/host/memory.py +0 -0
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/src/rustuya_local/host/overrides.py +0 -0
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/src/rustuya_local/host/rules.py +0 -0
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/src/rustuya_local/host/runner.py +0 -0
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/src/rustuya_local/manager_plugin/__init__.py +0 -0
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/src/rustuya_local/manager_plugin/api.py +0 -0
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/src/rustuya_local/manager_plugin/static/index.js +0 -0
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/src/rustuya_local/service.py +0 -0
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/src/rustuya_local/validation.py +0 -0
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/tests/e2e/conftest.py +0 -0
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/tests/e2e/devices.py +0 -0
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/tests/e2e/test_bridge_registration.py +0 -0
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/tests/e2e/test_chain_in_process.py +0 -0
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/tests/e2e/test_chain_mqtt.py +0 -0
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/tests/e2e/test_daemon.py +0 -0
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/tests/e2e/test_full_stack.py +0 -0
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/tests/e2e/test_full_stack_custom_templates.py +0 -0
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/tests/e2e/test_full_stack_devices.py +0 -0
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/tests/e2e/test_manager_plugin.py +0 -0
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/tests/e2e/test_purge.py +0 -0
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/tests/e2e/test_service.py +0 -0
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/tests/ha/conftest.py +0 -0
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/tests/ha/fake_manager.py +0 -0
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/tests/ha/test_bridge_probe.py +0 -0
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/tests/ha/test_config_flow.py +0 -0
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/tests/ha/test_init.py +0 -0
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/tests/ha/test_manager_registry.py +0 -0
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/tests/ha/test_manager_session.py +0 -0
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/tests/ha/test_migrate.py +0 -0
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/tests/unit/test_config.py +0 -0
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/tests/unit/test_cover_settings.py +0 -0
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/tests/unit/test_dropin.py +0 -0
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/tests/unit/test_files.py +0 -0
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/tests/unit/test_hazardous.py +0 -0
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/tests/unit/test_hub.py +0 -0
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/tests/unit/test_il_vectors.py +0 -0
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/tests/unit/test_link_watcher.py +0 -0
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/tests/unit/test_panel_js.py +0 -0
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/tests/unit/test_plugin_api.py +0 -0
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/tests/unit/test_release_notes.py +0 -0
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/tests/unit/test_rule_files.py +0 -0
- {rustuya_local-0.1.0.dev7 → rustuya_local-0.1.0.dev8}/tests/unit/tuya_samples.py +0 -0
|
@@ -3,11 +3,26 @@
|
|
|
3
3
|
All notable changes to this project are documented in this file. A release's GitHub Release notes are its section
|
|
4
4
|
here (the release workflow reads it, and refuses to publish a version without one).
|
|
5
5
|
|
|
6
|
-
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
7
|
-
|
|
6
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). The current development series uses
|
|
7
|
+
`0.1.0.devN`; the earlier `0.0.x` releases remain below as history.
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [0.1.0.dev8] — 2026-10-13
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
- Always stop the embedded bridge during Home Assistant unload, including service-stop failure or cancellation.
|
|
15
|
+
- Close MQTT callback tasks, ignore queued dispatch after shutdown, retrieve asynchronous handler errors and stop
|
|
16
|
+
the paho thread outside the event loop, including when a connection attempt fails.
|
|
17
|
+
- Preserve concurrent partial Home Assistant settings/options saves by locking reads, merges and restart together.
|
|
18
|
+
- Reject unsafe pack ownership paths, malformed hashes and bounds; preserve host settings and symlink targets.
|
|
19
|
+
|
|
20
|
+
### Documentation
|
|
21
|
+
- Separate current status, architecture and outstanding work from the historical redesign record.
|
|
22
|
+
- Document the 2026-10-13 code review, verification results and remaining reliability issues.
|
|
23
|
+
- Correct daemon setup, test prerequisites and saved cover settings precedence.
|
|
24
|
+
- Translate all Korean Markdown documents into English, including archived design and roadmap history.
|
|
25
|
+
|
|
11
26
|
## [0.1.0.dev7] — 2026-10-12
|
|
12
27
|
|
|
13
28
|
### 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.dev8
|
|
4
4
|
Summary: Local Tuya control: rustuya-bridge -> tuya-interpreter -> IL (ildevice). HA-independent core.
|
|
5
5
|
Project-URL: Homepage, https://github.com/3735943886/rustuya-local
|
|
6
6
|
Project-URL: Repository, https://github.com/3735943886/rustuya-local
|
|
@@ -46,11 +46,29 @@ rustuya-bridge ──MQTT──► rustuya-local ──MQTT (il/…)──► il
|
|
|
46
46
|
|
|
47
47
|
## Daemon
|
|
48
48
|
|
|
49
|
+
```bash
|
|
50
|
+
uv venv --python 3.13
|
|
51
|
+
uv pip install -e .
|
|
52
|
+
.venv/bin/rustuya-local run --config config.json
|
|
49
53
|
```
|
|
50
|
-
|
|
51
|
-
|
|
54
|
+
|
|
55
|
+
Create `config.json` beside the device file before running:
|
|
56
|
+
|
|
57
|
+
```json
|
|
58
|
+
{
|
|
59
|
+
"bridge": {"host": "localhost", "port": 1883, "root": "rustuya"},
|
|
60
|
+
"il": {"host": "localhost", "port": 1883, "prefix": "il", "source": "tuya"},
|
|
61
|
+
"devices": "tuyadevices.json",
|
|
62
|
+
"custom_converters": "custom_converters",
|
|
63
|
+
"pack": true,
|
|
64
|
+
"watch_interval": 5,
|
|
65
|
+
"options": {"allow_hazardous": false, "expose_unused": false}
|
|
66
|
+
}
|
|
52
67
|
```
|
|
53
68
|
|
|
69
|
+
The package requires Python 3.13 or newer. Device and converter paths are relative to the configuration file.
|
|
70
|
+
`watch_interval: 0` disables polling; use a positive interval to apply file edits and downloaded rules while running.
|
|
71
|
+
|
|
54
72
|
`config.json` (see `src/rustuya_local/config.py`): the bridge and IL brokers, the IL prefix, the device list, a
|
|
55
73
|
`custom_converters` directory, and options for the Hub (`allow_hazardous`, `expose_unused`, `overrides`).
|
|
56
74
|
The device list is the `tuyadevices.json` that [rustuya-manager](https://github.com/3735943886/rustuya-manager) keeps (its QR-login wizard writes
|
|
@@ -134,7 +152,9 @@ device settings are enabled, unless an override selects another source. A settin
|
|
|
134
152
|
the setting is saved as the device's block in `zz_settings.json` in the converters directory and applied at once, so it
|
|
135
153
|
survives a restart and can be edited there too. Without a converters directory (a daemon with no
|
|
136
154
|
`custom_converters`) the switches are not offered; `"options": {"device_settings": false}` turns them off.
|
|
137
|
-
A block with the same `cover` keys in your own file or the builtin rules sets a model's starting point
|
|
155
|
+
A block with the same `cover` keys in your own file or the builtin rules sets a model's starting point.
|
|
156
|
+
User files, including `zz_settings.json`, are applied in filename order; a later file can override saved settings.
|
|
157
|
+
The daemon's inline `options.overrides` is applied after file layers.
|
|
138
158
|
Older `cover.infer_motion` and `converters.cover_motion` blocks are upgraded on loading to `cover.state_source`
|
|
139
159
|
(`inferred`, or `none` for `infer_motion: false`) and `cover.settle`; see [migration notes](docs/MIGRATING.md).
|
|
140
160
|
|
|
@@ -172,15 +192,22 @@ rustuya-local release (bump the patch version and tag; the manager's catalog fol
|
|
|
172
192
|
|
|
173
193
|
## Tests
|
|
174
194
|
|
|
175
|
-
```
|
|
176
|
-
|
|
195
|
+
```bash
|
|
196
|
+
uv venv --python 3.14
|
|
197
|
+
uv pip install -e ../ildevice-homeassistant -e ".[test,test-ha,fullstack,manager]"
|
|
198
|
+
.venv/bin/python -m pytest -q
|
|
199
|
+
ruff check src tests custom_components scripts
|
|
177
200
|
```
|
|
178
201
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
202
|
+
The complete suite uses Python 3.14 because the consumer and Home Assistant test dependencies require it.
|
|
203
|
+
Install `mosquitto` for broker tests and Node.js for panel JavaScript tests. The command above installs the consumer
|
|
204
|
+
from a sibling checkout and other dependencies from the package index; CI follows the same approach.
|
|
205
|
+
Missing base or test dependencies can fail collection; optional broker, emulator and fixture tests skip when their
|
|
206
|
+
prerequisites are unavailable. The full-stack device comparison uses `../tuya-interpreter/tests/golden`; IL wire and
|
|
207
|
+
topic vectors use `../ildevice/vectors` (or the `ILDEVICE` environment variable). Keep those checkouts for these checks.
|
|
182
208
|
|
|
183
|
-
- `tests/unit/`:
|
|
209
|
+
- `tests/unit/`: configuration validation, host lifecycle, IL vectors, rule loading, settings persistence,
|
|
210
|
+
pack ownership, converter APIs, panel JavaScript and release packaging.
|
|
184
211
|
- `tests/e2e/test_service.py`: the service on in-memory brokers (overrides directory, a failed start).
|
|
185
212
|
- `tests/e2e/test_chain_*.py`, `test_daemon.py`: the runner with ildevice-homeassistant's consumer model on the other side, in one process
|
|
186
213
|
and over a real broker; the daemon as a subprocess.
|
|
@@ -190,8 +217,14 @@ Everything here needs the sibling repos and, for the last two groups, `mosquitto
|
|
|
190
217
|
(`tuyamock`) around the daemon: state, commands, rejections, a device dropping off and returning, a daemon restart, and a
|
|
191
218
|
differential check that 38 real device shapes come out of the whole chain exactly as `tuya-interpreter` computes them.
|
|
192
219
|
|
|
193
|
-
##
|
|
220
|
+
## Documentation
|
|
194
221
|
|
|
195
|
-
|
|
222
|
+
- [Current status](docs/STATUS.md): version, document index and verification boundaries.
|
|
223
|
+
- [Architecture](docs/ARCHITECTURE.md): current components, lifecycle and rule precedence.
|
|
224
|
+
- [Migration](docs/MIGRATING.md): upgrading old converters and rustuya-homeassistant installations.
|
|
225
|
+
- [Code review](docs/REVIEW.md): findings and local verification from 2026-10-13.
|
|
226
|
+
- [Roadmap](docs/ROADMAP.md): outstanding work and real-device checks.
|
|
227
|
+
- [Design history](docs/REDESIGN.md): the September redesign record; historical APIs and versions.
|
|
228
|
+
- [Changelog](CHANGELOG.md): changes grouped by release.
|
|
196
229
|
|
|
197
|
-
Licence:
|
|
230
|
+
Licence: [MIT](LICENSE).
|
|
@@ -20,11 +20,29 @@ rustuya-bridge ──MQTT──► rustuya-local ──MQTT (il/…)──► il
|
|
|
20
20
|
|
|
21
21
|
## Daemon
|
|
22
22
|
|
|
23
|
+
```bash
|
|
24
|
+
uv venv --python 3.13
|
|
25
|
+
uv pip install -e .
|
|
26
|
+
.venv/bin/rustuya-local run --config config.json
|
|
23
27
|
```
|
|
24
|
-
|
|
25
|
-
|
|
28
|
+
|
|
29
|
+
Create `config.json` beside the device file before running:
|
|
30
|
+
|
|
31
|
+
```json
|
|
32
|
+
{
|
|
33
|
+
"bridge": {"host": "localhost", "port": 1883, "root": "rustuya"},
|
|
34
|
+
"il": {"host": "localhost", "port": 1883, "prefix": "il", "source": "tuya"},
|
|
35
|
+
"devices": "tuyadevices.json",
|
|
36
|
+
"custom_converters": "custom_converters",
|
|
37
|
+
"pack": true,
|
|
38
|
+
"watch_interval": 5,
|
|
39
|
+
"options": {"allow_hazardous": false, "expose_unused": false}
|
|
40
|
+
}
|
|
26
41
|
```
|
|
27
42
|
|
|
43
|
+
The package requires Python 3.13 or newer. Device and converter paths are relative to the configuration file.
|
|
44
|
+
`watch_interval: 0` disables polling; use a positive interval to apply file edits and downloaded rules while running.
|
|
45
|
+
|
|
28
46
|
`config.json` (see `src/rustuya_local/config.py`): the bridge and IL brokers, the IL prefix, the device list, a
|
|
29
47
|
`custom_converters` directory, and options for the Hub (`allow_hazardous`, `expose_unused`, `overrides`).
|
|
30
48
|
The device list is the `tuyadevices.json` that [rustuya-manager](https://github.com/3735943886/rustuya-manager) keeps (its QR-login wizard writes
|
|
@@ -108,7 +126,9 @@ device settings are enabled, unless an override selects another source. A settin
|
|
|
108
126
|
the setting is saved as the device's block in `zz_settings.json` in the converters directory and applied at once, so it
|
|
109
127
|
survives a restart and can be edited there too. Without a converters directory (a daemon with no
|
|
110
128
|
`custom_converters`) the switches are not offered; `"options": {"device_settings": false}` turns them off.
|
|
111
|
-
A block with the same `cover` keys in your own file or the builtin rules sets a model's starting point
|
|
129
|
+
A block with the same `cover` keys in your own file or the builtin rules sets a model's starting point.
|
|
130
|
+
User files, including `zz_settings.json`, are applied in filename order; a later file can override saved settings.
|
|
131
|
+
The daemon's inline `options.overrides` is applied after file layers.
|
|
112
132
|
Older `cover.infer_motion` and `converters.cover_motion` blocks are upgraded on loading to `cover.state_source`
|
|
113
133
|
(`inferred`, or `none` for `infer_motion: false`) and `cover.settle`; see [migration notes](docs/MIGRATING.md).
|
|
114
134
|
|
|
@@ -146,15 +166,22 @@ rustuya-local release (bump the patch version and tag; the manager's catalog fol
|
|
|
146
166
|
|
|
147
167
|
## Tests
|
|
148
168
|
|
|
149
|
-
```
|
|
150
|
-
|
|
169
|
+
```bash
|
|
170
|
+
uv venv --python 3.14
|
|
171
|
+
uv pip install -e ../ildevice-homeassistant -e ".[test,test-ha,fullstack,manager]"
|
|
172
|
+
.venv/bin/python -m pytest -q
|
|
173
|
+
ruff check src tests custom_components scripts
|
|
151
174
|
```
|
|
152
175
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
176
|
+
The complete suite uses Python 3.14 because the consumer and Home Assistant test dependencies require it.
|
|
177
|
+
Install `mosquitto` for broker tests and Node.js for panel JavaScript tests. The command above installs the consumer
|
|
178
|
+
from a sibling checkout and other dependencies from the package index; CI follows the same approach.
|
|
179
|
+
Missing base or test dependencies can fail collection; optional broker, emulator and fixture tests skip when their
|
|
180
|
+
prerequisites are unavailable. The full-stack device comparison uses `../tuya-interpreter/tests/golden`; IL wire and
|
|
181
|
+
topic vectors use `../ildevice/vectors` (or the `ILDEVICE` environment variable). Keep those checkouts for these checks.
|
|
156
182
|
|
|
157
|
-
- `tests/unit/`:
|
|
183
|
+
- `tests/unit/`: configuration validation, host lifecycle, IL vectors, rule loading, settings persistence,
|
|
184
|
+
pack ownership, converter APIs, panel JavaScript and release packaging.
|
|
158
185
|
- `tests/e2e/test_service.py`: the service on in-memory brokers (overrides directory, a failed start).
|
|
159
186
|
- `tests/e2e/test_chain_*.py`, `test_daemon.py`: the runner with ildevice-homeassistant's consumer model on the other side, in one process
|
|
160
187
|
and over a real broker; the daemon as a subprocess.
|
|
@@ -164,8 +191,14 @@ Everything here needs the sibling repos and, for the last two groups, `mosquitto
|
|
|
164
191
|
(`tuyamock`) around the daemon: state, commands, rejections, a device dropping off and returning, a daemon restart, and a
|
|
165
192
|
differential check that 38 real device shapes come out of the whole chain exactly as `tuya-interpreter` computes them.
|
|
166
193
|
|
|
167
|
-
##
|
|
194
|
+
## Documentation
|
|
168
195
|
|
|
169
|
-
|
|
196
|
+
- [Current status](docs/STATUS.md): version, document index and verification boundaries.
|
|
197
|
+
- [Architecture](docs/ARCHITECTURE.md): current components, lifecycle and rule precedence.
|
|
198
|
+
- [Migration](docs/MIGRATING.md): upgrading old converters and rustuya-homeassistant installations.
|
|
199
|
+
- [Code review](docs/REVIEW.md): findings and local verification from 2026-10-13.
|
|
200
|
+
- [Roadmap](docs/ROADMAP.md): outstanding work and real-device checks.
|
|
201
|
+
- [Design history](docs/REDESIGN.md): the September redesign record; historical APIs and versions.
|
|
202
|
+
- [Changelog](CHANGELOG.md): changes grouped by release.
|
|
170
203
|
|
|
171
|
-
Licence:
|
|
204
|
+
Licence: [MIT](LICENSE).
|
|
@@ -252,13 +252,19 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|
|
252
252
|
# setup that follows puts it back anyway; only disabling the entry (or deleting it, below) removes it
|
|
253
253
|
if entry.disabled_by is not None:
|
|
254
254
|
panel.async_remove(hass)
|
|
255
|
-
runtime: RuntimeData = hass.data[DOMAIN]
|
|
255
|
+
runtime: RuntimeData = hass.data[DOMAIN][entry.entry_id]
|
|
256
256
|
restarting = getattr(runtime, "restarting", False) and entry.disabled_by is None
|
|
257
|
-
|
|
257
|
+
try:
|
|
258
|
+
await runtime.service.stop(offline=not restarting)
|
|
259
|
+
finally:
|
|
260
|
+
try:
|
|
261
|
+
if runtime.embedded_bridge:
|
|
262
|
+
await runtime.embedded_bridge.stop()
|
|
263
|
+
finally:
|
|
264
|
+
# A stopped service cannot be reused, even when draining its commands failed.
|
|
265
|
+
hass.data[DOMAIN].pop(entry.entry_id, None)
|
|
258
266
|
if restarting:
|
|
259
267
|
hass.data[RESUME] = True
|
|
260
|
-
if runtime.embedded_bridge:
|
|
261
|
-
await runtime.embedded_bridge.stop()
|
|
262
268
|
return True
|
|
263
269
|
|
|
264
270
|
|
|
@@ -221,11 +221,12 @@ class OptionsView(_View):
|
|
|
221
221
|
or not all(isinstance(v, bool) for v in body.values())):
|
|
222
222
|
return self.json_message(f"the options are {sorted(DEFAULT_OPTIONS)}, each true or false",
|
|
223
223
|
HTTPStatus.BAD_REQUEST)
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
224
|
+
async with _settings_lock(request.app["hass"]):
|
|
225
|
+
options = {**entry.options, **body}
|
|
226
|
+
if options == dict(entry.options):
|
|
227
|
+
return self.json({"restarting": False})
|
|
228
|
+
await _restart(request.app["hass"], entry, options=options) # answered once it has restarted
|
|
229
|
+
return self.json({"restarting": True})
|
|
229
230
|
|
|
230
231
|
|
|
231
232
|
def settings_of(data: Any) -> dict[str, Any]:
|
|
@@ -275,24 +276,30 @@ class SettingsView(_View):
|
|
|
275
276
|
@_entry_view
|
|
276
277
|
async def put(self, request: web.Request, entry: Any, runtime: Any) -> web.Response:
|
|
277
278
|
hass = request.app["hass"]
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
279
|
+
body = await _json_body(request)
|
|
280
|
+
async with _settings_lock(hass):
|
|
281
|
+
try:
|
|
282
|
+
new = validate_settings(hass, entry.data, body)
|
|
283
|
+
except ValueError as e:
|
|
284
|
+
return self.json_message(str(e), HTTPStatus.BAD_REQUEST)
|
|
285
|
+
old = dict(entry.data)
|
|
286
|
+
if new == settings_of(old):
|
|
287
|
+
return self.json({"restarting": False})
|
|
288
|
+
external = old.get(CONF_BRIDGE_MODE) != BRIDGE_EMBEDDED
|
|
289
|
+
if external and new[CONF_BRIDGE_ROOT] != old[CONF_BRIDGE_ROOT]:
|
|
290
|
+
from .config_flow import _probe_bridge
|
|
291
|
+
|
|
292
|
+
error = await _probe_bridge(hass, {**old, **new})
|
|
293
|
+
if error is not None:
|
|
294
|
+
return self.json_message(
|
|
295
|
+
f"no rustuya-bridge answers on {new[CONF_BRIDGE_ROOT]}" if error == "bridge_not_found"
|
|
296
|
+
else "cannot connect to the broker", HTTPStatus.BAD_REQUEST)
|
|
297
|
+
await _apply(hass, entry, old, {**old, **new}) # answered once it has restarted
|
|
298
|
+
return self.json({"restarting": True})
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _settings_lock(hass: HomeAssistant) -> asyncio.Lock:
|
|
302
|
+
return hass.data.setdefault(f"{DOMAIN}_settings_lock", asyncio.Lock())
|
|
296
303
|
|
|
297
304
|
|
|
298
305
|
async def _apply(hass: HomeAssistant, entry: Any, old: dict[str, Any], data: dict[str, Any]) -> None:
|
|
@@ -318,34 +325,33 @@ async def _restart(hass: HomeAssistant, entry: Any, *, data: dict[str, Any] | No
|
|
|
318
325
|
"""Stop the entry, run `between`, store the new data / options, start it again — all awaited, so the panel's save
|
|
319
326
|
answers once the restart is over (the page then reloads onto the restarted entry, not onto one about to go).
|
|
320
327
|
Stopped, the entry has no update listener, so storing does not start a second reload. An options-only restart
|
|
321
|
-
keeps the IL presence online across it (`mark_restart`); new data may move the broker or the IL topics.
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
328
|
+
keeps the IL presence online across it (`mark_restart`); new data may move the broker or the IL topics.
|
|
329
|
+
The request handler holds `_settings_lock` from reading the current entry through this restart."""
|
|
330
|
+
old_data, old_options = dict(entry.data), dict(entry.options)
|
|
331
|
+
if data is None:
|
|
332
|
+
from . import mark_restart
|
|
333
|
+
|
|
334
|
+
mark_restart(hass, entry)
|
|
335
|
+
if not await hass.config_entries.async_unload(entry.entry_id):
|
|
336
|
+
raise RestartFailed("Could not stop the integration; settings were not changed")
|
|
337
|
+
try:
|
|
338
|
+
if between is not None:
|
|
339
|
+
await between()
|
|
340
|
+
changes = {k: v for k, v in (("data", data), ("options", options)) if v is not None}
|
|
341
|
+
hass.config_entries.async_update_entry(entry, **changes)
|
|
342
|
+
if not await hass.config_entries.async_setup(entry.entry_id):
|
|
343
|
+
raise RestartFailed("The integration could not start with these settings")
|
|
344
|
+
except Exception as e:
|
|
345
|
+
# Failed setup leaves SETUP_ERROR/SETUP_RETRY, which cannot be set up
|
|
346
|
+
# directly. Unload also cancels any scheduled retry with the bad data.
|
|
347
|
+
await hass.config_entries.async_unload(entry.entry_id)
|
|
348
|
+
hass.config_entries.async_update_entry(entry, data=old_data, options=old_options)
|
|
331
349
|
try:
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
raise RestartFailed("The integration could not start with these settings")
|
|
338
|
-
except Exception as e:
|
|
339
|
-
# Failed setup leaves SETUP_ERROR/SETUP_RETRY, which cannot be set up
|
|
340
|
-
# directly. Unload also cancels any scheduled retry with the bad data.
|
|
341
|
-
await hass.config_entries.async_unload(entry.entry_id)
|
|
342
|
-
hass.config_entries.async_update_entry(entry, data=old_data, options=old_options)
|
|
343
|
-
try:
|
|
344
|
-
restored = await hass.config_entries.async_setup(entry.entry_id)
|
|
345
|
-
except Exception: # noqa: BLE001 -- keep the recovery API usable even if rollback startup fails
|
|
346
|
-
restored = False
|
|
347
|
-
suffix = "Previous settings restored" if restored else "Previous settings restored, but startup still fails"
|
|
348
|
-
raise RestartFailed(f"{e}. {suffix}; settings remain editable") from e
|
|
350
|
+
restored = await hass.config_entries.async_setup(entry.entry_id)
|
|
351
|
+
except Exception: # noqa: BLE001 -- keep the recovery API usable even if rollback startup fails
|
|
352
|
+
restored = False
|
|
353
|
+
suffix = "Previous settings restored" if restored else "Previous settings restored, but startup still fails"
|
|
354
|
+
raise RestartFailed(f"{e}. {suffix}; settings remain editable") from e
|
|
349
355
|
|
|
350
356
|
|
|
351
357
|
class PackView(_View):
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Current architecture
|
|
2
|
+
|
|
3
|
+
Baseline: 2026-10-13, `rustuya-local 0.1.0.dev8`.
|
|
4
|
+
See [README](../README.md) for installation and [REDESIGN](REDESIGN.md) for earlier designs.
|
|
5
|
+
|
|
6
|
+
```text
|
|
7
|
+
Tuya LAN devices
|
|
8
|
+
↕
|
|
9
|
+
rustuya-bridge / embedded pyrustuyabridge
|
|
10
|
+
↕ MQTT (topic and payload templates announced by the bridge)
|
|
11
|
+
BridgeClient ↔ Runner ↔ Hub ↔ tuya-interpreter TuyaDriver
|
|
12
|
+
↕
|
|
13
|
+
IL MQTT transport
|
|
14
|
+
↕
|
|
15
|
+
ildevice-homeassistant / other IL consumers
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Components
|
|
19
|
+
|
|
20
|
+
| Location | Responsibility |
|
|
21
|
+
|---|---|
|
|
22
|
+
| `src/rustuya_local/service.py` | Shared assembly, startup/shutdown, pack updates, retained cleanup, raw DP commands |
|
|
23
|
+
| `bridge_client.py` | Bridge templates, paginated registry snapshots, intersection of registered and configured devices |
|
|
24
|
+
| `host/hub.py` | Driver registry, replacement/reload, native rule-pack reference management |
|
|
25
|
+
| `host/runner.py` | IL subscriptions and publish queue, timers, presence/probes, settings callbacks |
|
|
26
|
+
| `host/il.py` | Mapping driver outputs to IL MQTT topics and values |
|
|
27
|
+
| `host/rules.py`, `overrides.py` | Rule discovery/merging, legacy cover keys, file watching and settings persistence |
|
|
28
|
+
| `host/pack.py` | Official rule downloads, hash verification and ownership ledger |
|
|
29
|
+
| `host/mqtt.py`, `memory.py` | paho MQTT and in-process test transports |
|
|
30
|
+
| `cli.py`, `config.py` | Standalone daemon and configuration validation |
|
|
31
|
+
| `manager_plugin/` | Supervision by the manager, settings/file APIs and web tab |
|
|
32
|
+
| `custom_components/rustuya/` | HA config entry, embedded bridge, login/registration and administrator panel |
|
|
33
|
+
| External `tuya-interpreter` | Single-device Tuya DP interpretation, command validation and rule composition |
|
|
34
|
+
|
|
35
|
+
HA entity creation belongs to the IL consumer. The base package does not import Home Assistant.
|
|
36
|
+
|
|
37
|
+
## Startup and shutdown
|
|
38
|
+
|
|
39
|
+
`Service.start()` prepares the native library and rules in worker threads, then connects the bridge and IL transports
|
|
40
|
+
and creates the Hub. It refuses startup if a producer with the same IL prefix/source answers a probe. This is a
|
|
41
|
+
preflight check, not an atomic distributed lock. After a complete bridge registry snapshot, it advertises devices
|
|
42
|
+
present in both the device file and registry and clears stale IL topics left by the previous run. Pages are checked
|
|
43
|
+
for order, counts and duplicates, but the bridge has no request ID for complete correlation with other requests.
|
|
44
|
+
|
|
45
|
+
Normal shutdown marks presence offline and leaves descriptors/state retained. Restarting within the same namespace
|
|
46
|
+
preserves presence. Moving namespaces or running `purge` clears the previous retained topics. `AsyncExitStack` manages
|
|
47
|
+
service resources. HA unload always attempts embedded-bridge shutdown, even if service shutdown fails or is cancelled,
|
|
48
|
+
and then removes the unusable runtime. The resume marker is set only after successful cleanup.
|
|
49
|
+
|
|
50
|
+
MQTT close blocks new dispatch, cancels and awaits asynchronous handlers, and stops the paho thread outside the event
|
|
51
|
+
loop. Cancelling close still waits for its cleanup before propagating cancellation. Asynchronous handler errors are
|
|
52
|
+
retrieved and logged. Closed transports cannot be connected, subscribed or published through again.
|
|
53
|
+
|
|
54
|
+
## Rule and settings precedence
|
|
55
|
+
|
|
56
|
+
1. Bundled `rules/builtin/*.json` from `tuya-interpreter`.
|
|
57
|
+
2. Downloaded `rules/builtin/*.json` below the configured rule directories.
|
|
58
|
+
3. JSON files from `TUYA_ENGINE_RULES` and user directories, merged by filename byte order.
|
|
59
|
+
4. Inline service `hub_options.overrides`; the daemon calls this `options.overrides`.
|
|
60
|
+
|
|
61
|
+
Later values override earlier ones. `manifest.json` is excluded from rule layers, and directory files starting with
|
|
62
|
+
`.` or `_` are skipped. `zz_settings.json` is a normal user layer: later filenames or inline overrides can replace
|
|
63
|
+
its settings. Invalid user layers are skipped with warnings. A rejected Hub reload preserves the existing drivers.
|
|
64
|
+
|
|
65
|
+
Automatic pack updates run at startup and daily; manual updates also work when automatic updates are disabled.
|
|
66
|
+
Only files with matching ownership hashes are replaced or removed; user edits are preserved. Manifest and ledger
|
|
67
|
+
entries must have safe filenames and SHA-256 hashes. Host settings, manifests and symlink targets cannot be owned
|
|
68
|
+
by the pack. Each managed file is checked against the directory boundary.
|
|
69
|
+
|
|
70
|
+
The watcher must run to apply downloaded files to an active Hub. The daemon's `watch_interval: 0` disables polling.
|
|
71
|
+
IL settings saves use a separate lock and atomic replacement, then reload immediately. HA settings/options API
|
|
72
|
+
requests share a lock covering the current-entry read, patch merge, validation, probing and restart.
|
|
73
|
+
|
|
74
|
+
## Verification and coupling
|
|
75
|
+
|
|
76
|
+
Unit tests, in-process services, real MQTT brokers, Tuya emulators and HA integration tests verify different
|
|
77
|
+
boundaries. They do not replace real LAN-device tests. Driver state internals and the manager's private registry API
|
|
78
|
+
remain coupling points that need checking when dependencies change. See [REVIEW](REVIEW.md) for fixes and limits.
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Migration guide
|
|
2
|
+
|
|
3
|
+
For the current component boundaries and file precedence, see [Architecture](ARCHITECTURE.md).
|
|
4
|
+
Use [README](../README.md) for installation and [Changelog](../CHANGELOG.md) for version-specific changes.
|
|
5
|
+
|
|
6
|
+
## Moving to rustuya-local 0.1 / tuya-interpreter
|
|
2
7
|
|
|
3
8
|
The device file, MQTT configuration and IL topics stay the same. Custom files in the converters directory now use
|
|
4
9
|
the rule-layer format. Wrap an existing product/device override mapping in `{"overrides": ...}`. For example:
|
|
@@ -7,13 +12,15 @@ the rule-layer format. Wrap an existing product/device override mapping in `{"ov
|
|
|
7
12
|
{"overrides": {"product_id": {"cover": {"invert_position": true}}}}
|
|
8
13
|
```
|
|
9
14
|
|
|
10
|
-
Apply the same conversion to `zz_settings.json` to preserve cover settings saved by an older version. Leave
|
|
15
|
+
Back up the converters directory before editing. Apply the same conversion to `zz_settings.json` to preserve cover settings saved by an older version. Leave
|
|
11
16
|
`options.overrides` in the daemon configuration as a product/device mapping; the host still accepts that format.
|
|
12
17
|
Rewrite Python converters as JSON programs under `converters.declarative`. Invalid JSON rule layers are reported
|
|
13
18
|
and skipped, so check the logs after upgrading. The managed override pack updates automatically when enabled;
|
|
14
|
-
files you edited remain yours to convert.
|
|
19
|
+
files you edited remain yours to convert. JSON files use filename order, including `zz_settings.json`; check for later
|
|
20
|
+
files or inline overrides that replace saved settings. See the cover compatibility details below for `infer_motion`
|
|
21
|
+
and `converters.cover_motion`.
|
|
15
22
|
|
|
16
|
-
|
|
23
|
+
## Moving from rustuya-homeassistant
|
|
17
24
|
|
|
18
25
|
rustuya-homeassistant (`rustuya-ha`) published Home Assistant MQTT discovery for your Tuya devices, as a CLI or a
|
|
19
26
|
rustuya-manager plugin. It is retired. Its replacement is two pieces:
|
|
@@ -24,7 +31,7 @@ rustuya-manager plugin. It is retired. Its replacement is two pieces:
|
|
|
24
31
|
- **[ildevice-homeassistant](https://github.com/3735943886/ildevice-homeassistant)** (HACS) turns IL devices
|
|
25
32
|
into Home Assistant entities. MQTT discovery is no longer used.
|
|
26
33
|
|
|
27
|
-
|
|
34
|
+
### What changes for you
|
|
28
35
|
|
|
29
36
|
- **Entity ids change.** The old entities belonged to Home Assistant's `mqtt` integration; the new ones belong to
|
|
30
37
|
`ildevice`, and Home Assistant keeps the two apart. Automations, scripts and dashboards that name the old entities
|
|
@@ -34,7 +41,7 @@ rustuya-manager plugin. It is retired. Its replacement is two pieces:
|
|
|
34
41
|
with more, fewer or differently split entities than v1 made.
|
|
35
42
|
- **Availability** comes from rustuya-local's presence: when it stops, its devices show unavailable.
|
|
36
43
|
|
|
37
|
-
|
|
44
|
+
### Steps
|
|
38
45
|
|
|
39
46
|
1. **Remove the old discovery** while `rustuya-ha` still runs, so Home Assistant drops those entities:
|
|
40
47
|
|
|
@@ -65,7 +72,7 @@ rustuya-manager plugin. It is retired. Its replacement is two pieces:
|
|
|
65
72
|
A property the tables do not build is defined from its dp in `props`.
|
|
66
73
|
- A curtain's `invert_position` / `invert_set_position`, reversed command words and derived state are not files any
|
|
67
74
|
more: each cover has configuration switches for them (README, *Cover settings*).
|
|
68
|
-
- Rewrite Python converters as JSON programs under `converters.declarative`; Python plugins are not loaded
|
|
75
|
+
- Rewrite Python converters as JSON programs under `converters.declarative`; Python plugins are not loaded by rustuya-local 0.1.
|
|
69
76
|
- `converters.cover_motion` and a cover's `infer_motion` are read as the cover's `state_source` (`inferred`, or
|
|
70
77
|
`none` for `infer_motion: false`), and a `cover_motion` `settle` as the cover's `settle`; its other keys are
|
|
71
78
|
dropped with a warning. `zz_settings.json` is rewritten in the new form the next time a setting is saved.
|