rustuya-local 0.1.0.dev10__tar.gz → 0.1.0.dev12__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.dev10 → rustuya_local-0.1.0.dev12}/CHANGELOG.md +13 -0
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/PKG-INFO +2 -2
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/custom_components/rustuya/manifest.json +3 -3
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/docs/ARCHITECTURE.md +9 -0
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/docs/REVIEW.md +2 -1
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/docs/ROADMAP.md +1 -1
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/docs/STATUS.md +3 -3
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/pyproject.toml +2 -2
- rustuya_local-0.1.0.dev12/release-notes.md +4 -0
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/src/rustuya_local/host/hub.py +1 -10
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/src/rustuya_local/host/il.py +4 -1
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/src/rustuya_local/service.py +10 -6
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/tests/e2e/test_bridge_registration.py +1 -1
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/tests/e2e/test_chain_in_process.py +1 -1
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/tests/e2e/test_chain_mqtt.py +1 -1
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/tests/e2e/test_daemon.py +6 -6
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/tests/e2e/test_full_stack.py +2 -2
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/tests/e2e/test_full_stack_custom_templates.py +1 -1
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/tests/e2e/test_manager_plugin.py +6 -6
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/tests/e2e/test_purge.py +6 -6
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/tests/e2e/test_service.py +12 -8
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/tests/ha/test_init.py +10 -10
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/tests/unit/test_host.py +10 -10
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/tests/unit/test_hub.py +7 -7
- rustuya_local-0.1.0.dev12/tests/unit/test_il_topics.py +27 -0
- rustuya_local-0.1.0.dev10/release-notes.md +0 -9
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/.github/dependabot.yml +0 -0
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/.github/workflows/_test.yml +0 -0
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/.github/workflows/ci.yml +0 -0
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/.github/workflows/hacs.yml +0 -0
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/.github/workflows/hassfest.yml +0 -0
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/.github/workflows/release.yml +0 -0
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/.gitignore +0 -0
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/LICENSE +0 -0
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/README.md +0 -0
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/custom_components/rustuya/__init__.py +0 -0
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/custom_components/rustuya/brand/icon.png +0 -0
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/custom_components/rustuya/brand/icon.svg +0 -0
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/custom_components/rustuya/brand/icon@2x.png +0 -0
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/custom_components/rustuya/brand/logo.png +0 -0
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/custom_components/rustuya/brand/logo.svg +0 -0
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/custom_components/rustuya/brand/logo@2x.png +0 -0
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/custom_components/rustuya/bridge_supervisor.py +0 -0
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/custom_components/rustuya/bridge_sync.py +0 -0
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/custom_components/rustuya/broker.py +0 -0
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/custom_components/rustuya/config_flow.py +0 -0
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/custom_components/rustuya/const.py +0 -0
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/custom_components/rustuya/manager_session.py +0 -0
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/custom_components/rustuya/panel.py +0 -0
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/custom_components/rustuya/panel_bridge.py +0 -0
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/custom_components/rustuya/services.yaml +0 -0
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/custom_components/rustuya/strings.json +0 -0
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/custom_components/rustuya/translations/en.json +0 -0
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/custom_components/rustuya/translations/ko.json +0 -0
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/custom_components/rustuya/www/rustuya-panel.js +0 -0
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/docs/MIGRATING.md +0 -0
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/docs/REDESIGN.md +0 -0
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/docs/history/ROADMAP-2026-10-12.md +0 -0
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/hacs.json +0 -0
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/scripts/build_dropin.py +0 -0
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/scripts/release_notes.py +0 -0
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/src/rustuya_local/__init__.py +0 -0
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/src/rustuya_local/__main__.py +0 -0
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/src/rustuya_local/async_tools.py +0 -0
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/src/rustuya_local/bridge_client.py +0 -0
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/src/rustuya_local/cli.py +0 -0
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/src/rustuya_local/config.py +0 -0
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/src/rustuya_local/files.py +0 -0
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/src/rustuya_local/host/__init__.py +0 -0
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/src/rustuya_local/host/devices.py +0 -0
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/src/rustuya_local/host/memory.py +0 -0
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/src/rustuya_local/host/mqtt.py +0 -0
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/src/rustuya_local/host/overrides.py +0 -0
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/src/rustuya_local/host/pack.py +0 -0
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/src/rustuya_local/host/rules.py +0 -0
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/src/rustuya_local/host/runner.py +0 -0
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/src/rustuya_local/host/transport.py +0 -0
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/src/rustuya_local/manager_plugin/__init__.py +0 -0
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/src/rustuya_local/manager_plugin/api.py +0 -0
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/src/rustuya_local/manager_plugin/static/index.js +0 -0
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/src/rustuya_local/validation.py +0 -0
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/tests/e2e/conftest.py +0 -0
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/tests/e2e/devices.py +0 -0
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/tests/e2e/test_full_stack_devices.py +0 -0
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/tests/ha/conftest.py +0 -0
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/tests/ha/fake_manager.py +0 -0
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/tests/ha/test_bridge_probe.py +0 -0
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/tests/ha/test_bridge_supervisor.py +0 -0
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/tests/ha/test_config_flow.py +0 -0
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/tests/ha/test_manager_session.py +0 -0
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/tests/ha/test_migrate.py +0 -0
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/tests/ha/test_panel.py +0 -0
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/tests/unit/test_config.py +0 -0
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/tests/unit/test_cover_settings.py +0 -0
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/tests/unit/test_dropin.py +0 -0
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/tests/unit/test_files.py +0 -0
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/tests/unit/test_hazardous.py +0 -0
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/tests/unit/test_il_vectors.py +0 -0
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/tests/unit/test_link_watcher.py +0 -0
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/tests/unit/test_mqtt_lifecycle.py +0 -0
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/tests/unit/test_pack.py +0 -0
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/tests/unit/test_panel_js.py +0 -0
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/tests/unit/test_plugin_api.py +0 -0
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/tests/unit/test_release_notes.py +0 -0
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/tests/unit/test_rule_files.py +0 -0
- {rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/tests/unit/tuya_samples.py +0 -0
|
@@ -8,6 +8,19 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [0.1.0.dev12] — 2026-10-18
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
- Promote the 0.1 development line to master; preserve the previous 0.0 line on `legacy/0.0.x`.
|
|
15
|
+
- Publish retained IL descriptors at `<prefix>/<id>/descriptor`. Reserve `descriptor` and `reject` as property names.
|
|
16
|
+
Reconciliation and purge use the new descriptor path; old root topics are left untouched.
|
|
17
|
+
|
|
18
|
+
## [0.1.0.dev11] — 2026-10-18
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
- Decide which drivers a reload replaces with tuya-interpreter's public `TuyaDriver.same_config` instead of
|
|
22
|
+
comparing its version-specific state snapshot; require tuya-interpreter 0.1.0rc2.
|
|
23
|
+
|
|
11
24
|
## [0.1.0.dev10] — 2026-10-18
|
|
12
25
|
|
|
13
26
|
### 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.dev12
|
|
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
|
|
@@ -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: tuya-interpreter>=0.1.
|
|
13
|
+
Requires-Dist: tuya-interpreter>=0.1.0rc2
|
|
14
14
|
Provides-Extra: fullstack
|
|
15
15
|
Requires-Dist: tuyamock; extra == 'fullstack'
|
|
16
16
|
Provides-Extra: manager
|
{rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/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
|
-
"tuya-interpreter>=0.1.
|
|
12
|
+
"tuya-interpreter>=0.1.0rc2",
|
|
13
13
|
"paho-mqtt>=2.0",
|
|
14
14
|
"rustuya-manager>=0.2.3",
|
|
15
15
|
"pyrustuyabridge>=0.4",
|
|
16
|
-
"rustuya-local>=0.1.0.
|
|
16
|
+
"rustuya-local>=0.1.0.dev12"
|
|
17
17
|
],
|
|
18
18
|
"single_config_entry": true,
|
|
19
|
-
"version": "0.1.0.
|
|
19
|
+
"version": "0.1.0.dev12"
|
|
20
20
|
}
|
|
@@ -34,6 +34,15 @@ ildevice-homeassistant / other IL consumers
|
|
|
34
34
|
|
|
35
35
|
HA entity creation belongs to the IL consumer. The base package does not import Home Assistant.
|
|
36
36
|
|
|
37
|
+
## IL topics
|
|
38
|
+
|
|
39
|
+
Descriptors are retained at `<prefix>/<id>/descriptor`; property values stay at
|
|
40
|
+
`<prefix>/<id>/<prop>` and commands at `<prefix>/<id>/<prop>/set`.
|
|
41
|
+
`descriptor` and `reject` are reserved property names. Consumers can discover devices by
|
|
42
|
+
subscribing to `<prefix>/+/descriptor`. The consumer must support this descriptor layout.
|
|
43
|
+
Reconciliation and purge identify owned devices only through this descriptor path.
|
|
44
|
+
Other topics, including `<prefix>/<id>`, are ignored and left untouched.
|
|
45
|
+
|
|
37
46
|
## Startup and shutdown
|
|
38
47
|
|
|
39
48
|
`Service.start()` prepares the native library and rules in worker threads, then connects the bridge and IL transports
|
|
@@ -71,7 +71,8 @@ reproduction. README, architecture and migration notes now explain actual preced
|
|
|
71
71
|
|
|
72
72
|
## Remaining maintenance considerations
|
|
73
73
|
|
|
74
|
-
-
|
|
74
|
+
- ~~`Hub._same` depends on driver state keys `block`, `delta` and `covers`.~~ Resolved: the reload comparison uses
|
|
75
|
+
tuya-interpreter's public `TuyaDriver.same_config`.
|
|
75
76
|
- `manager_registry.ready_registry` patches private manager methods/fields; a public registry contract would reduce coupling.
|
|
76
77
|
- Producer probing is a startup check, not atomic exclusion of two simultaneously starting producers.
|
|
77
78
|
- Bridge status has no request ID; contiguous-page validation cannot perfectly distinguish all other-client responses.
|
|
@@ -15,7 +15,7 @@ Completed milestones and earlier deployment records are preserved in the [previo
|
|
|
15
15
|
## Maintenance
|
|
16
16
|
|
|
17
17
|
- [x] Replace private manager registry patches and wizard state polling with public async contracts.
|
|
18
|
-
- [
|
|
18
|
+
- [x] Reduce dependence on driver state internals in `Hub._same` through a public contract (`TuyaDriver.same_config`).
|
|
19
19
|
- [ ] Verify Python 3.13 with minimum supported dependency versions. The full CI suite uses 3.14; wheel import/CLI smoke tests pass on 3.13.
|
|
20
20
|
- [ ] Define the supported drop-in OS/architectures and provide platform assets as needed. Each archive includes its build host's native library.
|
|
21
21
|
- [ ] Verify ownership/conflict policy when manual file editing, IL settings saves and automatic pack updates overlap.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Current status
|
|
2
2
|
|
|
3
|
-
Updated: 2026-10-18. Baseline: `0.1.0.
|
|
3
|
+
Updated: 2026-10-18. Baseline: `0.1.0.dev11`.
|
|
4
4
|
The state of deployed servers has not been checked.
|
|
5
5
|
|
|
6
6
|
## Implementation
|
|
@@ -12,9 +12,9 @@ rule files and settings persistence. It does not create HA entities or publish M
|
|
|
12
12
|
|
|
13
13
|
| Component | Declared requirement |
|
|
14
14
|
|---|---|
|
|
15
|
-
| rustuya-local | `0.1.0.
|
|
15
|
+
| rustuya-local | `0.1.0.dev11` |
|
|
16
16
|
| Python | Base package `>=3.13`; full tests use 3.14 |
|
|
17
|
-
| tuya-interpreter | `>=0.1.
|
|
17
|
+
| tuya-interpreter | `>=0.1.0rc2` |
|
|
18
18
|
| pyrustuyabridge | `>=0.4` |
|
|
19
19
|
| paho-mqtt | `>=2.0` |
|
|
20
20
|
| rustuya-manager | `>=0.2.3` in the manager test extra and HA manifest |
|
|
@@ -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.dev12"
|
|
8
8
|
description = "Local Tuya control: rustuya-bridge -> tuya-interpreter -> IL (ildevice). HA-independent core."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.13"
|
|
11
11
|
license = "MIT"
|
|
12
|
-
dependencies = ["tuya-interpreter>=0.1.
|
|
12
|
+
dependencies = ["tuya-interpreter>=0.1.0rc2", "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,4 @@
|
|
|
1
|
+
### Changed
|
|
2
|
+
- Promote the 0.1 development line to master; preserve the previous 0.0 line on `legacy/0.0.x`.
|
|
3
|
+
- Publish retained IL descriptors at `<prefix>/<id>/descriptor`. Reserve `descriptor` and `reject` as property names.
|
|
4
|
+
Reconciliation and purge use the new descriptor path; old root topics are left untouched.
|
|
@@ -87,15 +87,6 @@ class Hub:
|
|
|
87
87
|
if not usable_id(device_id):
|
|
88
88
|
raise ValueError(f'device id {device_id!r} is not usable as an il-mqtt topic level (M-2)')
|
|
89
89
|
|
|
90
|
-
@staticmethod
|
|
91
|
-
def _same(old: TuyaDriver, new: TuyaDriver) -> bool:
|
|
92
|
-
# Version-specific driver configuration metadata, compared without interpreting Tuya DPs.
|
|
93
|
-
return (old.rules_id == new.rules_id
|
|
94
|
-
and old.state['block'] == new.state['block']
|
|
95
|
-
and old.state['delta'] == new.state['delta']
|
|
96
|
-
and old.descriptor == new.descriptor
|
|
97
|
-
and [c['settings'] for c in old.state['covers']] == [c['settings'] for c in new.state['covers']])
|
|
98
|
-
|
|
99
90
|
def _adopt(self, old: TuyaDriver | None, new: TuyaDriver, seed: Sequence, now: float,
|
|
100
91
|
*, rules: dict) -> list:
|
|
101
92
|
device_id = new.descriptor['id']
|
|
@@ -160,7 +151,7 @@ class Hub:
|
|
|
160
151
|
replacements = {}
|
|
161
152
|
for device_id, new in fresh.items():
|
|
162
153
|
old = self.drivers[device_id]
|
|
163
|
-
if
|
|
154
|
+
if old.same_config(new):
|
|
164
155
|
continue
|
|
165
156
|
seed = [Connected()] if old.linked else []
|
|
166
157
|
if old.synced:
|
|
@@ -72,7 +72,7 @@ class IlTopics:
|
|
|
72
72
|
self.prefix, self.source = prefix, source
|
|
73
73
|
|
|
74
74
|
def descriptor(self, device_id: str) -> str:
|
|
75
|
-
return f"{self.prefix}/{device_id}"
|
|
75
|
+
return f"{self.prefix}/{device_id}/descriptor"
|
|
76
76
|
|
|
77
77
|
def state(self, device_id: str, prop: str) -> str:
|
|
78
78
|
return f"{self.prefix}/{device_id}/{prop}"
|
|
@@ -106,6 +106,9 @@ class IlTopics:
|
|
|
106
106
|
actions = []
|
|
107
107
|
for output in outputs:
|
|
108
108
|
if isinstance(output, Descriptor):
|
|
109
|
+
reserved = {"descriptor", "reject"} & output.desc.get("props", {}).keys()
|
|
110
|
+
if reserved:
|
|
111
|
+
raise ValueError(f"reserved IL property names: {', '.join(sorted(reserved))}")
|
|
109
112
|
desc = {**output.desc, "source": self.source}
|
|
110
113
|
actions.append(Publish(IL, self.descriptor(device_id), json.dumps(desc, ensure_ascii=False), True))
|
|
111
114
|
elif isinstance(output, Value):
|
|
@@ -360,8 +360,10 @@ class Service:
|
|
|
360
360
|
def _owned_ids(retained: dict, prefix: str, source: str) -> set[str]:
|
|
361
361
|
ids = set()
|
|
362
362
|
for topic, payload in retained.items():
|
|
363
|
-
|
|
364
|
-
|
|
363
|
+
if not topic.startswith(f"{prefix}/") or not topic.endswith("/descriptor"):
|
|
364
|
+
continue
|
|
365
|
+
device_id = topic[len(prefix) + 1:-len("/descriptor")]
|
|
366
|
+
if "/" in device_id or not device_id or device_id.startswith("_"):
|
|
365
367
|
continue
|
|
366
368
|
try:
|
|
367
369
|
desc = json.loads(payload)
|
|
@@ -373,8 +375,9 @@ def _owned_ids(retained: dict, prefix: str, source: str) -> set[str]:
|
|
|
373
375
|
|
|
374
376
|
|
|
375
377
|
def _device_topics(retained: dict, prefix: str, ids: set[str]) -> list[str]:
|
|
376
|
-
descriptors = {f"{prefix}/{i}" for i in ids}
|
|
377
|
-
|
|
378
|
+
descriptors = {f"{prefix}/{i}/descriptor" for i in ids}
|
|
379
|
+
roots = tuple(f"{prefix}/{i}/" for i in ids)
|
|
380
|
+
return sorted((t for t in retained if t.startswith(roots)),
|
|
378
381
|
key=lambda t: (t in descriptors, t))
|
|
379
382
|
|
|
380
383
|
|
|
@@ -382,8 +385,9 @@ async def purge_il(il: Transport, prefix: str, source: str, *, settle: float = 0
|
|
|
382
385
|
"""Take a producer out of IL for good (the integration deleted, not just stopped): every retained topic it left is
|
|
383
386
|
cleared, so IL consumers drop its devices. `stop()` only says `offline`; the descriptors and values stay retained on
|
|
384
387
|
purpose then, since the producer is expected back. What is cleared is found on the broker, not in a Hub: every
|
|
385
|
-
descriptor `<prefix>/<id
|
|
386
|
-
|
|
388
|
+
descriptor `<prefix>/<id>/descriptor` whose `source` is this one
|
|
389
|
+
(devices published by an earlier run included), each retained topic under that device, and the presence
|
|
390
|
+
`<prefix>/_producer/<source>`. Returns the device ids taken out.
|
|
387
391
|
|
|
388
392
|
Refuses (`AnotherProducer`) while a producer still answers on that presence: it would publish them all again."""
|
|
389
393
|
presence = IlTopics(prefix, source).presence
|
{rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/tests/e2e/test_bridge_registration.py
RENAMED
|
@@ -32,7 +32,7 @@ async def settle(runner, bridge_client, *buses):
|
|
|
32
32
|
|
|
33
33
|
|
|
34
34
|
def advertised(il):
|
|
35
|
-
return {t.split("/")[1] for t, m in il.retained.items() if t.count("/") ==
|
|
35
|
+
return {t.split("/")[1] for t, m in il.retained.items() if t.count("/") == 2 and t.endswith("/descriptor") and not t.startswith("il/_") and m.payload}
|
|
36
36
|
|
|
37
37
|
|
|
38
38
|
async def reply(bridge, payload):
|
|
@@ -99,7 +99,7 @@ async def test_devices_can_be_added_and_removed_while_running(chain):
|
|
|
99
99
|
assert set(model.devices) == {"lamp1", "lamp2"}
|
|
100
100
|
runner.remove_device("lamp2")
|
|
101
101
|
await settle(runner, bridge_client, bridge, il)
|
|
102
|
-
assert set(model.devices) == {"lamp1"} and "il/lamp2" not in il.retained
|
|
102
|
+
assert set(model.devices) == {"lamp1"} and "il/lamp2/descriptor" not in il.retained
|
|
103
103
|
assert not [t for t in il.retained if t.startswith("il/lamp2/")] # M-11: values cleared before the descriptor
|
|
104
104
|
|
|
105
105
|
|
|
@@ -46,7 +46,7 @@ async def mqtt_chain(broker):
|
|
|
46
46
|
# leave no retained state behind for the next test
|
|
47
47
|
cleaner = MqttTransport("127.0.0.1", broker, client_id="cleaner")
|
|
48
48
|
await cleaner.connect()
|
|
49
|
-
for topic in ("il/lamp1", "il/lamp1/available", "il/lamp1/switch_led", "il/lamp1/brightness",
|
|
49
|
+
for topic in ("il/lamp1/descriptor", "il/lamp1/available", "il/lamp1/switch_led", "il/lamp1/brightness",
|
|
50
50
|
"il/lamp1/color_temperature", "il/_producer/tuya", "rustuya/error/lamp1", "rustuya/event/state/lamp1"):
|
|
51
51
|
await cleaner.publish(topic, "", 1, True)
|
|
52
52
|
await asyncio.sleep(0.1)
|
|
@@ -27,7 +27,7 @@ async def test_the_daemon_publishes_devices_and_goes_offline_on_sigterm(broker,
|
|
|
27
27
|
|
|
28
28
|
def on_message(message):
|
|
29
29
|
seen[message.topic] = message.payload.decode()
|
|
30
|
-
if seen.get("ild/_producer/tuya") == "online" and "ild/daemon1" in seen:
|
|
30
|
+
if seen.get("ild/_producer/tuya") == "online" and "ild/daemon1/descriptor" in seen:
|
|
31
31
|
online.set()
|
|
32
32
|
if seen.get("ild/_producer/tuya") == "offline":
|
|
33
33
|
offline.set()
|
|
@@ -42,7 +42,7 @@ async def test_the_daemon_publishes_devices_and_goes_offline_on_sigterm(broker,
|
|
|
42
42
|
# Startup includes the producer-presence deadline and the missing
|
|
43
43
|
# bridge-config fallback. Wait for readiness, not 100 polling ticks.
|
|
44
44
|
await asyncio.wait_for(online.wait(), 10)
|
|
45
|
-
assert json.loads(seen["ild/daemon1"])["kind"] == "light"
|
|
45
|
+
assert json.loads(seen["ild/daemon1/descriptor"])["kind"] == "light"
|
|
46
46
|
proc.send_signal(signal.SIGTERM)
|
|
47
47
|
assert await asyncio.to_thread(proc.wait, 10) == 0
|
|
48
48
|
await asyncio.wait_for(offline.wait(), 10)
|
|
@@ -50,7 +50,7 @@ async def test_the_daemon_publishes_devices_and_goes_offline_on_sigterm(broker,
|
|
|
50
50
|
finally:
|
|
51
51
|
if proc.poll() is None:
|
|
52
52
|
proc.kill()
|
|
53
|
-
cleaner = ["ild/daemon1", "ild/daemon1/available", "ild/daemon1/switch_led", "ild/daemon1/brightness",
|
|
53
|
+
cleaner = ["ild/daemon1/descriptor", "ild/daemon1/available", "ild/daemon1/switch_led", "ild/daemon1/brightness",
|
|
54
54
|
"ild/daemon1/color_temperature", "ild/_producer/tuya"]
|
|
55
55
|
for t in cleaner:
|
|
56
56
|
await watcher.publish(t, "", 1, True)
|
|
@@ -82,14 +82,14 @@ async def test_the_daemon_uses_the_bridges_templates_and_follows_the_device_file
|
|
|
82
82
|
raise AssertionError(what)
|
|
83
83
|
|
|
84
84
|
try:
|
|
85
|
-
await until(lambda: "ilw/w1" in seen, "w1 was not published")
|
|
85
|
+
await until(lambda: "ilw/w1/descriptor" in seen, "w1 was not published")
|
|
86
86
|
await t.publish("rb/error/w1", '{"errorCode":0,"errorMsg":"Connection Successful"}', 1, True)
|
|
87
87
|
await t.publish("rb/ev/state/w1", '{"20":true,"22":1000,"23":0}', 1, True)
|
|
88
88
|
await until(lambda: seen.get("ilw/w1/brightness") == "100", "the state on the bridge's own topic was not followed")
|
|
89
89
|
devfile.write_text(json.dumps([lamp("w1"), lamp("w2")])) # manager adds a device
|
|
90
|
-
await until(lambda: "ilw/w2" in seen, "the new device was not picked up")
|
|
90
|
+
await until(lambda: "ilw/w2/descriptor" in seen, "the new device was not picked up")
|
|
91
91
|
devfile.write_text(json.dumps([lamp("w2")])) # and removes one
|
|
92
|
-
await until(lambda: seen.get("ilw/w1") == "" and seen.get("ilw/w1/brightness") == "", "w1 was not removed")
|
|
92
|
+
await until(lambda: seen.get("ilw/w1/descriptor") == "" and seen.get("ilw/w1/brightness") == "", "w1 was not removed")
|
|
93
93
|
finally:
|
|
94
94
|
proc.send_signal(signal.SIGTERM)
|
|
95
95
|
proc.wait(10)
|
|
@@ -101,7 +101,7 @@ def stack(broker, tmp_path_factory):
|
|
|
101
101
|
"devices": "tuyadevices.json", "watch_interval": 0.2}))
|
|
102
102
|
daemon = subprocess.Popen([sys.executable, "-m", "rustuya_local", "run", "--config", str(cfg)],
|
|
103
103
|
stderr=subprocess.PIPE, text=True)
|
|
104
|
-
assert wait(lambda: obs.last.get(f"{prefix}/{DEV}"), 20), "the daemon never published the descriptor"
|
|
104
|
+
assert wait(lambda: obs.last.get(f"{prefix}/{DEV}/descriptor"), 20), "the daemon never published the descriptor"
|
|
105
105
|
st = type("Stack", (), {"obs": obs, "mock": mock, "root": root, "prefix": prefix, "daemon": daemon, "bridge": bridge,
|
|
106
106
|
"devfile": devfile})
|
|
107
107
|
yield st
|
|
@@ -123,7 +123,7 @@ def il(stack, prop):
|
|
|
123
123
|
|
|
124
124
|
|
|
125
125
|
def test_the_device_appears_as_an_il_light_with_its_real_state(stack):
|
|
126
|
-
desc = json.loads(stack.obs.last[f"{stack.prefix}/{DEV}"])
|
|
126
|
+
desc = json.loads(stack.obs.last[f"{stack.prefix}/{DEV}/descriptor"])
|
|
127
127
|
assert desc["kind"] == "light" and desc["source"] == "tuya" and desc["props"]["brightness"]["role"] == "brightness"
|
|
128
128
|
assert stack.obs.last[f"{stack.prefix}/_producer/tuya"] == "online"
|
|
129
129
|
assert wait(lambda: il(stack, "available") == "true"), "the device never became available"
|
|
@@ -48,7 +48,7 @@ def test_a_custom_event_command_and_payload_template_all_work_together(broker, t
|
|
|
48
48
|
daemon = subprocess.Popen([sys.executable, "-m", "rustuya_local", "run", "--config", str(cfg)],
|
|
49
49
|
stderr=subprocess.PIPE, text=True)
|
|
50
50
|
try:
|
|
51
|
-
assert wait(lambda: obs.last.get(f"{prefix}/{DEV}"), 20), "the daemon never published the descriptor"
|
|
51
|
+
assert wait(lambda: obs.last.get(f"{prefix}/{DEV}/descriptor"), 20), "the daemon never published the descriptor"
|
|
52
52
|
assert wait(lambda: obs.last.get(f"{prefix}/{DEV}/switch_led") == "true"
|
|
53
53
|
and obs.last.get(f"{prefix}/{DEV}/brightness") == "49", 20), \
|
|
54
54
|
"state through the custom event/payload templates never reached IL"
|
|
@@ -42,14 +42,14 @@ async def test_the_plugin_runs_the_service_under_the_manager(broker, tmp_path):
|
|
|
42
42
|
await answer_status(w, "rmp", ["mp1", "mp2"])
|
|
43
43
|
task = asyncio.ensure_future(reg.services[0]())
|
|
44
44
|
try:
|
|
45
|
-
await until(lambda: "ilmp/mp1" in seen, "not published")
|
|
45
|
+
await until(lambda: "ilmp/mp1/descriptor" in seen, "not published")
|
|
46
46
|
assert seen["ilmp/_producer/tuya"] == "online"
|
|
47
47
|
await until(lambda: (state.get_plugin_data("rustuya-local") or {}).get("devices"), "no status", n=300)
|
|
48
48
|
status = state.get_plugin_data("rustuya-local")
|
|
49
49
|
assert [d["id"] for d in status["devices"]] == ["mp1"]
|
|
50
50
|
|
|
51
51
|
await state.set_cloud({"mp1": Device.from_dict(lamp("mp1")), "mp2": Device.from_dict(lamp("mp2"))})
|
|
52
|
-
await until(lambda: "ilmp/mp2" in seen, "mp2 not followed")
|
|
52
|
+
await until(lambda: "ilmp/mp2/descriptor" in seen, "mp2 not followed")
|
|
53
53
|
assert (tmp_path / "rustuya-local" / "custom_converters").is_dir()
|
|
54
54
|
finally:
|
|
55
55
|
task.cancel()
|
|
@@ -181,7 +181,7 @@ async def test_a_settings_change_restarts_the_service_and_bad_settings_wait_for_
|
|
|
181
181
|
assert not task.done() # waits for a fix, no crash-and-retry
|
|
182
182
|
api.save_settings(data, {"il": {"prefix": "ilr1"}, "options": {"pack": False}})
|
|
183
183
|
await plugin.restart()
|
|
184
|
-
await until(lambda: seen.get("ilr1/_producer/tuya") == "online" and "ilr1/mp1" in seen, "not started")
|
|
184
|
+
await until(lambda: seen.get("ilr1/_producer/tuya") == "online" and "ilr1/mp1/descriptor" in seen, "not started")
|
|
185
185
|
|
|
186
186
|
previous = plugin.service
|
|
187
187
|
(data / "settings.json").write_text('{"options":{"pack":"false"}}')
|
|
@@ -195,9 +195,9 @@ async def test_a_settings_change_restarts_the_service_and_bad_settings_wait_for_
|
|
|
195
195
|
|
|
196
196
|
api.save_settings(data, {"il": {"prefix": "ilr2"}, "options": {"pack": False}})
|
|
197
197
|
await plugin.restart()
|
|
198
|
-
await until(lambda: seen.get("ilr2/_producer/tuya") == "online" and "ilr2/mp1" in seen, "not restarted")
|
|
198
|
+
await until(lambda: seen.get("ilr2/_producer/tuya") == "online" and "ilr2/mp1/descriptor" in seen, "not restarted")
|
|
199
199
|
assert seen["ilr1/_producer/tuya"] == "" # the old namespace was retired
|
|
200
|
-
assert seen["ilr1/mp1"] == ""
|
|
200
|
+
assert seen["ilr1/mp1/descriptor"] == ""
|
|
201
201
|
finally:
|
|
202
202
|
task.cancel()
|
|
203
203
|
with pytest.raises(asyncio.CancelledError):
|
|
@@ -248,7 +248,7 @@ async def test_another_producer_is_shown_and_the_plugin_starts_once_it_is_gone(b
|
|
|
248
248
|
task.cancel()
|
|
249
249
|
with pytest.raises(asyncio.CancelledError):
|
|
250
250
|
await task
|
|
251
|
-
for topic in ("ilo/_producer/tuya", "ilo/mp1"):
|
|
251
|
+
for topic in ("ilo/_producer/tuya", "ilo/mp1/descriptor"):
|
|
252
252
|
await w.publish(topic, "", 1, True)
|
|
253
253
|
await asyncio.sleep(0.2)
|
|
254
254
|
await w.close()
|
|
@@ -32,15 +32,15 @@ async def _retained(t: InProcessTransport, topic_filter: str) -> dict[str, str]:
|
|
|
32
32
|
|
|
33
33
|
async def test_purge_il_clears_one_source_and_leaves_the_others():
|
|
34
34
|
t = InProcessTransport()
|
|
35
|
-
for topic, payload in {"il/a": _desc("a", "tuya"), "il/a/power": "true", "il/a/brightness": "40",
|
|
36
|
-
"il/b": _desc("b", "tuya"), "il/z": _desc("z", "zigbee"), "il/z/power": "false",
|
|
35
|
+
for topic, payload in {"il/a": _desc("a", "tuya"), "il/a/descriptor": _desc("a", "tuya"), "il/a/power": "true", "il/a/brightness": "40",
|
|
36
|
+
"il/b/descriptor": _desc("b", "tuya"), "il/z/descriptor": _desc("z", "zigbee"), "il/z/power": "false",
|
|
37
37
|
"il/_producer/tuya": "offline", "il/_producer/zigbee": "online"}.items():
|
|
38
38
|
await t.publish(topic, payload, 1, True)
|
|
39
39
|
|
|
40
40
|
ids = await purge_il(t, "il", "tuya", settle=0.05)
|
|
41
41
|
|
|
42
42
|
assert ids == ["a", "b"]
|
|
43
|
-
assert await _retained(t, "il/#") == {"il/z": _desc("z", "zigbee"), "il/z/power": "false",
|
|
43
|
+
assert await _retained(t, "il/#") == {"il/a": _desc("a", "tuya"), "il/z/descriptor": _desc("z", "zigbee"), "il/z/power": "false",
|
|
44
44
|
"il/_producer/zigbee": "online"}
|
|
45
45
|
|
|
46
46
|
|
|
@@ -51,11 +51,11 @@ async def test_purge_il_refuses_while_the_producer_answers():
|
|
|
51
51
|
runner = Runner(Hub([], il=IlTopics("il", "tuya")), t)
|
|
52
52
|
await runner.start()
|
|
53
53
|
await runner.drain()
|
|
54
|
-
await t.publish("il/a", _desc("a", "tuya"), 1, True)
|
|
54
|
+
await t.publish("il/a/descriptor", _desc("a", "tuya"), 1, True)
|
|
55
55
|
try:
|
|
56
56
|
with pytest.raises(AnotherProducer):
|
|
57
57
|
await purge_il(t, "il", "tuya", settle=0.05)
|
|
58
|
-
assert "il/a" in await _retained(t, "il/#")
|
|
58
|
+
assert "il/a/descriptor" in await _retained(t, "il/#")
|
|
59
59
|
finally:
|
|
60
60
|
await runner.stop()
|
|
61
61
|
|
|
@@ -86,7 +86,7 @@ def test_the_purge_command_clears_the_daemons_devices(broker, tmp_path, capsys):
|
|
|
86
86
|
c.connect("127.0.0.1", broker)
|
|
87
87
|
c.loop_start()
|
|
88
88
|
try:
|
|
89
|
-
for topic, payload in (("ilp/a", _desc("a", "tuya")), ("ilp/a/power", "true"),
|
|
89
|
+
for topic, payload in (("ilp/a/descriptor", _desc("a", "tuya")), ("ilp/a/power", "true"),
|
|
90
90
|
("ilp/_producer/tuya", "offline")):
|
|
91
91
|
c.publish(topic, payload, 1, True).wait_for_publish(5)
|
|
92
92
|
config = tmp_path / "config.json"
|
|
@@ -66,7 +66,7 @@ async def test_devices_the_bridge_holds_reach_il_and_stop_goes_offline(tmp_path,
|
|
|
66
66
|
await service.start()
|
|
67
67
|
await settle(service, t)
|
|
68
68
|
assert t.wills == [("il/_producer/tuya", "offline", 1, True)]
|
|
69
|
-
assert "il/lamp1" in t.il.retained and "il/lamp2" not in t.il.retained # lamp2: not on the bridge
|
|
69
|
+
assert "il/lamp1/descriptor" in t.il.retained and "il/lamp2/descriptor" not in t.il.retained # lamp2: not on the bridge
|
|
70
70
|
await service.stop()
|
|
71
71
|
await service.stop() # idempotent
|
|
72
72
|
assert t.il.retained["il/_producer/tuya"].payload == "offline"
|
|
@@ -88,7 +88,7 @@ async def test_an_overrides_directory_is_applied_and_followed(tmp_path, monkeypa
|
|
|
88
88
|
hub_options={"overrides": {"lamp1": {"device": {"label": "Inline"}}}})
|
|
89
89
|
await service.start()
|
|
90
90
|
await settle(service, t)
|
|
91
|
-
desc = json.loads(t.il.retained["il/lamp1"].payload)
|
|
91
|
+
desc = json.loads(t.il.retained["il/lamp1/descriptor"].payload)
|
|
92
92
|
assert "glow" in desc["props"] and desc["model"] == "Desk lamp" and desc["label"] == "Inline"
|
|
93
93
|
|
|
94
94
|
await t.bridge.publish("rustuya/error/lamp1", '{"errorCode":0}', 0, True)
|
|
@@ -99,7 +99,7 @@ async def test_an_overrides_directory_is_applied_and_followed(tmp_path, monkeypa
|
|
|
99
99
|
(conv / "10.json").unlink() # followed: the converter goes away
|
|
100
100
|
assert service.override_watcher.check()
|
|
101
101
|
await settle(service, t)
|
|
102
|
-
assert "glow" not in json.loads(t.il.retained["il/lamp1"].payload)["props"]
|
|
102
|
+
assert "glow" not in json.loads(t.il.retained["il/lamp1/descriptor"].payload)["props"]
|
|
103
103
|
await service.stop()
|
|
104
104
|
|
|
105
105
|
|
|
@@ -123,7 +123,7 @@ async def test_a_cover_setting_written_through_il_is_saved_and_applied(tmp_path,
|
|
|
123
123
|
await t.bridge.publish("rustuya/error/cur1", '{"errorCode":0}', 0, True)
|
|
124
124
|
await t.bridge.publish("rustuya/event/state/cur1", '{"3":30}', 0, True)
|
|
125
125
|
await settle(service, t)
|
|
126
|
-
props = json.loads(t.il.retained["il/cur1"].payload)["props"]
|
|
126
|
+
props = json.loads(t.il.retained["il/cur1/descriptor"].payload)["props"]
|
|
127
127
|
assert {"cover_invert_position", "cover_invert_set_position", "cover_state_source"} <= set(props)
|
|
128
128
|
assert "cover_infer_motion" not in props # replaced by the state-source selector in 0.3.16
|
|
129
129
|
assert "cover_invert_control" not in props # the target position opens and closes it
|
|
@@ -151,7 +151,7 @@ async def test_a_cover_setting_written_through_il_is_saved_and_applied(tmp_path,
|
|
|
151
151
|
connect_il=t.connect_il)
|
|
152
152
|
await service.start()
|
|
153
153
|
await settle(service, t)
|
|
154
|
-
assert not [p for p in json.loads(t.il.retained["il/cur1"].payload)["props"] if p.startswith("cover_")]
|
|
154
|
+
assert not [p for p in json.loads(t.il.retained["il/cur1/descriptor"].payload)["props"] if p.startswith("cover_")]
|
|
155
155
|
await service.stop()
|
|
156
156
|
|
|
157
157
|
|
|
@@ -282,19 +282,23 @@ async def test_restart_removes_only_old_owned_devices():
|
|
|
282
282
|
ids = ["lamp1", "lamp2"]
|
|
283
283
|
await _answer_status(t.bridge, ids)
|
|
284
284
|
# Another producer's data must survive reconciliation.
|
|
285
|
-
await t.il.publish("il/other", '{"source":"other"}', 1, True)
|
|
285
|
+
await t.il.publish("il/other/descriptor", '{"source":"other"}', 1, True)
|
|
286
286
|
first = Service(Settings(devices=[lamp(i) for i in ids]),
|
|
287
287
|
connect_bridge=t.connect_bridge, connect_il=t.connect_il)
|
|
288
288
|
await first.start()
|
|
289
289
|
await settle(first, t)
|
|
290
290
|
await first.stop()
|
|
291
|
+
# Simulate retained descriptors from a producer using the legacy layout.
|
|
292
|
+
for device_id in ids:
|
|
293
|
+
await t.il.publish(f"il/{device_id}", json.dumps({"id": device_id, "source": "tuya"}), 1, True)
|
|
291
294
|
ids.remove("lamp2")
|
|
292
295
|
second = Service(Settings(devices=[lamp("lamp1"), lamp("lamp2")]),
|
|
293
296
|
connect_bridge=t.connect_bridge, connect_il=t.connect_il)
|
|
294
297
|
await second.start()
|
|
295
298
|
await settle(second, t)
|
|
296
|
-
assert "il/lamp1" in t.il.retained and "il/other" in t.il.retained
|
|
297
|
-
assert
|
|
299
|
+
assert "il/lamp1/descriptor" in t.il.retained and "il/other/descriptor" in t.il.retained
|
|
300
|
+
assert "il/lamp1" in t.il.retained and "il/lamp2" in t.il.retained
|
|
301
|
+
assert not any(topic == "il/lamp2/descriptor" or topic.startswith("il/lamp2/") for topic in t.il.retained)
|
|
298
302
|
await second.stop()
|
|
299
303
|
|
|
300
304
|
|
|
@@ -95,8 +95,8 @@ async def test_setup_publishes_and_unload_goes_offline(hass, broker, tmp_path, s
|
|
|
95
95
|
|
|
96
96
|
await until(lambda: "il/_producer/tuya" in watcher.last)
|
|
97
97
|
assert watcher.last["il/_producer/tuya"] == "online"
|
|
98
|
-
await until(lambda: "il/lamp1" in watcher.last)
|
|
99
|
-
assert json.loads(watcher.last["il/lamp1"])["kind"] == "light"
|
|
98
|
+
await until(lambda: "il/lamp1/descriptor" in watcher.last)
|
|
99
|
+
assert json.loads(watcher.last["il/lamp1/descriptor"])["kind"] == "light"
|
|
100
100
|
|
|
101
101
|
# the runner asked the bridge for the device's state, exactly as the standalone daemon does
|
|
102
102
|
watcher.publish("rustuya/error/lamp1", '{"errorCode":0,"errorMsg":"Connection Successful"}', retain=True)
|
|
@@ -141,7 +141,7 @@ async def test_setup_is_retried_while_another_producer_runs(hass, broker, tmp_pa
|
|
|
141
141
|
finally:
|
|
142
142
|
await runner.stop()
|
|
143
143
|
await t.publish("il/_producer/tuya", "", 1, True)
|
|
144
|
-
await t.publish("il/lamp1", "", 1, True)
|
|
144
|
+
await t.publish("il/lamp1/descriptor", "", 1, True)
|
|
145
145
|
await t.close()
|
|
146
146
|
|
|
147
147
|
|
|
@@ -191,7 +191,7 @@ async def test_driving_the_first_device_does_no_blocking_io_in_the_event_loop(ha
|
|
|
191
191
|
try:
|
|
192
192
|
assert await hass.config_entries.async_setup(entry.entry_id)
|
|
193
193
|
await hass.async_block_till_done()
|
|
194
|
-
await until(lambda: "il/lamp1" in watcher.last)
|
|
194
|
+
await until(lambda: "il/lamp1/descriptor" in watcher.last)
|
|
195
195
|
assert in_loop == []
|
|
196
196
|
assert await hass.config_entries.async_unload(entry.entry_id)
|
|
197
197
|
await hass.async_block_till_done()
|
|
@@ -213,25 +213,25 @@ async def test_deleting_the_entry_takes_its_devices_out_of_il(hass, broker, tmp_
|
|
|
213
213
|
entry.add_to_hass(hass)
|
|
214
214
|
watcher = Watcher(broker, registered=("lamp1",))
|
|
215
215
|
try:
|
|
216
|
-
watcher.publish("il/stale1", json.dumps({"il": 1, "id": "stale1", "source": "tuya", "props": {}}), retain=True)
|
|
216
|
+
watcher.publish("il/stale1/descriptor", json.dumps({"il": 1, "id": "stale1", "source": "tuya", "props": {}}), retain=True)
|
|
217
217
|
watcher.publish("il/stale1/power", "true", retain=True)
|
|
218
|
-
watcher.publish("il/zb1", json.dumps({"il": 1, "id": "zb1", "source": "zigbee", "props": {}}), retain=True)
|
|
218
|
+
watcher.publish("il/zb1/descriptor", json.dumps({"il": 1, "id": "zb1", "source": "zigbee", "props": {}}), retain=True)
|
|
219
219
|
watcher.publish("il/zb1/power", "true", retain=True)
|
|
220
220
|
assert await hass.config_entries.async_setup(entry.entry_id)
|
|
221
221
|
await hass.async_block_till_done()
|
|
222
|
-
await until(lambda: "il/lamp1" in watcher.last)
|
|
222
|
+
await until(lambda: "il/lamp1/descriptor" in watcher.last)
|
|
223
223
|
watcher.publish("rustuya/event/state/lamp1", '{"20":true,"22":1000,"23":0}', retain=True)
|
|
224
224
|
await until(lambda: watcher.last.get("il/lamp1/brightness") == "100")
|
|
225
225
|
|
|
226
226
|
await hass.config_entries.async_remove(entry.entry_id)
|
|
227
227
|
await hass.async_block_till_done()
|
|
228
228
|
|
|
229
|
-
for topic in ("il/lamp1", "il/lamp1/brightness", "il/stale1", "il/stale1/power", "il/_producer/tuya"):
|
|
229
|
+
for topic in ("il/lamp1/descriptor", "il/lamp1/brightness", "il/stale1/descriptor", "il/stale1/power", "il/_producer/tuya"):
|
|
230
230
|
try:
|
|
231
231
|
await until(lambda t=topic: watcher.last.get(t) == "")
|
|
232
232
|
except AssertionError:
|
|
233
233
|
raise AssertionError((topic, dict(watcher.last)))
|
|
234
|
-
assert watcher.last["il/zb1/power"] == "true" and json.loads(watcher.last["il/zb1"])["source"] == "zigbee"
|
|
234
|
+
assert watcher.last["il/zb1/power"] == "true" and json.loads(watcher.last["il/zb1/descriptor"])["source"] == "zigbee"
|
|
235
235
|
# nothing is left retained for it: a fresh subscriber sees none of those topics
|
|
236
236
|
late = Watcher(broker)
|
|
237
237
|
await asyncio.sleep(0.5)
|
|
@@ -344,7 +344,7 @@ async def test_send_command_writes_a_raw_dp_and_refuses_a_lock(hass, broker, tmp
|
|
|
344
344
|
try:
|
|
345
345
|
assert await hass.config_entries.async_setup(entry.entry_id)
|
|
346
346
|
await hass.async_block_till_done()
|
|
347
|
-
await until(lambda: "il/lamp1" in watcher.last and "il/lock1" in watcher.last)
|
|
347
|
+
await until(lambda: "il/lamp1/descriptor" in watcher.last and "il/lock1/descriptor" in watcher.last)
|
|
348
348
|
|
|
349
349
|
await hass.services.async_call(DOMAIN, "send_command", {"device_id": "lamp1", "dp": 20, "value": False},
|
|
350
350
|
blocking=True)
|
|
@@ -75,7 +75,7 @@ async def running():
|
|
|
75
75
|
async def test_start_publishes_presence_and_descriptors_and_values_flow_both_ways(running):
|
|
76
76
|
il, runner, _, commands = running
|
|
77
77
|
assert il.retained["il/_producer/tuya"].payload == "online"
|
|
78
|
-
assert json.loads(il.retained["il/lamp1"].payload)["kind"] == "light"
|
|
78
|
+
assert json.loads(il.retained["il/lamp1/descriptor"].payload)["kind"] == "light"
|
|
79
79
|
|
|
80
80
|
runner.on_bridge_message("lamp1", Connected())
|
|
81
81
|
runner.on_bridge_message("lamp1", Message("state", {"20": True, "22": 1000, "23": 0}))
|
|
@@ -163,7 +163,7 @@ async def test_sync_adds_changes_and_removes_and_clears_before_removing(running)
|
|
|
163
163
|
assert done == {"added": ["new"], "changed": ["edit"], "removed": ["gone"], "failed": []}
|
|
164
164
|
await runner.drain()
|
|
165
165
|
assert set(hub.records) == {"lamp1", "edit", "new"} and not [t for t in il.retained if t.startswith("il/gone")]
|
|
166
|
-
assert json.loads(il.retained["il/edit"].payload)["label"] == "Renamed"
|
|
166
|
+
assert json.loads(il.retained["il/edit/descriptor"].payload)["label"] == "Renamed"
|
|
167
167
|
bad = lamp("bad")
|
|
168
168
|
bad["id"] = "_reserved"
|
|
169
169
|
assert runner.sync_devices([lamp("lamp1"), bad])["failed"] == ["_reserved"]
|
|
@@ -175,10 +175,10 @@ async def test_a_device_an_override_refused_is_driven_once_the_overrides_are_fix
|
|
|
175
175
|
assert runner.sync_devices([lamp("lamp1"), lamp("late")])["failed"] == ["late"] # it has no dp 99 to rename
|
|
176
176
|
runner.reload({"late": {"dp": {"98": {"code": "gone"}}}}) # still refused
|
|
177
177
|
await runner.drain()
|
|
178
|
-
assert "late" not in hub.records and "il/late" not in il.retained
|
|
178
|
+
assert "late" not in hub.records and "il/late/descriptor" not in il.retained
|
|
179
179
|
runner.reload({"late": {"device": {"label": "Late"}}}) # fixed: driven now, without a device-list change
|
|
180
180
|
await runner.drain()
|
|
181
|
-
assert "late" in hub.records and json.loads(il.retained["il/late"].payload)["label"] == "Late"
|
|
181
|
+
assert "late" in hub.records and json.loads(il.retained["il/late/descriptor"].payload)["label"] == "Late"
|
|
182
182
|
|
|
183
183
|
|
|
184
184
|
async def test_overrides_the_engine_refuses_are_refused_whole_and_the_ones_in_effect_stay(running):
|
|
@@ -188,7 +188,7 @@ async def test_overrides_the_engine_refuses_are_refused_whole_and_the_ones_in_ef
|
|
|
188
188
|
with pytest.raises(OverrideError): # any block, even one for no device here
|
|
189
189
|
runner.reload({"lamp1": {"device": {"label": "Other"}}, "late": {"bogus": True}})
|
|
190
190
|
await runner.drain()
|
|
191
|
-
assert json.loads(il.retained["il/lamp1"].payload)["label"] == "Desk"
|
|
191
|
+
assert json.loads(il.retained["il/lamp1/descriptor"].payload)["label"] == "Desk"
|
|
192
192
|
|
|
193
193
|
|
|
194
194
|
# ---- the device file --------------------------------------------------------------------------------------------------
|
|
@@ -299,7 +299,7 @@ async def test_the_override_watcher_reloads_the_hub(running, tmp_path):
|
|
|
299
299
|
(tmp_path / "a.json").write_text(json.dumps({"overrides": {"p": {"converters": {"declarative": {**LIT, "word": "bright"}}}}}))
|
|
300
300
|
assert watcher.check()
|
|
301
301
|
await runner.drain()
|
|
302
|
-
desc = json.loads(il.retained["il/lamp1"].payload)
|
|
302
|
+
desc = json.loads(il.retained["il/lamp1/descriptor"].payload)
|
|
303
303
|
assert "lit" in desc["props"] and desc["label"] == "Desk"
|
|
304
304
|
runner.on_bridge_message("lamp1", Connected())
|
|
305
305
|
runner.on_bridge_message("lamp1", Message("state", {"20": True}))
|
|
@@ -309,16 +309,16 @@ async def test_the_override_watcher_reloads_the_hub(running, tmp_path):
|
|
|
309
309
|
(tmp_path / "a.json").write_text(json.dumps({"overrides": {"p": {"converters": {"nope": {}}}}})) # refused: left out
|
|
310
310
|
assert watcher.check()
|
|
311
311
|
await runner.drain()
|
|
312
|
-
assert "lit" not in json.loads(il.retained["il/lamp1"].payload)["props"]
|
|
312
|
+
assert "lit" not in json.loads(il.retained["il/lamp1/descriptor"].payload)["props"]
|
|
313
313
|
assert [w.split(":")[0] for w in watcher.load().warnings] == ["a.json"]
|
|
314
314
|
(tmp_path / "a.json").write_text(json.dumps({"overrides": {"p": {"converters": {"declarative": LIT}}}}))
|
|
315
315
|
assert watcher.check()
|
|
316
316
|
await runner.drain()
|
|
317
|
-
assert "lit" in json.loads(il.retained["il/lamp1"].payload)["props"]
|
|
317
|
+
assert "lit" in json.loads(il.retained["il/lamp1/descriptor"].payload)["props"]
|
|
318
318
|
(tmp_path / "a.json").unlink()
|
|
319
319
|
assert watcher.check()
|
|
320
320
|
await runner.drain()
|
|
321
|
-
assert "lit" not in json.loads(il.retained["il/lamp1"].payload)["props"]
|
|
321
|
+
assert "lit" not in json.loads(il.retained["il/lamp1/descriptor"].payload)["props"]
|
|
322
322
|
|
|
323
323
|
|
|
324
324
|
# ---- paho against a real broker --------------------------------------------------------------------------------------
|
|
@@ -371,7 +371,7 @@ async def test_paho_transport_runs_the_runner_survives_a_dropped_connection_and_
|
|
|
371
371
|
runner = Runner(hub, il_side, on_bridge_command=commands.append)
|
|
372
372
|
await runner.start()
|
|
373
373
|
try:
|
|
374
|
-
await until(lambda: seen.get("il/_producer/tuya") == "online" and "il/lamp1" in seen)
|
|
374
|
+
await until(lambda: seen.get("il/_producer/tuya") == "online" and "il/lamp1/descriptor" in seen)
|
|
375
375
|
runner.on_bridge_message("lamp1", Connected())
|
|
376
376
|
await until(lambda: BridgeCommand("lamp1", "get") in commands)
|
|
377
377
|
runner.on_bridge_message("lamp1", Message("state", {"20": True, "22": 1000, "23": 0}))
|
|
@@ -15,7 +15,7 @@ def test_bridge_to_il_and_back():
|
|
|
15
15
|
hub = Hub([light(), curtain()])
|
|
16
16
|
start = topics(hub.start(), "il")
|
|
17
17
|
assert start["il/_producer/tuya"].payload == "online" and start["il/_producer/tuya"].retain
|
|
18
|
-
assert json.loads(start["il/lamp1"].payload)["kind"] == "light" and start["il/lamp1"].retain
|
|
18
|
+
assert json.loads(start["il/lamp1/descriptor"].payload)["kind"] == "light" and start["il/lamp1/descriptor"].retain
|
|
19
19
|
assert start["il/lamp1/available"].payload == "false"
|
|
20
20
|
|
|
21
21
|
# bridge says connected -> a `get` goes back to the bridge
|
|
@@ -85,7 +85,7 @@ def test_reload_overrides_republishes_and_clears_removed_props():
|
|
|
85
85
|
pubs = hub.reload({"cur1": {"props": {"stop": {"hide": True}}, "device": {"label": "Bedroom"}}})
|
|
86
86
|
by = topics(pubs)
|
|
87
87
|
assert by["il/cur1/stop"].payload == "" and by["il/cur1/stop"].retain # M-11
|
|
88
|
-
assert json.loads(by["il/cur1"].payload)["label"] == "Bedroom" and "stop" not in json.loads(by["il/cur1"].payload)["props"]
|
|
88
|
+
assert json.loads(by["il/cur1/descriptor"].payload)["label"] == "Bedroom" and "stop" not in json.loads(by["il/cur1/descriptor"].payload)["props"]
|
|
89
89
|
assert by["il/cur1/position"].payload == "30" # worked out again from the last dps
|
|
90
90
|
assert BridgeCommand("cur1", "get") not in pubs # which the host has: nothing to ask
|
|
91
91
|
with pytest.raises(OverrideError):
|
|
@@ -114,7 +114,7 @@ def test_a_seeded_device_goes_out_up_with_its_values_and_never_false():
|
|
|
114
114
|
hub = Hub([])
|
|
115
115
|
out = hub.set_device(light(), [Connected(), Message("state", {"20": True, "22": 1000, "23": 0})])
|
|
116
116
|
order = [p.topic for p in out if isinstance(p, Publish)]
|
|
117
|
-
assert order[0] == "il/lamp1" # R-2: the descriptor first
|
|
117
|
+
assert order[0] == "il/lamp1/descriptor" # R-2: the descriptor first
|
|
118
118
|
pubs = topics(out)
|
|
119
119
|
assert pubs["il/lamp1/available"].payload == "true" and pubs["il/lamp1/brightness"].payload == "100"
|
|
120
120
|
assert [p for p in out if isinstance(p, Publish) and p.topic == "il/lamp1/available"] == [pubs["il/lamp1/available"]]
|
|
@@ -124,7 +124,7 @@ def test_a_seeded_device_goes_out_up_with_its_values_and_never_false():
|
|
|
124
124
|
assert topics(linked_only)["il/lamp1/available"].payload == "false" and BridgeCommand("lamp1", "get") in linked_only
|
|
125
125
|
down = Hub([]).set_device(light(), [Disconnected()])
|
|
126
126
|
assert topics(down)["il/lamp1/available"].payload == "false" and BridgeCommand("lamp1", "get") not in down
|
|
127
|
-
assert next(p.topic for p in down if isinstance(p, Publish)) == "il/lamp1"
|
|
127
|
+
assert next(p.topic for p in down if isinstance(p, Publish)) == "il/lamp1/descriptor"
|
|
128
128
|
# a sub-device has no link state of its own, only its retained snapshot: up with its values, and no `get`
|
|
129
129
|
sub = Hub([]).set_device(light(), [Message("state", {"20": False, "22": 10, "23": 0})])
|
|
130
130
|
assert topics(sub)["il/lamp1/available"].payload == "true" and not any(isinstance(p, BridgeCommand) for p in sub)
|
|
@@ -135,7 +135,7 @@ def test_set_device_adds_replaces_and_remove_clears_first():
|
|
|
135
135
|
hub.start()
|
|
136
136
|
cid = curtain()["id"]
|
|
137
137
|
added = topics(hub.set_device(curtain())) # runtime add: descriptor out, retained
|
|
138
|
-
assert added[f"il/{cid}"].retain and json.loads(added[f"il/{cid}"].payload)["kind"] == "cover"
|
|
138
|
+
assert added[f"il/{cid}/descriptor"].retain and json.loads(added[f"il/{cid}/descriptor"].payload)["kind"] == "cover"
|
|
139
139
|
assert cid in hub.drivers
|
|
140
140
|
|
|
141
141
|
# replacing a known device that lost a property clears that property before the new descriptor (M-11)
|
|
@@ -144,12 +144,12 @@ def test_set_device_adds_replaces_and_remove_clears_first():
|
|
|
144
144
|
small["local_strategy"].pop("23")
|
|
145
145
|
out = hub.set_device(small)
|
|
146
146
|
order = [p.topic for p in out]
|
|
147
|
-
assert order == ["il/lamp1/color_temperature", "il/lamp1", "il/lamp1/available"]
|
|
147
|
+
assert order == ["il/lamp1/color_temperature", "il/lamp1/descriptor", "il/lamp1/available"]
|
|
148
148
|
assert out[0].payload == "" and out[0].retain
|
|
149
149
|
|
|
150
150
|
# removal: every retained value cleared, then the descriptor last, and the device is gone
|
|
151
151
|
out = hub.remove_device("lamp1")
|
|
152
|
-
assert out[-1].topic == "il/lamp1" and out[-1].payload == "" and out[-1].retain
|
|
152
|
+
assert out[-1].topic == "il/lamp1/descriptor" and out[-1].payload == "" and out[-1].retain
|
|
153
153
|
assert all(p.payload == "" and p.retain for p in out) and len(out) > 2
|
|
154
154
|
assert hub.on_bridge_message(2, "lamp1", Message("state", {"20": True}), retained=True) == [] and hub.remove_device("lamp1") == []
|
|
155
155
|
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Descriptor routing and reserved transport topics."""
|
|
2
|
+
import pytest
|
|
3
|
+
from tuya_interpreter import Descriptor
|
|
4
|
+
|
|
5
|
+
from rustuya_local.host.il import IlTopics
|
|
6
|
+
from rustuya_local.service import _device_topics, _owned_ids
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@pytest.mark.parametrize("prop", ["descriptor", "reject"])
|
|
10
|
+
def test_descriptor_refuses_properties_colliding_with_transport_topics(prop):
|
|
11
|
+
with pytest.raises(ValueError, match="reserved IL property"):
|
|
12
|
+
IlTopics().actions("d", [Descriptor({"id": "d", "props": {prop: {"type": "string"}}})])
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_cleanup_only_recognizes_descriptor_topics_under_a_nested_prefix():
|
|
16
|
+
retained = {
|
|
17
|
+
"home/il/a": '{"source":"tuya"}',
|
|
18
|
+
"home/il/a/descriptor": '{"source":"tuya"}',
|
|
19
|
+
"home/il/a/power": 'true',
|
|
20
|
+
"home/il/old": '{"source":"tuya"}',
|
|
21
|
+
"home/il/b/descriptor": '{"source":"other"}',
|
|
22
|
+
"else/il/a/descriptor": '{"source":"tuya"}',
|
|
23
|
+
}
|
|
24
|
+
assert _owned_ids(retained, "home/il", "tuya") == {"a"}
|
|
25
|
+
assert _device_topics(retained, "home/il", {"a"}) == [
|
|
26
|
+
"home/il/a/power", "home/il/a/descriptor",
|
|
27
|
+
]
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
### Changed
|
|
2
|
-
- Wait for live bridge acknowledgements and the panel's registry refresh instead of sleeping after device commands.
|
|
3
|
-
- Verify embedded bridge startup through a live MQTT config publication, supervise its native async task, and
|
|
4
|
-
retain ownership when shutdown times out.
|
|
5
|
-
- Wait for producer presence with an event and an explicit timeout instead of repeated short sleeps.
|
|
6
|
-
- Use manager state-change notifications for wizard progress and cloud-fetch completion; remove status polling.
|
|
7
|
-
- Use manager registry readiness instead of patching its private methods; require rustuya-manager 0.2.3.
|
|
8
|
-
- Create session directories with a regular loop instead of a discarded list comprehension.
|
|
9
|
-
- Wait for daemon readiness events in the subprocess test, allowing both startup timeout stages to finish.
|
|
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.dev10 → rustuya_local-0.1.0.dev12}/custom_components/rustuya/__init__.py
RENAMED
|
File without changes
|
{rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/custom_components/rustuya/brand/icon.png
RENAMED
|
File without changes
|
{rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/custom_components/rustuya/brand/icon.svg
RENAMED
|
File without changes
|
{rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/custom_components/rustuya/brand/icon@2x.png
RENAMED
|
File without changes
|
{rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/custom_components/rustuya/brand/logo.png
RENAMED
|
File without changes
|
{rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/custom_components/rustuya/brand/logo.svg
RENAMED
|
File without changes
|
{rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/custom_components/rustuya/brand/logo@2x.png
RENAMED
|
File without changes
|
|
File without changes
|
{rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/custom_components/rustuya/bridge_sync.py
RENAMED
|
File without changes
|
|
File without changes
|
{rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/custom_components/rustuya/config_flow.py
RENAMED
|
File without changes
|
|
File without changes
|
{rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/custom_components/rustuya/manager_session.py
RENAMED
|
File without changes
|
|
File without changes
|
{rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/custom_components/rustuya/panel_bridge.py
RENAMED
|
File without changes
|
{rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/custom_components/rustuya/services.yaml
RENAMED
|
File without changes
|
{rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/custom_components/rustuya/strings.json
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
|
|
File without changes
|
|
File without changes
|
{rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/src/rustuya_local/manager_plugin/__init__.py
RENAMED
|
File without changes
|
{rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/src/rustuya_local/manager_plugin/api.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{rustuya_local-0.1.0.dev10 → rustuya_local-0.1.0.dev12}/tests/e2e/test_full_stack_devices.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
|