Skip to content

Commit d6ff63a

Browse files
authored
Merge branch 'master' into cli-device-control
2 parents e9a23af + 86dc9d2 commit d6ff63a

File tree

7 files changed

+812
-29
lines changed

7 files changed

+812
-29
lines changed

README.md

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -236,9 +236,25 @@ BulbDevice Additional Functions
236236
result = state():
237237
238238
CoverDevice Additional Functions
239-
open_cover(switch=1):
240-
close_cover(switch=1):
241-
stop_cover(switch=1):
239+
open_cover(switch=None, nowait=False):
240+
close_cover(switch=None, nowait=False):
241+
stop_cover(switch=None, nowait=False):
242+
continue_cover(switch=None, nowait=False):
243+
set_cover_type(cover_type): # Manually set cover type (1-8)
244+
245+
CoverDevice automatically detects one of 8 device types by checking status:
246+
Type 1: ["open", "close", "stop", "continue"] - Most curtains, blinds, roller shades (DEFAULT)
247+
Type 2: [true, false] - Simple relays, garage doors, locks
248+
Type 3: ["0", "1", "2"] - String-numeric position/state
249+
Type 4: ["00", "01", "02", "03"] - Zero-prefixed numeric position/state
250+
Type 5: ["fopen", "fclose"] - Directional binary (no stop)
251+
Type 6: ["on", "off", "stop"] - Switch-lexicon open/close
252+
Type 7: ["up", "down", "stop"] - Vertical-motion (lifts, hoists)
253+
Type 8: ["ZZ", "FZ", "STOP"] - Vendor-specific (Abalon-style, older standard)
254+
255+
Detection uses priority ordering based on real-world frequency (Type 1 → Type 8 → Type 3 → others).
256+
Defaults to Type 1 if detection fails. Manual override: set_cover_type(type_id).
257+
Common DPS IDs: 1 (most common), 101 (second most common), 4 (dual-curtain second curtain).
242258
243259
Cloud Functions
244260
setregion(apiRegion)
@@ -349,6 +365,34 @@ d.set_mode('scene')
349365
# Scene Example: Set Color Rotation Scene
350366
d.set_value(25, '07464602000003e803e800000000464602007803e803e80000000046460200f003e803e800000000464602003d03e803e80000000046460200ae03e803e800000000464602011303e803e800000000')
351367
368+
"""
369+
Cover Device (Window Shade)
370+
"""
371+
c = tinytuya.CoverDevice('DEVICE_ID_HERE', 'IP_ADDRESS_HERE', 'LOCAL_KEY_HERE')
372+
c.set_version(3.3)
373+
data = c.status()
374+
375+
# Show status
376+
print('Dictionary %r' % data)
377+
378+
# CoverDevice will automatically detect the device type (1-8)
379+
# and use the appropriate commands
380+
381+
# Open the cover
382+
c.open_cover()
383+
384+
# Close the cover
385+
c.close_cover()
386+
387+
# Stop the cover
388+
c.stop_cover()
389+
390+
# Continue cover motion (if supported by device type)
391+
c.continue_cover()
392+
393+
# Manually set cover type if auto-detection doesn't work
394+
c.set_cover_type(1) # Force Type 1 (open/close/stop/continue)
395+
352396
```
353397
### Example Device Monitor
354398

RELEASE.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,25 @@
1111
* Missing credentials (`--key`, `--ip`, `--version`) are automatically filled in from the matching `devices.json` entry.
1212
* Updated `API.md` and `README.md` to document all new commands and flags.
1313

14+
* Contrib: Add `WiFiDualMeterDevice`, a new community-contributed module to support Tuya WiFi Dual Meter devices by @ggardet in https://github.com/jasonacox/tinytuya/pull/680
15+
16+
* CoverDevice: Major rewrite to support 8 different device command types with automatic detection (credit for discovery: @make-all):
17+
* Type 1: `["open", "close", "stop", "continue"]` - Most curtains, blinds, roller shades (DEFAULT)
18+
* Type 2: `[true, false]` - Simple relays, garage doors, locks
19+
* Type 3: `["0", "1", "2"]` - String-numeric position/state
20+
* Type 4: `["00", "01", "02", "03"]` - Zero-prefixed numeric position/state
21+
* Type 5: `["fopen", "fclose"]` - Directional binary (no stop)
22+
* Type 6: `["on", "off", "stop"]` - Switch-lexicon
23+
* Type 7: `["up", "down", "stop"]` - Vertical-motion (lifts, hoists)
24+
* Type 8: `["ZZ", "FZ", "STOP"]` - Vendor-specific (Abalon-style, older standard)
25+
* Added `continue_cover()` method for device types that support it (Types 1 and 4)
26+
* Added `set_cover_type(type_id)` method to manually override auto-detection
27+
* Added `DEFAULT_COVER_TYPE` constant set to Type 1 (most comprehensive)
28+
* Device type is automatically detected on first command using priority ordering based on real-world frequency:
29+
* Priority: Type 1 (most common) → Type 8 (second most common, older standard) → Type 3 → others
30+
* Common DPS IDs: 1 (most common), 101 (second most common), 4 (dual-curtain second curtain)
31+
* Defaults to Type 1 if detection fails for best compatibility
32+
1433
## 1.17.4 - Cloud Config
1534

1635
- Cloud: Add `configFile` option to the Cloud constructor, allowing users to specify the config file location (default remains 'tinytuya.json') by @blackw1ng in https://github.com/jasonacox/tinytuya/pull/640
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
# examples/Contrib/SoriaInverterDevice-example.py
2+
"""
3+
TinyTuya - Example - SoriaInverterDevice
4+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5+
This script demonstrates how to use the SoriaInverterDevice community
6+
module to monitor a SORIA solar micro-inverter over a local network
7+
using the Tuya v3.5 protocol.
8+
9+
Author : Markourai (https://github.com/Markourai)
10+
Issue : https://github.com/jasonacox/tinytuya/issues/658
11+
12+
Setup:
13+
pip install tinytuya
14+
15+
Replace DEVICE_ID, DEVICE_IP and LOCAL_KEY with your own device
16+
credentials. You can retrieve them using the TinyTuya wizard:
17+
python -m tinytuya wizard
18+
"""
19+
20+
import time
21+
import tinytuya
22+
from tinytuya.Contrib import SoriaInverterDevice
23+
24+
# ---------------------------------------------------------------------------
25+
# Device credentials -- replace with your own values
26+
# ---------------------------------------------------------------------------
27+
DEVICE_ID = 'XXXX'
28+
DEVICE_IP = 'YYYY'
29+
LOCAL_KEY = 'ZZZZ'
30+
VERSION = 3.5
31+
32+
# ---------------------------------------------------------------------------
33+
# Helper — pretty-print a report dict
34+
# ---------------------------------------------------------------------------
35+
36+
def print_realtime(data):
37+
if not data:
38+
return
39+
print('-' * 40)
40+
print('Real-time power (DPS 25)')
41+
print(' PV power : %s W' % data.get('W_PV'))
42+
print(' AC power : %s VA' % data.get('W_AC'))
43+
44+
def print_full_report(data):
45+
if not data:
46+
return
47+
print('-' * 40)
48+
print('Full report (DPS 21)')
49+
print(' --- DC circuit (solar panel) ---')
50+
print(' Voltage : %s V' % data.get('V1_volts'))
51+
print(' Current : %s A' % data.get('A1_amperes'))
52+
print(' Power : %s W' % data.get('W1_watts'))
53+
print(' --- AC grid ---')
54+
print(' Voltage : %s V' % data.get('V2_volts'))
55+
print(' Current : %s A' % data.get('A2_amperes'))
56+
print(' Power : %s W' % data.get('W2_watts'))
57+
print(' Frequency : %s Hz' % data.get('Hz'))
58+
print(' Power factor : %s' % data.get('cos_phi'))
59+
print(' --- Other ---')
60+
print(' Temperature 1 : %s C' % data.get('temp1_C'))
61+
print(' Temperature 2 : %s C' % data.get('temp2_C'))
62+
print(' Energy total : %s kWh' % data.get('energy_kwh'))
63+
print(' WiFi signal : %s' % data.get('wifi_signal'))
64+
65+
def print_status(data):
66+
if not data:
67+
return
68+
print('-' * 40)
69+
print('Circuit status (DPS 24)')
70+
for key, val in data.items():
71+
print(' %s : %s' % (key, 'ON' if val else 'OFF'))
72+
73+
# ---------------------------------------------------------------------------
74+
# Example 1 — simple one-shot read
75+
# Connects, waits for the first real-time update and prints it.
76+
# ---------------------------------------------------------------------------
77+
78+
def example_oneshot():
79+
print('\n=== Example 1: one-shot read ===\n')
80+
81+
d = SoriaInverterDevice(
82+
dev_id = DEVICE_ID,
83+
address = DEVICE_IP,
84+
local_key = LOCAL_KEY,
85+
version = VERSION,
86+
)
87+
88+
d.receive() # initial handshake
89+
data = d.receive_and_update() # wait for first broadcast
90+
91+
print_realtime(d.get_realtime_power())
92+
print_full_report(d.get_full_report())
93+
print_status(d.get_circuit_status())
94+
95+
# ---------------------------------------------------------------------------
96+
# Example 2 — persistent monitoring loop
97+
# Keeps the connection open and prints every update as it arrives.
98+
# The device sends DPS 25 every ~2 s and DPS 21 every ~60 s.
99+
# ---------------------------------------------------------------------------
100+
101+
def example_monitor():
102+
print('\n=== Example 2: persistent monitor loop ===\n')
103+
print('Press Ctrl+C to stop.\n')
104+
105+
d = SoriaInverterDevice(
106+
dev_id = DEVICE_ID,
107+
address = DEVICE_IP,
108+
local_key = LOCAL_KEY,
109+
version = VERSION,
110+
persist = True,
111+
connection_timeout = 1,
112+
connection_retry_limit = 999,
113+
connection_retry_delay = 0.1,
114+
)
115+
116+
d.receive() # initial handshake
117+
print('Connected. Listening for updates...')
118+
119+
last_heartbeat = time.time()
120+
121+
try:
122+
while True:
123+
data = d.receive_and_update()
124+
125+
if data and 'dps' in data:
126+
dps_keys = list(data['dps'].keys())
127+
128+
# DPS 25 arrives every ~2 s — show real-time power
129+
if '25' in dps_keys:
130+
print_realtime(d.get_realtime_power())
131+
132+
# DPS 21 arrives every ~60 s — show full report
133+
if '21' in dps_keys:
134+
print_full_report(d.get_full_report())
135+
136+
# DPS 24 arrives on state changes
137+
if '24' in dps_keys:
138+
print_status(d.get_circuit_status())
139+
140+
# Send a heartbeat every 20 s to keep the connection alive
141+
if time.time() - last_heartbeat > 20:
142+
payload = d.generate_payload(tinytuya.HEART_BEAT)
143+
d.send(payload)
144+
last_heartbeat = time.time()
145+
146+
time.sleep(0.1)
147+
148+
except KeyboardInterrupt:
149+
print('\nStopped.')
150+
151+
# ---------------------------------------------------------------------------
152+
# Example 3 — using the cached status() method
153+
# status() returns the last known DPS values without querying the device.
154+
# ---------------------------------------------------------------------------
155+
156+
def example_cached_status():
157+
print('\n=== Example 3: cached status ===\n')
158+
159+
d = SoriaInverterDevice(
160+
dev_id = DEVICE_ID,
161+
address = DEVICE_IP,
162+
local_key = LOCAL_KEY,
163+
version = VERSION,
164+
persist = True,
165+
)
166+
167+
# Warm up the cache — wait for a few updates
168+
d.receive()
169+
for _ in range(5):
170+
d.receive_and_update()
171+
time.sleep(0.5)
172+
173+
# status() now returns cached data without touching the socket
174+
cached = d.status()
175+
print('Raw cached DPS keys: %s' % list(cached.get('dps', {}).keys()))
176+
177+
# Decoded values are always available from the getters
178+
print_realtime(d.get_realtime_power())
179+
print_full_report(d.get_full_report())
180+
181+
# ---------------------------------------------------------------------------
182+
# Main
183+
# ---------------------------------------------------------------------------
184+
185+
if __name__ == '__main__':
186+
# Run Example 1 by default.
187+
# Switch to example_monitor() for continuous monitoring.
188+
example_oneshot()
189+
190+
# Uncomment to run continuous monitoring:
191+
# example_monitor()
192+
193+
# Uncomment to demonstrate the cached status API:
194+
# example_cached_status()

0 commit comments

Comments
 (0)