rustuya-manager 0.1.0__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_manager-0.1.0/PKG-INFO +20 -0
- rustuya_manager-0.1.0/README.md +302 -0
- rustuya_manager-0.1.0/pyproject.toml +76 -0
- rustuya_manager-0.1.0/setup.cfg +4 -0
- rustuya_manager-0.1.0/src/rustuya_manager/__init__.py +12 -0
- rustuya_manager-0.1.0/src/rustuya_manager/__main__.py +6 -0
- rustuya_manager-0.1.0/src/rustuya_manager/catalog.py +443 -0
- rustuya_manager-0.1.0/src/rustuya_manager/cli.py +821 -0
- rustuya_manager-0.1.0/src/rustuya_manager/cloud.py +82 -0
- rustuya_manager-0.1.0/src/rustuya_manager/data/plugin-sources.json +10 -0
- rustuya_manager-0.1.0/src/rustuya_manager/data/plugins.json +15 -0
- rustuya_manager-0.1.0/src/rustuya_manager/diff.py +58 -0
- rustuya_manager-0.1.0/src/rustuya_manager/models.py +130 -0
- rustuya_manager-0.1.0/src/rustuya_manager/mqtt.py +1152 -0
- rustuya_manager-0.1.0/src/rustuya_manager/plugins.py +704 -0
- rustuya_manager-0.1.0/src/rustuya_manager/requirements.py +235 -0
- rustuya_manager-0.1.0/src/rustuya_manager/scan.py +141 -0
- rustuya_manager-0.1.0/src/rustuya_manager/state.py +422 -0
- rustuya_manager-0.1.0/src/rustuya_manager/static/api.js +137 -0
- rustuya_manager-0.1.0/src/rustuya_manager/static/app.js +456 -0
- rustuya_manager-0.1.0/src/rustuya_manager/static/cards.js +415 -0
- rustuya_manager-0.1.0/src/rustuya_manager/static/dom.js +197 -0
- rustuya_manager-0.1.0/src/rustuya_manager/static/header-actions.js +125 -0
- rustuya_manager-0.1.0/src/rustuya_manager/static/i18n.js +164 -0
- rustuya_manager-0.1.0/src/rustuya_manager/static/locales/en.json +295 -0
- rustuya_manager-0.1.0/src/rustuya_manager/static/locales/ko.json +295 -0
- rustuya_manager-0.1.0/src/rustuya_manager/static/modal-confirm.js +56 -0
- rustuya_manager-0.1.0/src/rustuya_manager/static/modal-device.js +172 -0
- rustuya_manager-0.1.0/src/rustuya_manager/static/modal-log.js +81 -0
- rustuya_manager-0.1.0/src/rustuya_manager/static/modal-plugins.js +275 -0
- rustuya_manager-0.1.0/src/rustuya_manager/static/modal-sync.js +313 -0
- rustuya_manager-0.1.0/src/rustuya_manager/static/modal-wizard.js +258 -0
- rustuya_manager-0.1.0/src/rustuya_manager/static/plugins.js +272 -0
- rustuya_manager-0.1.0/src/rustuya_manager/static/render.js +663 -0
- rustuya_manager-0.1.0/src/rustuya_manager/static/state.js +54 -0
- rustuya_manager-0.1.0/src/rustuya_manager/static/ws.js +72 -0
- rustuya_manager-0.1.0/src/rustuya_manager/templates/index.html +441 -0
- rustuya_manager-0.1.0/src/rustuya_manager/versions.py +195 -0
- rustuya_manager-0.1.0/src/rustuya_manager/web.py +993 -0
- rustuya_manager-0.1.0/src/rustuya_manager/wizard.py +334 -0
- rustuya_manager-0.1.0/src/rustuya_manager.egg-info/PKG-INFO +20 -0
- rustuya_manager-0.1.0/src/rustuya_manager.egg-info/SOURCES.txt +59 -0
- rustuya_manager-0.1.0/src/rustuya_manager.egg-info/dependency_links.txt +1 -0
- rustuya_manager-0.1.0/src/rustuya_manager.egg-info/entry_points.txt +2 -0
- rustuya_manager-0.1.0/src/rustuya_manager.egg-info/requires.txt +15 -0
- rustuya_manager-0.1.0/src/rustuya_manager.egg-info/top_level.txt +1 -0
- rustuya_manager-0.1.0/tests/test_catalog.py +475 -0
- rustuya_manager-0.1.0/tests/test_cli.py +500 -0
- rustuya_manager-0.1.0/tests/test_cloud.py +152 -0
- rustuya_manager-0.1.0/tests/test_e2e_bridge.py +662 -0
- rustuya_manager-0.1.0/tests/test_integration_mosquitto.py +294 -0
- rustuya_manager-0.1.0/tests/test_models.py +158 -0
- rustuya_manager-0.1.0/tests/test_mqtt_client.py +1343 -0
- rustuya_manager-0.1.0/tests/test_plugins.py +826 -0
- rustuya_manager-0.1.0/tests/test_requirements.py +144 -0
- rustuya_manager-0.1.0/tests/test_scan.py +204 -0
- rustuya_manager-0.1.0/tests/test_state.py +197 -0
- rustuya_manager-0.1.0/tests/test_topic_helpers.py +281 -0
- rustuya_manager-0.1.0/tests/test_versions.py +121 -0
- rustuya_manager-0.1.0/tests/test_web.py +544 -0
- rustuya_manager-0.1.0/tests/test_wizard.py +390 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rustuya-manager
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Management tool for rustuya-bridge โ keeps Tuya Cloud devices and the bridge in sync
|
|
5
|
+
License: MIT
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Requires-Dist: aiomqtt>=2.0
|
|
8
|
+
Requires-Dist: rich>=13.0
|
|
9
|
+
Requires-Dist: fastapi>=0.115
|
|
10
|
+
Requires-Dist: uvicorn[standard]>=0.30
|
|
11
|
+
Requires-Dist: pyrustuyabridge<0.4,>=0.3.0
|
|
12
|
+
Requires-Dist: tuyawizard>=0.1.9
|
|
13
|
+
Requires-Dist: cryptography>=41
|
|
14
|
+
Requires-Dist: packaging>=23
|
|
15
|
+
Provides-Extra: dev
|
|
16
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
17
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
18
|
+
Requires-Dist: pytest-playwright>=0.5; extra == "dev"
|
|
19
|
+
Requires-Dist: httpx>=0.27; extra == "dev"
|
|
20
|
+
Requires-Dist: ruff>=0.6; extra == "dev"
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
# Rustuya Manager
|
|
2
|
+
|
|
3
|
+
A management tool for [rustuya-bridge](https://github.com/3735943886/rustuya-bridge) that diffs Tuya Cloud devices against the running bridge and syncs add / remove / update operations. Includes a web UI with built-in Tuya Cloud login.
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+
<sub>Desktop view โ sync categories highlighted with their actions; the header's <b>โฐ Menu</b> holds add device, cloud login, <b>๐ก Scan</b>, theme, refresh, and <b>๐ง Reconfigure bridge</b>. Each row also carries a live-status dot and per-device โ edit / ๐ remove / โป query-status icons.</sub>
|
|
7
|
+
|
|
8
|
+
<img src="docs/screenshots/main-mobile.png" alt="Mobile layout" width="280">
|
|
9
|
+
<br><sub>Mobile view โ layout adapts to narrow viewports.</sub>
|
|
10
|
+
|
|
11
|
+
<sub>Other views: [unannotated](docs/screenshots/main-light.png) ยท [dark](docs/screenshots/main-dark.png) ยท [bulk sync](docs/screenshots/sync-modal.png)</sub>
|
|
12
|
+
|
|
13
|
+
## Key Features
|
|
14
|
+
|
|
15
|
+
- **Status dashboard** โ Missing / Orphaned / Mismatched / Synced categories by diffing the Tuya Cloud device list against the bridge's live state.
|
|
16
|
+
- **Built-in Tuya Cloud login** โ fetch the device list straight from the web UI; no external tooling needed. A `tuyadevices.json` upload / drop-zone is still available for offline workflows.
|
|
17
|
+
- **No separate config** โ picks up the bridge's topic and payload templates from its retained `bridge/config`.
|
|
18
|
+
- **Live updates over MQTT** โ DPS values stream into the UI in real time.
|
|
19
|
+
- **Web UI** โ single-page UI with search, sort, sub-device tree, per-device add / edit / remove and bulk-sync.
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
Start with `--web` (the Docker image does this by default). The
|
|
24
|
+
dashboard loads every device known to either side and categorizes
|
|
25
|
+
it by how the bridge's view compares to the Tuya Cloud-of-record
|
|
26
|
+
โ uploaded as `tuyadevices.json` or pulled in-app via the header **โฐ Menu โ โ Fetch from cloud**:
|
|
27
|
+
|
|
28
|
+
- ๐ฆ **Missing** โ in cloud, not yet on the bridge. Click **Add** on
|
|
29
|
+
the card to publish it; the bridge picks it up and starts polling.
|
|
30
|
+
- ๐ฅ **Orphan** โ on the bridge, not in cloud (or dropped from cloud
|
|
31
|
+
since last sync). Click **๐** to remove it from the bridge.
|
|
32
|
+
- ๐จ **Mismatch** โ in both, but a field drifted (IP / key / version
|
|
33
|
+
differ). Click **Update** to push the cloud values; expand the row
|
|
34
|
+
to see exactly which fields are out of sync.
|
|
35
|
+
- ๐ฉ **Synced** โ in both, fields match. No action needed.
|
|
36
|
+
|
|
37
|
+
Every per-card action has a bulk path โ the buttons above the list
|
|
38
|
+
(**Add missing** / **Remove orphan** / **Update mismatch** / **Apply
|
|
39
|
+
all**) open a modal showing the full plan, let individual rows be
|
|
40
|
+
unchecked to skip, then run them sequentially with per-row status.
|
|
41
|
+
|
|
42
|
+
Click any row to expand it: live DP values stream in via MQTT, plus
|
|
43
|
+
the bridge's last error/status message and the resolved IP / key /
|
|
44
|
+
version. The pencil โ opens an editor that re-publishes the device
|
|
45
|
+
to the bridge with the modified fields (the cloud-of-record JSON is
|
|
46
|
+
unchanged); the trash ๐ removes the device from the bridge.
|
|
47
|
+
|
|
48
|
+
### Refreshing from Tuya Cloud
|
|
49
|
+
|
|
50
|
+
The header **โฐ Menu โ โ Fetch from cloud** opens the in-app login wizard. Sign in
|
|
51
|
+
once via QR with the Smart Life or Tuya Smart app โ credentials are
|
|
52
|
+
cached in `tuyacreds.json`, so subsequent re-fetches skip the scan and
|
|
53
|
+
go straight to the device list.
|
|
54
|
+
|
|
55
|
+
<img src="docs/screenshots/wizard-modal.png" alt="Tuya Cloud login modal" width="420">
|
|
56
|
+
|
|
57
|
+
**Scan device IPs after fetch** (off by default) decides what the
|
|
58
|
+
manager writes into the bridge record:
|
|
59
|
+
|
|
60
|
+
- **Off (default)** โ devices ship to the bridge with no IP. The
|
|
61
|
+
bridge runs its own LAN scan at runtime and catches DHCP IP
|
|
62
|
+
changes automatically. Recommended unless every Tuya device on the
|
|
63
|
+
LAN has a pinned address.
|
|
64
|
+
- **On** โ best performance: the bridge **never scans the LAN** and
|
|
65
|
+
every reconnect goes straight to the recorded address. Only useful
|
|
66
|
+
when every device has a pinned IP (manual static or DHCP
|
|
67
|
+
reservation on the router). On DHCP networks where leases rotate
|
|
68
|
+
the bridge ends up retrying stale addresses; the header **โฐ Menu โ
|
|
69
|
+
๐ก Scan LAN** (or a fresh cloud re-fetch) recovers visibility.
|
|
70
|
+
|
|
71
|
+
The toggle state is persisted per browser.
|
|
72
|
+
|
|
73
|
+
**๐ก Scan LAN** (header **โฐ Menu**) asks the bridge for a one-shot LAN
|
|
74
|
+
scan. Any device registered with an explicit (non-auto) IP that has drifted
|
|
75
|
+
surfaces as `ERR_STATE 906` in the MSG line, so the right device can
|
|
76
|
+
be fixed at the router.
|
|
77
|
+
|
|
78
|
+
## Quick Start
|
|
79
|
+
|
|
80
|
+
Requires Python 3.10+ and a running [rustuya-bridge](https://github.com/3735943886/rustuya-bridge) reachable via MQTT.
|
|
81
|
+
|
|
82
|
+
### Install
|
|
83
|
+
|
|
84
|
+
**pipx (recommended)** โ drops a `rustuya-manager` shim into `~/.local/bin/`, no activate step:
|
|
85
|
+
```bash
|
|
86
|
+
sudo apt install -y pipx # if not already
|
|
87
|
+
pipx ensurepath
|
|
88
|
+
pipx install rustuya-manager
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
**venv + pip** โ alternative install without pipx:
|
|
92
|
+
```bash
|
|
93
|
+
python3 -m venv ~/.venvs/rustuya-manager
|
|
94
|
+
~/.venvs/rustuya-manager/bin/pip install rustuya-manager
|
|
95
|
+
~/.venvs/rustuya-manager/bin/rustuya-manager --help
|
|
96
|
+
```
|
|
97
|
+
Run it by full path, or activate the venv first (`source ~/.venvs/rustuya-manager/bin/activate`). The systemd unit in the next section assumes the pipx path โ change `ExecStart` to `%h/.venvs/rustuya-manager/bin/rustuya-manager` for the venv install.
|
|
98
|
+
|
|
99
|
+
### Run
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
rustuya-manager --broker mqtt://localhost:1883 --root rustuya \
|
|
103
|
+
--web --port 8373 --auth admin:CHANGE_ME
|
|
104
|
+
```
|
|
105
|
+
Then open the URL printed at startup. The default bind is `127.0.0.1` so the UI is reachable only from the same machine. To open it to the LAN add `--host 0.0.0.0` โ pair with a real `--auth user:pass`.
|
|
106
|
+
|
|
107
|
+
Common flags:
|
|
108
|
+
- `--cloud PATH` (default `tuyadevices.json`) โ Tuya devices JSON. If
|
|
109
|
+
missing, the web UI offers an in-app Tuya Cloud login or a JSON
|
|
110
|
+
drop-zone.
|
|
111
|
+
- `--broker URL` (default `mqtt://localhost:1883`) โ accepts
|
|
112
|
+
`mqtt://[user:pass@]host:port`.
|
|
113
|
+
- `--root TOPIC` (default `rustuya`) โ must match the bridge's
|
|
114
|
+
`--mqtt-root-topic`.
|
|
115
|
+
- `--host`, `--port` (default `127.0.0.1:8373`) โ web server bind.
|
|
116
|
+
- `--auth USER:PASS` (default off) โ HTTP Basic auth for the web UI.
|
|
117
|
+
- `--embed-bridge` (default off) โ run the bridge inside this process
|
|
118
|
+
via the `pyrustuyabridge` bindings (single-process deploy). Refused
|
|
119
|
+
at startup if another bridge already publishes on `--root`.
|
|
120
|
+
- `--bridge-state PATH` (default: `rustuya.json` in the same
|
|
121
|
+
directory as `--cloud`, matching the standalone bridge's filename) โ
|
|
122
|
+
embedded bridge's device state file. **Only meaningful with
|
|
123
|
+
`--embed-bridge`.**
|
|
124
|
+
- `--bridge-config PATH` (default off) โ JSON config for the embedded
|
|
125
|
+
bridge, same format as `rustuya-bridge --config` (read and merged;
|
|
126
|
+
auto-created if missing). Sets custom topics / MQTT auth / scanner
|
|
127
|
+
options without re-exposing every bridge flag here. **Only meaningful
|
|
128
|
+
with `--embed-bridge`.** For the three fields the manager and bridge
|
|
129
|
+
share (`mqtt_broker`, `mqtt_root_topic`, `state_file`) the manager
|
|
130
|
+
adopts the config's values as its own defaults; a CLI flag still wins.
|
|
131
|
+
|
|
132
|
+
### Run as a service (systemd, user-level, no sudo)
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
mkdir -p ~/.config/systemd/user ~/.local/share/rustuya-manager
|
|
136
|
+
cp examples/rustuya-manager.service ~/.config/systemd/user/
|
|
137
|
+
# edit the file โ change --auth, --broker, --root to match the local setup
|
|
138
|
+
systemctl --user daemon-reload
|
|
139
|
+
systemctl --user enable --now rustuya-manager
|
|
140
|
+
journalctl --user -u rustuya-manager -f # follow logs
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
To keep the service running after logout (one-time, the only sudo step):
|
|
144
|
+
```bash
|
|
145
|
+
sudo loginctl enable-linger $USER
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Update
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
pipx upgrade rustuya-manager # pipx install
|
|
152
|
+
# or, for the venv install:
|
|
153
|
+
~/.venvs/rustuya-manager/bin/pip install -U rustuya-manager
|
|
154
|
+
systemctl --user restart rustuya-manager
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Docker
|
|
158
|
+
|
|
159
|
+
Single-container deploy with the bridge bundled in โ aimed at HA OS,
|
|
160
|
+
unraid, CasaOS and similar container-first setups (distinct from the
|
|
161
|
+
pipx + systemd track above, which keeps `rustuya-bridge` as a separate
|
|
162
|
+
service).
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
docker run -d \
|
|
166
|
+
--name rustuya-manager \
|
|
167
|
+
--network host \
|
|
168
|
+
--restart unless-stopped \
|
|
169
|
+
-e AUTH=admin:CHANGE_ME \
|
|
170
|
+
-v rustuya-manager-data:/data \
|
|
171
|
+
3735943886/rustuya-manager:latest
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
The image runs `rustuya-manager --web --embed-bridge`, so manager and
|
|
175
|
+
bridge share one process and the only external dependency is an MQTT
|
|
176
|
+
broker. Key points:
|
|
177
|
+
|
|
178
|
+
- **`--network host` is required** โ the embedded bridge discovers Tuya
|
|
179
|
+
devices with UDP broadcasts (ports 6666/6667), which Docker's default
|
|
180
|
+
bridge network drops, so devices are never seen.
|
|
181
|
+
- **Broker** defaults to `mqtt://localhost:1883` (host-local mosquitto
|
|
182
|
+
under `--network host`). For a remote broker, set `mqtt_broker` in
|
|
183
|
+
`/data/config.json` โ the embedded bridge auto-creates that file and
|
|
184
|
+
treats it as the single source of truth.
|
|
185
|
+
- **Already running a separate bridge?** Pass `-e EMBED_BRIDGE=0` so the
|
|
186
|
+
container doesn't spawn a second one that double-publishes on the same
|
|
187
|
+
topics.
|
|
188
|
+
- **`--restart unless-stopped`** lets Docker recover the manager โ and
|
|
189
|
+
with it a fresh embedded bridge โ after the in-process supervisor hits
|
|
190
|
+
its restart rate limit.
|
|
191
|
+
|
|
192
|
+
Environment variables (defaults shown; all optional unless noted):
|
|
193
|
+
|
|
194
|
+
| Variable | Default | Maps to |
|
|
195
|
+
|---|---|---|
|
|
196
|
+
| `HOST` | `0.0.0.0` | `--host` |
|
|
197
|
+
| `PORT` | `8373` | `--port` |
|
|
198
|
+
| `BROKER` | *(unset โ bridge-config, then `mqtt://localhost:1883`)* | `--broker` |
|
|
199
|
+
| `ROOT` | *(unset โ bridge-config, then `rustuya`)* | `--root` |
|
|
200
|
+
| `AUTH` | *(off)* | `--auth USER:PASS` |
|
|
201
|
+
| `CLOUD` | `/data/tuyadevices.json` | `--cloud` |
|
|
202
|
+
| `PLUGIN_DIR` | `/data/plugins` | `--plugin-dir` |
|
|
203
|
+
| `BRIDGE_CONFIG` | `/data/config.json` | `--bridge-config` |
|
|
204
|
+
| `BRIDGE_STATE` | *(unset โ bridge-config `state_file`, then `/data/rustuya.json`)* | `--bridge-state` |
|
|
205
|
+
| `PUID` | `1000` | UID the app runs as |
|
|
206
|
+
| `PGID` | `1000` | GID the app runs as |
|
|
207
|
+
| `EMBED_BRIDGE` | `1` | `--embed-bridge` (set `0` when an external bridge is already on the broker) |
|
|
208
|
+
|
|
209
|
+
All persistent state lives under `/data` โ cloud cache
|
|
210
|
+
(`tuyadevices.json`), wizard credentials (`tuyacreds.json`), bridge
|
|
211
|
+
config (`config.json`) and bridge state (`rustuya.json`) โ so the volume
|
|
212
|
+
is the only backup target. Pass an empty value to disable an optional
|
|
213
|
+
flag (e.g. `-e BRIDGE_CONFIG=`).
|
|
214
|
+
|
|
215
|
+
For **bind-mounted** host directories (`-v /host/path:/data`), pass
|
|
216
|
+
`PUID`/`PGID` so the in-container user can write to them:
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
-e PUID=$(id -u) -e PGID=$(id -g)
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
The entrypoint renumbers its internal `manager` user to that UID/GID and
|
|
223
|
+
`chown`s `/data` on startup. Named volumes need none of this โ Docker
|
|
224
|
+
handles ownership.
|
|
225
|
+
|
|
226
|
+
### Plugins
|
|
227
|
+
|
|
228
|
+
A plugin can add a UI tab, REST routes, an MQTT subscription, and its
|
|
229
|
+
own slice of state. They arrive three ways:
|
|
230
|
+
|
|
231
|
+
1. **From the in-UI catalog** (recommended) โ the **Manage plugins** (๐งฉ)
|
|
232
|
+
item in the โฐ menu lists a curated catalog you install from with one
|
|
233
|
+
click, no shell or `pip`.
|
|
234
|
+
2. **As a pip-installed package** under the `rustuya_manager.plugins`
|
|
235
|
+
entry-point group.
|
|
236
|
+
3. **Hand-dropped** into the plugin directory (handy for development or
|
|
237
|
+
Docker, where you'd otherwise rebuild the image).
|
|
238
|
+
|
|
239
|
+
All three run **in-process with no sandbox**, so the trust anchor is the
|
|
240
|
+
same in every case: only install plugins you trust. The catalog is
|
|
241
|
+
curated for exactly this reason โ there is no arbitrary-URL field.
|
|
242
|
+
|
|
243
|
+
#### Installing from the catalog
|
|
244
|
+
|
|
245
|
+
**Manage plugins** (โฐ โ ๐งฉ) opens a modal listing each catalog plugin
|
|
246
|
+
with its install state and actions:
|
|
247
|
+
|
|
248
|
+
- **Install** downloads the plugin into the managed plugin directory,
|
|
249
|
+
verifies its checksum, and wires it up **live** โ the new tab appears
|
|
250
|
+
with no restart.
|
|
251
|
+
- **Update**, **Enable/Disable**, and **Uninstall** act on an installed
|
|
252
|
+
plugin. These need a manager restart to take effect (already-loaded
|
|
253
|
+
code can't be swapped or unloaded at runtime), so the modal offers a
|
|
254
|
+
**Restart now** button when an action requires it.
|
|
255
|
+
|
|
256
|
+
Installs need a writable plugin directory (see below). Under Docker that
|
|
257
|
+
means a mounted `PLUGIN_DIR`.
|
|
258
|
+
|
|
259
|
+
#### The plugin directory
|
|
260
|
+
|
|
261
|
+
The managed plugin directory is where the catalog installs plugins and
|
|
262
|
+
where you can also hand-drop your own. It defaults to a `plugins/` folder
|
|
263
|
+
next to the cloud file; override it with `--plugin-dir DIR` (env
|
|
264
|
+
`RUSTUYA_MANAGER_PLUGIN_DIR`). Under Docker the image defaults
|
|
265
|
+
`PLUGIN_DIR=/data/plugins` โ mount a folder there so installs persist.
|
|
266
|
+
|
|
267
|
+
To hand-drop a plugin, place a package (a directory with `__init__.py`
|
|
268
|
+
exposing `register(ctx)`) or a single `*.py` file in that directory:
|
|
269
|
+
|
|
270
|
+
```
|
|
271
|
+
<plugin-dir>/
|
|
272
|
+
rustuya_hello/ # package plugin
|
|
273
|
+
__init__.py # defines register(ctx)
|
|
274
|
+
static/ # its UI assets (served automatically)
|
|
275
|
+
quicktweak.py # single-file plugin: just register(ctx)
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
One caveat for hand-dropped plugins: they **can't install their own
|
|
279
|
+
dependencies** (they get the standard library plus what the manager
|
|
280
|
+
already provides โ for anything heavier, install it as an entry-point
|
|
281
|
+
package or via the catalog instead). See
|
|
282
|
+
[`examples/hello_plugin`](examples/hello_plugin) for a complete plugin.
|
|
283
|
+
|
|
284
|
+
#### Loading a hand-dropped plugin
|
|
285
|
+
|
|
286
|
+
Installing from the catalog ("Manage plugins" ๐งฉ) loads the plugin live, no
|
|
287
|
+
restart. If you instead copy a plugin package into the plugin dir by hand, pick
|
|
288
|
+
it up with:
|
|
289
|
+
|
|
290
|
+
- **Restart manager** (โป, โฐ-menu) โ restarts the manager process in place (same
|
|
291
|
+
PID, via re-exec). This is the full reload: it discovers hand-dropped plugins,
|
|
292
|
+
picks up edited plugin code, drops removed/disabled plugins, and respawns an
|
|
293
|
+
embedded bridge โ lighter than a container restart and works outside Docker
|
|
294
|
+
too. The UI reconnects automatically; an embedded bridge briefly disconnects
|
|
295
|
+
its devices.
|
|
296
|
+
|
|
297
|
+
(The add-only `POST /api/plugins/scan` endpoint still loads newly-dropped
|
|
298
|
+
plugins without a restart, but it's no longer surfaced as a menu item โ a
|
|
299
|
+
restart is the simpler story for the rare hand-drop.)
|
|
300
|
+
|
|
301
|
+
## License
|
|
302
|
+
MIT
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "rustuya-manager"
|
|
7
|
+
# Single source of truth: src/rustuya_manager/__init__.py:__version__. Bump it
|
|
8
|
+
# there only โ this resolves it at build time, web.py reads it at runtime, and
|
|
9
|
+
# check.sh reads it from the installed package. See [tool.setuptools.dynamic].
|
|
10
|
+
dynamic = ["version"]
|
|
11
|
+
description = "Management tool for rustuya-bridge โ keeps Tuya Cloud devices and the bridge in sync"
|
|
12
|
+
requires-python = ">=3.10"
|
|
13
|
+
license = { text = "MIT" }
|
|
14
|
+
dependencies = [
|
|
15
|
+
"aiomqtt>=2.0",
|
|
16
|
+
"rich>=13.0",
|
|
17
|
+
"fastapi>=0.115",
|
|
18
|
+
"uvicorn[standard]>=0.30",
|
|
19
|
+
"pyrustuyabridge>=0.3.0,<0.4", # Floor is the 0.3.0 stable line: it supersedes the 0.3.0rc28 the manager was developed against (by PEP 440, a stable 0.3.0 already includes everything through rc28), and clears the last pre-release pin from the tree for the final manager release. The rc-by-rc history below is retained as the rationale for why the manager needs this line. 0.2.0rc11 added the `reconfigure` self-terminate that _EmbeddedBridgeSupervisor (cli.py) drives. rc12โrc16 are bridge-side correctness fixes the manager benefits from: rc12 retained cleanup covers the whole fleet, rc13 fixes a cleanup deadlock + coalesced-removes gap, rc15 counts PubAcks instead of buffered writes, rc16 adds a liveness-probe singleton check so post-power-loss startup can't deadlock. rc17 makes multi-target commands (remove cascade, clear) reply per-id so the manager can act on each ack individually instead of guessing which devices the bridge wiped. rc18 moves the cache-mode retained snapshot from `{type}=passive` to `{type}=state` (passive becomes a no-retain delta); the manager wildcards `{type}` so no code change, but pinning rc18 keeps a mixed deployment from leaving orphan `passive` retained snapshots that briefly seed `retained_only` before the bridge's scavenger clears them. rc19 exposes `parse_seed_dps` so `_extract_dps_from_event` reads multi-DP event payloads (no `{dp}` in topic, bare DPS object โ e.g. a `get` reply on `event/passive/{id}`) byte-identically to the bridge instead of dropping them; the manager now calls it, so this is a hard pin. rc20 bumps the core rustuya crate (bridge-internal). rc21 lifts rumqttc's 10KiB client packet cap and paginates the `status` reply so large fleets aren't truncated mid-device-list, and caps connection-establishment concurrency so a big fleet's startup storm can't overwhelm the bridge. rc22 adds background-task panic logging (diagnostic). rc23 fixes a Python-binding shutdown-time process abort โ a tokio worker forwarding a log record to Python during interpreter finalization could force-terminate the process at the `panic=abort` boundary; since the manager drives the embedded bridge's `stop()`/`close()` (_EmbeddedBridgeSupervisor), pinning rc23 keeps an embedded-bridge shutdown from aborting the manager process. rc23 also pulls rustuya rc7's replay-latched-status fix so a late-attaching listener still gets a large fleet's straggler device states. rc24 fixes fleet-scale fan-out under QoS1: a name-addressed `set`/`get` or gateway cascade emitting hundreds of deltas+responses at once stalled the MQTT event loop (PUBACK round-trips + the in-flight window) and reset ~60% of a 500-device fleet's connections, so most responses were lost (~99/500 delivered). Live messages now publish at QoS0 (retained state stays QoS1) and the publish loop uses non-blocking try_publish; fan-out also runs bounded-concurrent. A 500-device name fan-out now completes with 0 connection resets โ the manager issues exactly these name/cascade commands, so this is the fix that makes whole-fleet ops reliable. rc25 stops publishing mqtt_user/mqtt_password into the retained {root}/bridge/config (skip_serializing) and injects the bridge `version` into that payload. The manager exposes the raw bridge config to third-party plugins via ctx.bridge_config(), and can run an embedded bridge through this binding โ so pinning rc25 guarantees the embedded path never writes credentials to the broker in the first place (the manager also redacts defensively in plugins.py for external/older bridges). The `version` field is surfaced in the UI's bridge-diagnostics panel. The pin moved 0.2.xโ0.3.x at the same rc25: the wheel was re-tagged 0.3.0rc25 to match the bridge crate version it ships (`CARGO_PKG_VERSION`, which the bridge publishes into {root}/bridge/config), so the pip label and the status-reported version finally agree โ same code, aligned numbering. The Info panel's online version check (PyPI max, prereleases included) relies on this alignment. rc26 makes the retain scavenger sweep in repeated passes (re-subscribe โ collect โ clear, until a pass clears nothing) instead of a single replay window with no retry: a mass `clear`/`remove` on a large fleet, where the broker's one-shot retained replay is gappy or slow, previously stranded most retained device state ("scavenger left orphans"). The manager issues exactly these mass clear/remove commands, so pinning rc26 is what makes whole-fleet teardown reliably leave no orphan retained state. rc27 bumps the core rustuya crate rc7โrc9 (bridge-internal, transparent through the binding): device connect now accepts IPv6 literals, link-local zone ids, and NAT64-synthesized addresses (the connect path passes `(host, port)` as a tuple instead of a formatted `addr:port` string, which mangled bracketless IPv6), and the crypto stack moves to aes-gcm 0.11.0 final, clearing the last pre-release dependency from the tree. No manager-facing API change. rc28 adds a `set_config` MQTT command (alias `configure`) that patches the six topic/template/retain settings into the config file and optionally chains a `reconfigure`, and โ BREAKING for the bridge's own CLI/env surface โ makes those same six settings (mqtt_command_topic, mqtt_event_topic, mqtt_message_topic, mqtt_scanner_topic, mqtt_payload_template, mqtt_retain) config-file/set_config only, dropping their `--mqtt-*` flags and `MQTT_*` env vars so a runtime config write can't be silently overridden on the next restart. This doesn't affect the manager: `_spawn_embedded_bridge` (cli.py) only forwards mqtt_broker/mqtt_root_topic/state_file/log_level (+ optional config_path) and broker creds as kwargs โ it never passed any of those six as flags or env, so the embedded path already complies with the config-file-only rule, and the manager reads templates from the retained {root}/bridge/config topic regardless. The new set_config command is available to surface in the UI later but is not yet used.
|
|
20
|
+
"tuyawizard>=0.1.9", # 0.1.8 added close()/context manager to stop the per-call requests.Session leak (~750-950 KB/cycle); 0.1.9 then changed close()'s default so it no longer POSTs to `/v1.0/m/token/terminal/expire` โ the previous default revoked the saved tokens server-side after every wizard run, forcing a fresh QR scan on the next call. Manager's wizard.close() is called with no args so it picks up the new revoke_terminal=False default automatically.
|
|
21
|
+
"cryptography>=41", # missing dep of tuya-sharing-sdk
|
|
22
|
+
"packaging>=23", # PEP440 parse/compare for the Info panel's PyPI update check (max-incl-prereleases vs installed); already present transitively, declared since versions.py imports it directly
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[project.optional-dependencies]
|
|
26
|
+
dev = [
|
|
27
|
+
"pytest>=8",
|
|
28
|
+
"pytest-asyncio>=0.23",
|
|
29
|
+
"pytest-playwright>=0.5",
|
|
30
|
+
"httpx>=0.27",
|
|
31
|
+
"ruff>=0.6",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[project.scripts]
|
|
35
|
+
rustuya-manager = "rustuya_manager.cli:main"
|
|
36
|
+
|
|
37
|
+
[tool.setuptools.dynamic]
|
|
38
|
+
version = { attr = "rustuya_manager.__version__" }
|
|
39
|
+
|
|
40
|
+
[tool.setuptools.packages.find]
|
|
41
|
+
where = ["src"]
|
|
42
|
+
|
|
43
|
+
[tool.setuptools.package-data]
|
|
44
|
+
rustuya_manager = ["templates/*.html", "static/*.js", "static/*.css", "static/locales/*.json", "data/*.json"]
|
|
45
|
+
|
|
46
|
+
[tool.pytest.ini_options]
|
|
47
|
+
testpaths = ["tests"]
|
|
48
|
+
asyncio_mode = "auto"
|
|
49
|
+
# e2e_ui boots uvicorn in a session-scoped daemon thread and drives the page
|
|
50
|
+
# via Playwright's sync API (greenlet bridge). Both leak event-loop state into
|
|
51
|
+
# the main thread, which then collides with pytest-asyncio's per-test Runner
|
|
52
|
+
# when async unit tests run in the same session. Default to excluding e2e here
|
|
53
|
+
# so a bare `pytest` is fast and conflict-free; CI runs `pytest -m e2e` in a
|
|
54
|
+
# separate step.
|
|
55
|
+
addopts = "-m 'not e2e'"
|
|
56
|
+
markers = [
|
|
57
|
+
"e2e: browser-driven UI smoke tests (Playwright + uvicorn); run separately",
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
[tool.ruff]
|
|
61
|
+
line-length = 100
|
|
62
|
+
target-version = "py310"
|
|
63
|
+
extend-exclude = ["build", "dist"]
|
|
64
|
+
|
|
65
|
+
[tool.ruff.lint]
|
|
66
|
+
select = ["E", "F", "I", "W", "B", "UP"]
|
|
67
|
+
ignore = [
|
|
68
|
+
"E501", # formatter handles line length
|
|
69
|
+
"B008", # FastAPI Depends/Body in defaults
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
[tool.ruff.lint.isort]
|
|
73
|
+
known-first-party = ["rustuya_manager"]
|
|
74
|
+
|
|
75
|
+
[tool.ruff.format]
|
|
76
|
+
quote-style = "double"
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""rustuya-manager โ sync layer between Tuya Cloud and rustuya-bridge.
|
|
2
|
+
|
|
3
|
+
The package exposes domain modules (models, diff, state, mqtt) plus a
|
|
4
|
+
`cli` entry point that preserves the original interactive workflow.
|
|
5
|
+
Topic and payload templating is delegated to `pyrustuyabridge` so the
|
|
6
|
+
manager's interpretation is byte-identical to the bridge's behavior.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
# The one place the version is defined. pyproject.toml resolves it at build
|
|
10
|
+
# time via [tool.setuptools.dynamic]; web.py uses it for the FastAPI title;
|
|
11
|
+
# check.sh reads it from the installed package. Bump here and nowhere else.
|
|
12
|
+
__version__ = "0.1.0"
|