pypopur 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.
- pypopur-0.1.0/LICENSE +21 -0
- pypopur-0.1.0/PKG-INFO +183 -0
- pypopur-0.1.0/README.md +152 -0
- pypopur-0.1.0/pyproject.toml +60 -0
- pypopur-0.1.0/setup.cfg +4 -0
- pypopur-0.1.0/src/pypopur/__init__.py +308 -0
- pypopur-0.1.0/src/pypopur/app/__init__.py +78 -0
- pypopur-0.1.0/src/pypopur/app/dp101.py +524 -0
- pypopur-0.1.0/src/pypopur/app/dp_string.py +401 -0
- pypopur-0.1.0/src/pypopur/app/helper.py +625 -0
- pypopur-0.1.0/src/pypopur/app/manager.py +776 -0
- pypopur-0.1.0/src/pypopur/app/repository.py +585 -0
- pypopur-0.1.0/src/pypopur/app/resolver.py +171 -0
- pypopur-0.1.0/src/pypopur/bootstrap.py +22 -0
- pypopur-0.1.0/src/pypopur/client.py +321 -0
- pypopur-0.1.0/src/pypopur/cloud.py +449 -0
- pypopur-0.1.0/src/pypopur/codec.py +86 -0
- pypopur-0.1.0/src/pypopur/discovery.py +374 -0
- pypopur-0.1.0/src/pypopur/dps.py +860 -0
- pypopur-0.1.0/src/pypopur/events.py +408 -0
- pypopur-0.1.0/src/pypopur/exceptions.py +37 -0
- pypopur-0.1.0/src/pypopur/local.py +477 -0
- pypopur-0.1.0/src/pypopur/mobile.py +2647 -0
- pypopur-0.1.0/src/pypopur/models.py +331 -0
- pypopur-0.1.0/src/pypopur/pipeline.py +389 -0
- pypopur-0.1.0/src/pypopur/popur_app2_material.py +12 -0
- pypopur-0.1.0/src/pypopur/py.typed +1 -0
- pypopur-0.1.0/src/pypopur/reads.py +420 -0
- pypopur-0.1.0/src/pypopur/reference.py +314 -0
- pypopur-0.1.0/src/pypopur/sdk/__init__.py +50 -0
- pypopur-0.1.0/src/pypopur/sdk/_fastjson.py +58 -0
- pypopur-0.1.0/src/pypopur/sdk/_java.py +107 -0
- pypopur-0.1.0/src/pypopur/sdk/atop.py +724 -0
- pypopur-0.1.0/src/pypopur/sdk/comm_pipeline.py +1038 -0
- pypopur-0.1.0/src/pypopur/sdk/crypto.py +166 -0
- pypopur-0.1.0/src/pypopur/sdk/dedup.py +114 -0
- pypopur-0.1.0/src/pypopur/sdk/device_cache.py +909 -0
- pypopur-0.1.0/src/pypopur/sdk/device_id.py +122 -0
- pypopur-0.1.0/src/pypopur/sdk/discovery.py +597 -0
- pypopur-0.1.0/src/pypopur/sdk/hexutil.py +138 -0
- pypopur-0.1.0/src/pypopur/sdk/lan_control.py +1321 -0
- pypopur-0.1.0/src/pypopur/sdk/lan_framing.py +218 -0
- pypopur-0.1.0/src/pypopur/sdk/lan_session.py +1989 -0
- pypopur-0.1.0/src/pypopur/sdk/lan_socket.py +957 -0
- pypopur-0.1.0/src/pypopur/sdk/low_power.py +431 -0
- pypopur-0.1.0/src/pypopur/sdk/mqtt_client.py +486 -0
- pypopur-0.1.0/src/pypopur/sdk/mqtt_framing.py +404 -0
- pypopur-0.1.0/src/pypopur/sdk/mqtt_session.py +451 -0
- pypopur-0.1.0/src/pypopur/sdk/mqtt_sign.py +115 -0
- pypopur-0.1.0/src/pypopur/sdk/sando.py +71 -0
- pypopur-0.1.0/src/pypopur/sdk/schema.py +223 -0
- pypopur-0.1.0/src/pypopur/sdk/security.py +224 -0
- pypopur-0.1.0/src/pypopur/sdk/thing_model.py +1229 -0
- pypopur-0.1.0/src/pypopur/sdk/timestamp.py +106 -0
- pypopur-0.1.0/src/pypopur/sdk/validation.py +293 -0
- pypopur-0.1.0/src/pypopur/transport.py +125 -0
- pypopur-0.1.0/src/pypopur/writes.py +127 -0
- pypopur-0.1.0/src/pypopur.egg-info/PKG-INFO +183 -0
- pypopur-0.1.0/src/pypopur.egg-info/SOURCES.txt +85 -0
- pypopur-0.1.0/src/pypopur.egg-info/dependency_links.txt +1 -0
- pypopur-0.1.0/src/pypopur.egg-info/requires.txt +6 -0
- pypopur-0.1.0/src/pypopur.egg-info/top_level.txt +1 -0
- pypopur-0.1.0/tests/test_app_helper.py +389 -0
- pypopur-0.1.0/tests/test_app_layer.py +1035 -0
- pypopur-0.1.0/tests/test_atop.py +418 -0
- pypopur-0.1.0/tests/test_bootstrap.py +29 -0
- pypopur-0.1.0/tests/test_client.py +271 -0
- pypopur-0.1.0/tests/test_cloud.py +153 -0
- pypopur-0.1.0/tests/test_cloud_channel.py +168 -0
- pypopur-0.1.0/tests/test_codecs.py +332 -0
- pypopur-0.1.0/tests/test_discovery.py +519 -0
- pypopur-0.1.0/tests/test_events.py +537 -0
- pypopur-0.1.0/tests/test_fallback_transport.py +143 -0
- pypopur-0.1.0/tests/test_hardening.py +248 -0
- pypopur-0.1.0/tests/test_lan_session.py +938 -0
- pypopur-0.1.0/tests/test_lan_socket.py +424 -0
- pypopur-0.1.0/tests/test_local.py +278 -0
- pypopur-0.1.0/tests/test_low_power.py +301 -0
- pypopur-0.1.0/tests/test_mobile.py +1417 -0
- pypopur-0.1.0/tests/test_mqtt_client.py +339 -0
- pypopur-0.1.0/tests/test_native_vectors.py +126 -0
- pypopur-0.1.0/tests/test_pipeline.py +402 -0
- pypopur-0.1.0/tests/test_reads.py +218 -0
- pypopur-0.1.0/tests/test_reference.py +58 -0
- pypopur-0.1.0/tests/test_sdk.py +1854 -0
- pypopur-0.1.0/tests/test_thing_model.py +701 -0
- pypopur-0.1.0/tests/test_writes.py +598 -0
pypopur-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 BookCatKid
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
pypopur-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pypopur
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Async Python client for the Popur S7 smart litter box — LAN control, cloud APIs, MQTT events, and pet tracking
|
|
5
|
+
Author: BookCatKid
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/BookCatKid/pypopur
|
|
8
|
+
Project-URL: Repository, https://github.com/BookCatKid/pypopur
|
|
9
|
+
Project-URL: Issues, https://github.com/BookCatKid/pypopur/issues
|
|
10
|
+
Project-URL: Documentation, https://github.com/BookCatKid/pypopur#readme
|
|
11
|
+
Project-URL: Changelog, https://github.com/BookCatKid/pypopur/releases
|
|
12
|
+
Keywords: popur,s7,litter-box,tuya,thingclips,home-assistant,iot,smart-home
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Home Automation
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.11
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: cryptography>=43
|
|
26
|
+
Requires-Dist: tinytuya>=1.15
|
|
27
|
+
Provides-Extra: test
|
|
28
|
+
Requires-Dist: pytest>=8; extra == "test"
|
|
29
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == "test"
|
|
30
|
+
Dynamic: license-file
|
|
31
|
+
|
|
32
|
+
# pypopur
|
|
33
|
+
|
|
34
|
+
**Experimental** — a reverse-engineered Python client for the Popur S7 smart litter box, built from static analysis of the official Android app (v2.0.0 / ThingClips SDK). Tested against a single real device; expect rough edges.
|
|
35
|
+
|
|
36
|
+
[](https://pypi.org/project/pypopur/)
|
|
37
|
+
[](LICENSE)
|
|
38
|
+
[](https://github.com/BookCatKid/pypopur/actions/workflows/ci.yml)
|
|
39
|
+
|
|
40
|
+
## Features
|
|
41
|
+
|
|
42
|
+
- **LAN control** — Tuya protocol 3.5 session-key negotiation, DP read/write over TCP:6668, auto-protocol probing (3.1–3.5), heartbeat keepalive, stale-session recovery
|
|
43
|
+
- **Cloud APIs** — full ThingClips/Popur app surface: auth, homes, devices, DP shadow, pets, pet records, firmware, timers, device management
|
|
44
|
+
- **MQTT events** — real-time DP deltas and alert events over TLS to the app's broker, with reconnect/resubscribe handling
|
|
45
|
+
- **Hybrid transport** — LAN-first with automatic cloud fallback; slow reconciliation tier for settings DPs and pet data the LAN omits
|
|
46
|
+
- **Discovery** — passive UDP announcements plus active host location via ARP MAC match and port-6668 subnet scan
|
|
47
|
+
- **DP codecs** — all firmware-4 packed payloads decoded (settings, calibration, radar config, cat weight, visit records)
|
|
48
|
+
- **Pet tracking** — household pet profiles, per-visit weight/duration records decoded from the app's own format
|
|
49
|
+
- **Home Assistant integration** — full entity coverage (sensors, switches, buttons, numbers, selects) in `custom_components/popur/`
|
|
50
|
+
|
|
51
|
+
## Install
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pip install pypopur
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Requires Python 3.11+.
|
|
58
|
+
|
|
59
|
+
## Quick start — local control
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
import asyncio
|
|
63
|
+
from pypopur import LocalTuyaTransport, PopurClient
|
|
64
|
+
|
|
65
|
+
async def main():
|
|
66
|
+
transport = LocalTuyaTransport(
|
|
67
|
+
device_id="your-device-id",
|
|
68
|
+
host="192.168.1.x",
|
|
69
|
+
local_key="your-local-key",
|
|
70
|
+
)
|
|
71
|
+
client = PopurClient(transport)
|
|
72
|
+
|
|
73
|
+
async with client:
|
|
74
|
+
snapshot = await client.refresh()
|
|
75
|
+
print(snapshot.machine_status, snapshot.cat_present, snapshot.bin_full)
|
|
76
|
+
await client.start_cleaning()
|
|
77
|
+
|
|
78
|
+
asyncio.run(main())
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Quick start — cloud account
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
import asyncio
|
|
85
|
+
from pypopur.mobile import MobileAppProfile, PopurAccount, ThingMobileApi
|
|
86
|
+
|
|
87
|
+
async def main():
|
|
88
|
+
api = ThingMobileApi(
|
|
89
|
+
MobileAppProfile.bundled_popur_app2(),
|
|
90
|
+
install_id="a-stable-id-for-this-install",
|
|
91
|
+
)
|
|
92
|
+
account = PopurAccount(api)
|
|
93
|
+
await account.login("you@example.com", "your-password")
|
|
94
|
+
|
|
95
|
+
home_id = (await account.homes())[0]["gid"]
|
|
96
|
+
devices = await account.home_devices(home_id)
|
|
97
|
+
|
|
98
|
+
for dev in devices:
|
|
99
|
+
print(dev.name, dev.device_id, dev.local_key)
|
|
100
|
+
|
|
101
|
+
asyncio.run(main())
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Quick start — pets
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
pets = await account.pets(home_id)
|
|
108
|
+
for pet in pets:
|
|
109
|
+
print(pet.name, pet.pet_type, pet.weight)
|
|
110
|
+
|
|
111
|
+
page = await account.pet_records(home_id, page_size=50)
|
|
112
|
+
for record in page.records:
|
|
113
|
+
usage = record.toilet_usage()
|
|
114
|
+
if usage:
|
|
115
|
+
print(record.pet_id, usage.weight_grams, "g", usage.duration_seconds, "s")
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Quick start — MQTT events
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
events = account.connect_events(
|
|
122
|
+
devices={dev.device_id: dev.local_key},
|
|
123
|
+
on_event=lambda e: print(e.dev_id, e.dps),
|
|
124
|
+
on_connect=lambda: print("connected"),
|
|
125
|
+
)
|
|
126
|
+
await events.connect()
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Architecture
|
|
130
|
+
|
|
131
|
+
```
|
|
132
|
+
┌─────────────────────────────────────────────────┐
|
|
133
|
+
│ PopurClient │
|
|
134
|
+
│ typed controls + DeviceSnapshot │
|
|
135
|
+
├─────────────────────────────────────────────────┤
|
|
136
|
+
│ FallbackTransport │
|
|
137
|
+
│ LAN primary (3.5) ──fail──► Cloud fallback │
|
|
138
|
+
├──────────┬──────────────────┬───────────────────┤
|
|
139
|
+
│ LAN TCP │ MQTT TLS │ Cloud HTTPS │
|
|
140
|
+
│ :6668 │ smart/mb/in/ │ a1.tuyaus.com │
|
|
141
|
+
│ DP r/w │ DP deltas │ shadow, pets, │
|
|
142
|
+
│ ~23 DPs │ alerts │ mgmt APIs │
|
|
143
|
+
└──────────┴──────────────────┴───────────────────┘
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
- **LAN** — fast path for reads/writes; ~23 DPs in status responses
|
|
147
|
+
- **MQTT** — real-time push (protocol-4 deltas, protocol-56 alerts); deltas only, no snapshots
|
|
148
|
+
- **Cloud** — settings DPs LAN omits (102 etc.), pet profiles/records, firmware, timers, device management
|
|
149
|
+
|
|
150
|
+
## Home Assistant
|
|
151
|
+
|
|
152
|
+
The integration lives in `custom_components/popur/` — install via HACS custom repository or copy the directory to your HA `custom_components/`. See [`custom_components/popur/README.md`](custom_components/popur/README.md) for details.
|
|
153
|
+
|
|
154
|
+
## Status
|
|
155
|
+
|
|
156
|
+
- **Tested against**: one Popur S7 (Wi-Fi 3.0.30 / MCU 4.3.0), firmware-4 protocol family
|
|
157
|
+
- **LAN protocol**: verified live — handshake, DP read/write, heartbeat, reconnect
|
|
158
|
+
- **Cloud APIs**: signature-exact to the app; read paths verified live, write paths untested
|
|
159
|
+
- **MQTT**: verified live — connect, subscribe, decode protocol-4/56 frames, reconnect
|
|
160
|
+
- **Test suite**: 769 tests, all passing
|
|
161
|
+
|
|
162
|
+
## Limitations
|
|
163
|
+
|
|
164
|
+
- Only tested against one device and one firmware version — other S7s may differ
|
|
165
|
+
- App-layer write APIs (pets, device management) are signature-verified but never executed live
|
|
166
|
+
- The standalone LAN transport doesn't fetch the thing model — no schema validation on writes
|
|
167
|
+
- MQTT is event-driven (deltas only) — it can't replace polling for state seeding or reconciliation
|
|
168
|
+
- Settings DPs and pet data require the cloud — they don't exist on the LAN channel
|
|
169
|
+
|
|
170
|
+
## Development
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
python -m pytest tests/ # 769 tests
|
|
174
|
+
ruff check src tests # lint
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## License
|
|
178
|
+
|
|
179
|
+
MIT — see [LICENSE](LICENSE).
|
|
180
|
+
|
|
181
|
+
## Disclaimer
|
|
182
|
+
|
|
183
|
+
This is an unofficial, reverse-engineered client. It is not affiliated with or endorsed by Popur. Use at your own risk — the author is not responsible for bricked devices, angry cats, or unexpected litter box behavior.
|
pypopur-0.1.0/README.md
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# pypopur
|
|
2
|
+
|
|
3
|
+
**Experimental** — a reverse-engineered Python client for the Popur S7 smart litter box, built from static analysis of the official Android app (v2.0.0 / ThingClips SDK). Tested against a single real device; expect rough edges.
|
|
4
|
+
|
|
5
|
+
[](https://pypi.org/project/pypopur/)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
[](https://github.com/BookCatKid/pypopur/actions/workflows/ci.yml)
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- **LAN control** — Tuya protocol 3.5 session-key negotiation, DP read/write over TCP:6668, auto-protocol probing (3.1–3.5), heartbeat keepalive, stale-session recovery
|
|
12
|
+
- **Cloud APIs** — full ThingClips/Popur app surface: auth, homes, devices, DP shadow, pets, pet records, firmware, timers, device management
|
|
13
|
+
- **MQTT events** — real-time DP deltas and alert events over TLS to the app's broker, with reconnect/resubscribe handling
|
|
14
|
+
- **Hybrid transport** — LAN-first with automatic cloud fallback; slow reconciliation tier for settings DPs and pet data the LAN omits
|
|
15
|
+
- **Discovery** — passive UDP announcements plus active host location via ARP MAC match and port-6668 subnet scan
|
|
16
|
+
- **DP codecs** — all firmware-4 packed payloads decoded (settings, calibration, radar config, cat weight, visit records)
|
|
17
|
+
- **Pet tracking** — household pet profiles, per-visit weight/duration records decoded from the app's own format
|
|
18
|
+
- **Home Assistant integration** — full entity coverage (sensors, switches, buttons, numbers, selects) in `custom_components/popur/`
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install pypopur
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Requires Python 3.11+.
|
|
27
|
+
|
|
28
|
+
## Quick start — local control
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
import asyncio
|
|
32
|
+
from pypopur import LocalTuyaTransport, PopurClient
|
|
33
|
+
|
|
34
|
+
async def main():
|
|
35
|
+
transport = LocalTuyaTransport(
|
|
36
|
+
device_id="your-device-id",
|
|
37
|
+
host="192.168.1.x",
|
|
38
|
+
local_key="your-local-key",
|
|
39
|
+
)
|
|
40
|
+
client = PopurClient(transport)
|
|
41
|
+
|
|
42
|
+
async with client:
|
|
43
|
+
snapshot = await client.refresh()
|
|
44
|
+
print(snapshot.machine_status, snapshot.cat_present, snapshot.bin_full)
|
|
45
|
+
await client.start_cleaning()
|
|
46
|
+
|
|
47
|
+
asyncio.run(main())
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Quick start — cloud account
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
import asyncio
|
|
54
|
+
from pypopur.mobile import MobileAppProfile, PopurAccount, ThingMobileApi
|
|
55
|
+
|
|
56
|
+
async def main():
|
|
57
|
+
api = ThingMobileApi(
|
|
58
|
+
MobileAppProfile.bundled_popur_app2(),
|
|
59
|
+
install_id="a-stable-id-for-this-install",
|
|
60
|
+
)
|
|
61
|
+
account = PopurAccount(api)
|
|
62
|
+
await account.login("you@example.com", "your-password")
|
|
63
|
+
|
|
64
|
+
home_id = (await account.homes())[0]["gid"]
|
|
65
|
+
devices = await account.home_devices(home_id)
|
|
66
|
+
|
|
67
|
+
for dev in devices:
|
|
68
|
+
print(dev.name, dev.device_id, dev.local_key)
|
|
69
|
+
|
|
70
|
+
asyncio.run(main())
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Quick start — pets
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
pets = await account.pets(home_id)
|
|
77
|
+
for pet in pets:
|
|
78
|
+
print(pet.name, pet.pet_type, pet.weight)
|
|
79
|
+
|
|
80
|
+
page = await account.pet_records(home_id, page_size=50)
|
|
81
|
+
for record in page.records:
|
|
82
|
+
usage = record.toilet_usage()
|
|
83
|
+
if usage:
|
|
84
|
+
print(record.pet_id, usage.weight_grams, "g", usage.duration_seconds, "s")
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Quick start — MQTT events
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
events = account.connect_events(
|
|
91
|
+
devices={dev.device_id: dev.local_key},
|
|
92
|
+
on_event=lambda e: print(e.dev_id, e.dps),
|
|
93
|
+
on_connect=lambda: print("connected"),
|
|
94
|
+
)
|
|
95
|
+
await events.connect()
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Architecture
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
┌─────────────────────────────────────────────────┐
|
|
102
|
+
│ PopurClient │
|
|
103
|
+
│ typed controls + DeviceSnapshot │
|
|
104
|
+
├─────────────────────────────────────────────────┤
|
|
105
|
+
│ FallbackTransport │
|
|
106
|
+
│ LAN primary (3.5) ──fail──► Cloud fallback │
|
|
107
|
+
├──────────┬──────────────────┬───────────────────┤
|
|
108
|
+
│ LAN TCP │ MQTT TLS │ Cloud HTTPS │
|
|
109
|
+
│ :6668 │ smart/mb/in/ │ a1.tuyaus.com │
|
|
110
|
+
│ DP r/w │ DP deltas │ shadow, pets, │
|
|
111
|
+
│ ~23 DPs │ alerts │ mgmt APIs │
|
|
112
|
+
└──────────┴──────────────────┴───────────────────┘
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
- **LAN** — fast path for reads/writes; ~23 DPs in status responses
|
|
116
|
+
- **MQTT** — real-time push (protocol-4 deltas, protocol-56 alerts); deltas only, no snapshots
|
|
117
|
+
- **Cloud** — settings DPs LAN omits (102 etc.), pet profiles/records, firmware, timers, device management
|
|
118
|
+
|
|
119
|
+
## Home Assistant
|
|
120
|
+
|
|
121
|
+
The integration lives in `custom_components/popur/` — install via HACS custom repository or copy the directory to your HA `custom_components/`. See [`custom_components/popur/README.md`](custom_components/popur/README.md) for details.
|
|
122
|
+
|
|
123
|
+
## Status
|
|
124
|
+
|
|
125
|
+
- **Tested against**: one Popur S7 (Wi-Fi 3.0.30 / MCU 4.3.0), firmware-4 protocol family
|
|
126
|
+
- **LAN protocol**: verified live — handshake, DP read/write, heartbeat, reconnect
|
|
127
|
+
- **Cloud APIs**: signature-exact to the app; read paths verified live, write paths untested
|
|
128
|
+
- **MQTT**: verified live — connect, subscribe, decode protocol-4/56 frames, reconnect
|
|
129
|
+
- **Test suite**: 769 tests, all passing
|
|
130
|
+
|
|
131
|
+
## Limitations
|
|
132
|
+
|
|
133
|
+
- Only tested against one device and one firmware version — other S7s may differ
|
|
134
|
+
- App-layer write APIs (pets, device management) are signature-verified but never executed live
|
|
135
|
+
- The standalone LAN transport doesn't fetch the thing model — no schema validation on writes
|
|
136
|
+
- MQTT is event-driven (deltas only) — it can't replace polling for state seeding or reconciliation
|
|
137
|
+
- Settings DPs and pet data require the cloud — they don't exist on the LAN channel
|
|
138
|
+
|
|
139
|
+
## Development
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
python -m pytest tests/ # 769 tests
|
|
143
|
+
ruff check src tests # lint
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## License
|
|
147
|
+
|
|
148
|
+
MIT — see [LICENSE](LICENSE).
|
|
149
|
+
|
|
150
|
+
## Disclaimer
|
|
151
|
+
|
|
152
|
+
This is an unofficial, reverse-engineered client. It is not affiliated with or endorsed by Popur. Use at your own risk — the author is not responsible for bricked devices, angry cats, or unexpected litter box behavior.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=69"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pypopur"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Async Python client for the Popur S7 smart litter box — LAN control, cloud APIs, MQTT events, and pet tracking"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{name = "BookCatKid"}]
|
|
14
|
+
keywords = ["popur", "s7", "litter-box", "tuya", "thingclips", "home-assistant", "iot", "smart-home"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.11",
|
|
20
|
+
"Programming Language :: Python :: 3.12",
|
|
21
|
+
"Programming Language :: Python :: 3.13",
|
|
22
|
+
"Topic :: Home Automation",
|
|
23
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
24
|
+
"Typing :: Typed",
|
|
25
|
+
]
|
|
26
|
+
dependencies = [
|
|
27
|
+
"cryptography>=43",
|
|
28
|
+
"tinytuya>=1.15",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[project.optional-dependencies]
|
|
32
|
+
test = ["pytest>=8", "pytest-asyncio>=0.24"]
|
|
33
|
+
|
|
34
|
+
[project.urls]
|
|
35
|
+
Homepage = "https://github.com/BookCatKid/pypopur"
|
|
36
|
+
Repository = "https://github.com/BookCatKid/pypopur"
|
|
37
|
+
Issues = "https://github.com/BookCatKid/pypopur/issues"
|
|
38
|
+
Documentation = "https://github.com/BookCatKid/pypopur#readme"
|
|
39
|
+
Changelog = "https://github.com/BookCatKid/pypopur/releases"
|
|
40
|
+
|
|
41
|
+
[tool.setuptools.packages.find]
|
|
42
|
+
where = ["src"]
|
|
43
|
+
|
|
44
|
+
[tool.setuptools.package-data]
|
|
45
|
+
pypopur = ["py.typed"]
|
|
46
|
+
|
|
47
|
+
[tool.pytest.ini_options]
|
|
48
|
+
addopts = "-q"
|
|
49
|
+
asyncio_mode = "auto"
|
|
50
|
+
testpaths = ["tests"]
|
|
51
|
+
|
|
52
|
+
[tool.ruff]
|
|
53
|
+
target-version = "py311"
|
|
54
|
+
line-length = 100
|
|
55
|
+
|
|
56
|
+
[tool.ruff.lint.per-file-ignores]
|
|
57
|
+
"**/__init__.py" = ["F401"]
|
|
58
|
+
# Ported modules mirror the app's catch-Throwable semantics (listener dispatch,
|
|
59
|
+
# frame parsing) — broad catches are the contract.
|
|
60
|
+
"src/pypopur/**" = ["BLE001", "S110", "S112"]
|
pypopur-0.1.0/setup.cfg
ADDED