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