Skip to content

Commit 86dc9d2

Browse files
authored
Merge pull request #654 from jasonacox/v1.17.5
Enhance CoverDevice with automatic command type detection
2 parents 5391bbf + ef895fb commit 86dc9d2

File tree

4 files changed

+310
-16
lines changed

4 files changed

+310
-16
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
@@ -1,5 +1,24 @@
11
# RELEASE NOTES
22

3+
## v1.17.5 - CoverDevice Enhanced Type Detection
4+
5+
* CoverDevice: Major rewrite to support 8 different device command types with automatic detection (credit for discovery: @make-all):
6+
* Type 1: `["open", "close", "stop", "continue"]` - Most curtains, blinds, roller shades (DEFAULT)
7+
* Type 2: `[true, false]` - Simple relays, garage doors, locks
8+
* Type 3: `["0", "1", "2"]` - String-numeric position/state
9+
* Type 4: `["00", "01", "02", "03"]` - Zero-prefixed numeric position/state
10+
* Type 5: `["fopen", "fclose"]` - Directional binary (no stop)
11+
* Type 6: `["on", "off", "stop"]` - Switch-lexicon
12+
* Type 7: `["up", "down", "stop"]` - Vertical-motion (lifts, hoists)
13+
* Type 8: `["ZZ", "FZ", "STOP"]` - Vendor-specific (Abalon-style, older standard)
14+
* Added `continue_cover()` method for device types that support it (Types 1 and 4)
15+
* Added `set_cover_type(type_id)` method to manually override auto-detection
16+
* Added `DEFAULT_COVER_TYPE` constant set to Type 1 (most comprehensive)
17+
* Device type is automatically detected on first command using priority ordering based on real-world frequency:
18+
* Priority: Type 1 (most common) → Type 8 (second most common, older standard) → Type 3 → others
19+
* Common DPS IDs: 1 (most common), 101 (second most common), 4 (dual-curtain second curtain)
20+
* Defaults to Type 1 if detection fails for best compatibility
21+
322
## 1.17.4 - Cloud Config
423

524
- 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

tinytuya/CoverDevice.py

Lines changed: 243 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,35 @@
1212
1313
Functions
1414
CoverDevice:
15-
open_cover(switch=1):
16-
close_cover(switch=1):
17-
stop_cover(switch=1):
15+
open_cover(switch=None, nowait=False) # Open the cover (switch defaults to DPS_INDEX_MOVE)
16+
close_cover(switch=None, nowait=False) # Close the cover (switch defaults to DPS_INDEX_MOVE)
17+
stop_cover(switch=None, nowait=False) # Stop the cover motion (switch defaults to DPS_INDEX_MOVE)
18+
continue_cover(switch=None, nowait=False) # Continue cover motion (if supported)
19+
set_cover_type(cover_type) # Manually set cover type (1-8)
20+
21+
Notes
22+
CoverDevice automatically detects the device type (1-8) based on status response:
23+
24+
Type 1: ["open", "close", "stop", "continue"] - Most curtains, blinds, roller shades (DEFAULT)
25+
Type 2: [true, false] - Simple relays, garage doors, locks
26+
Type 3: ["0", "1", "2"] - String-numeric position/state
27+
Type 4: ["00", "01", "02", "03"] - Zero-prefixed numeric position/state
28+
Type 5: ["fopen", "fclose"] - Directional binary (no stop)
29+
Type 6: ["on", "off", "stop"] - Switch-lexicon open/close
30+
Type 7: ["up", "down", "stop"] - Vertical-motion (lifts, hoists)
31+
Type 8: ["ZZ", "FZ", "STOP"] - Vendor-specific (Abalon-style, older standard)
32+
33+
Credit for discovery: @make-all in https://github.com/jasonacox/tinytuya/issues/653
34+
Detection occurs on first command by checking device status. Uses priority ordering
35+
to handle overlapping values (Type 1 has highest priority). Defaults to Type 1 if
36+
detection fails. You can manually override using set_cover_type(type_id) if needed.
37+
38+
Common DPS IDs:
39+
- DPS 1: Most common for cover control
40+
- DPS 101: Second most common (often backlight or secondary function)
41+
- DPS 4: Commonly used for second curtain in dual-curtain devices
42+
(DPS 2 and 3 typically for position write/read, DPS 5 and 6 for second curtain,
43+
with configuration and timers starting from DPS 7 onward)
1844
1945
Inherited
2046
json = status() # returns json payload
@@ -46,24 +72,229 @@
4672
class CoverDevice(Device):
4773
"""
4874
Represents a Tuya based Smart Window Cover.
75+
76+
Supports 8 different command types with automatic detection.
4977
"""
5078

5179
DPS_INDEX_MOVE = "1"
5280
DPS_INDEX_BL = "101"
81+
DEFAULT_COVER_TYPE = 1 # Default to Type 1 (most common)
5382

5483
DPS_2_STATE = {
5584
"1": "movement",
5685
"101": "backlight",
5786
}
5887

59-
def open_cover(self, switch=1, nowait=False):
60-
"""Open the cover"""
61-
self.set_status("on", switch, nowait=nowait)
88+
# Cover type command mappings
89+
COVER_TYPES = {
90+
1: { # Comprehensive movement class
91+
'open': 'open',
92+
'close': 'close',
93+
'stop': 'stop',
94+
'continue': 'continue',
95+
'detect_values': ['open', 'close', 'stop', 'continue']
96+
},
97+
2: { # Binary on/off class
98+
'open': True,
99+
'close': False,
100+
'stop': None, # Not supported
101+
'continue': None,
102+
'detect_values': [True, False]
103+
},
104+
3: { # String-numeric index class
105+
'open': '1',
106+
'close': '2',
107+
'stop': '0',
108+
'continue': None,
109+
'detect_values': ['0', '1', '2']
110+
},
111+
4: { # Zero-prefixed numeric index class
112+
'open': '01',
113+
'close': '02',
114+
'stop': '00',
115+
'continue': '03',
116+
'detect_values': ['00', '01', '02', '03']
117+
},
118+
5: { # Directional binary class
119+
'open': 'fopen',
120+
'close': 'fclose',
121+
'stop': None, # Not supported
122+
'continue': None,
123+
'detect_values': ['fopen', 'fclose']
124+
},
125+
6: { # Switch-lexicon class
126+
'open': 'on',
127+
'close': 'off',
128+
'stop': 'stop',
129+
'continue': None,
130+
'detect_values': ['on', 'off', 'stop']
131+
},
132+
7: { # Vertical-motion class
133+
'open': 'up',
134+
'close': 'down',
135+
'stop': 'stop',
136+
'continue': None,
137+
'detect_values': ['up', 'down', 'stop']
138+
},
139+
8: { # Vendor-specific class (Abalon-style)
140+
'open': 'ZZ',
141+
'close': 'FZ',
142+
'stop': 'STOP',
143+
'continue': None,
144+
'detect_values': ['ZZ', 'FZ', 'STOP']
145+
}
146+
}
147+
148+
def __init__(self, *args, **kwargs):
149+
super(CoverDevice, self).__init__(*args, **kwargs)
150+
self._cover_type_detected = False
151+
self._cover_type = None # Will be set to 1-8 after detection
152+
153+
def _detect_cover_type(self, switch=None):
154+
"""
155+
Automatically detect the cover device type (1-8) by checking device status.
156+
Uses priority ordering to handle overlapping values (e.g., 'stop' appears in Types 1, 6, 7).
157+
Type 1 has highest priority as it's the most comprehensive.
158+
159+
Args:
160+
switch (str/int): The DPS index to check. Defaults to DPS_INDEX_MOVE.
161+
"""
162+
if self._cover_type_detected:
163+
return
164+
165+
if switch is None:
166+
switch = self.DPS_INDEX_MOVE
167+
168+
# Set default to Type 1 (most comprehensive) before attempting detection
169+
self._cover_type = self.DEFAULT_COVER_TYPE
170+
171+
try:
172+
result = self.status()
173+
if result and 'dps' in result:
174+
dps_key = str(switch)
175+
dps_value = result['dps'].get(dps_key)
176+
177+
# Try to match the current value to a known cover type
178+
# Priority order: 1, 8, 3, 4, 5, 7, 2, 6 (most common to least common)
179+
# Type 1: Most common (comprehensive standard)
180+
# Type 8: Second most common (older vendor standard)
181+
# Type 3: Third most common (string-numeric)
182+
# Others: Rare variations
183+
if dps_value is not None:
184+
priority_order = [1, 8, 3, 4, 5, 7, 2, 6]
185+
for type_id in priority_order:
186+
type_info = self.COVER_TYPES[type_id]
187+
if dps_value in type_info['detect_values']:
188+
self._cover_type = type_id
189+
break
190+
191+
except Exception:
192+
# If status check fails, use default Type 1
193+
pass
194+
195+
self._cover_type_detected = True
196+
197+
def set_cover_type(self, cover_type):
198+
"""
199+
Manually set the cover device type.
200+
201+
Args:
202+
cover_type (int): Cover type ID (1-8).
203+
204+
Raises:
205+
ValueError: If cover_type is not between 1 and 8.
206+
207+
Example:
208+
cover.set_cover_type(1) # Set to Type 1 (open/close/stop/continue)
209+
cover.set_cover_type(6) # Set to Type 6 (on/off/stop)
210+
"""
211+
if cover_type not in self.COVER_TYPES:
212+
raise ValueError(f"Invalid cover_type: {cover_type}. Must be between 1 and 8.")
213+
214+
self._cover_type = cover_type
215+
self._cover_type_detected = True
216+
217+
def _get_command(self, action, switch=None):
218+
"""
219+
Get the appropriate command for the detected cover type.
220+
221+
Args:
222+
action (str): The action to perform ('open', 'close', 'stop', 'continue').
223+
switch (str/int): The DPS index. Defaults to DPS_INDEX_MOVE.
224+
225+
Returns:
226+
The command value for the detected cover type, or None if not supported.
227+
"""
228+
if not self._cover_type_detected:
229+
self._detect_cover_type(switch)
230+
231+
if self._cover_type and self._cover_type in self.COVER_TYPES:
232+
return self.COVER_TYPES[self._cover_type].get(action)
233+
234+
return None
235+
236+
def open_cover(self, switch=None, nowait=False):
237+
"""
238+
Open the cover.
239+
240+
Args:
241+
switch (str/int): The DPS index. Defaults to DPS_INDEX_MOVE.
242+
nowait (bool): Don't wait for device response.
243+
"""
244+
if switch is None:
245+
switch = self.DPS_INDEX_MOVE
246+
247+
command = self._get_command('open', switch)
248+
if command is not None:
249+
self.set_value(switch, command, nowait=nowait)
250+
251+
def close_cover(self, switch=None, nowait=False):
252+
"""
253+
Close the cover.
254+
255+
Args:
256+
switch (str/int): The DPS index. Defaults to DPS_INDEX_MOVE.
257+
nowait (bool): Don't wait for device response.
258+
"""
259+
if switch is None:
260+
switch = self.DPS_INDEX_MOVE
261+
262+
command = self._get_command('close', switch)
263+
if command is not None:
264+
self.set_value(switch, command, nowait=nowait)
62265

63-
def close_cover(self, switch=1, nowait=False):
64-
"""Close the cover"""
65-
self.set_status("off", switch, nowait=nowait)
266+
def stop_cover(self, switch=None, nowait=False):
267+
"""
268+
Stop the cover motion.
269+
270+
Args:
271+
switch (str/int): The DPS index. Defaults to DPS_INDEX_MOVE.
272+
nowait (bool): Don't wait for device response.
273+
274+
Note:
275+
Not all cover types support stop. Types 2 and 5 do not have a stop command.
276+
"""
277+
if switch is None:
278+
switch = self.DPS_INDEX_MOVE
279+
280+
command = self._get_command('stop', switch)
281+
if command is not None:
282+
self.set_value(switch, command, nowait=nowait)
66283

67-
def stop_cover(self, switch=1, nowait=False):
68-
"""Stop the motion of the cover"""
69-
self.set_status("stop", switch, nowait=nowait)
284+
def continue_cover(self, switch=None, nowait=False):
285+
"""
286+
Continue the cover motion (if supported).
287+
288+
Args:
289+
switch (str/int): The DPS index. Defaults to DPS_INDEX_MOVE.
290+
nowait (bool): Don't wait for device response.
291+
292+
Note:
293+
Only Type 1 and Type 4 support the continue command.
294+
"""
295+
if switch is None:
296+
switch = self.DPS_INDEX_MOVE
297+
298+
command = self._get_command('continue', switch)
299+
if command is not None:
300+
self.set_value(switch, command, nowait=nowait)

tinytuya/core/core.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@
101101
if HAVE_COLORAMA:
102102
init()
103103

104-
version_tuple = (1, 17, 4) # Major, Minor, Patch
104+
version_tuple = (1, 17, 5) # Major, Minor, Patch
105105
version = __version__ = "%d.%d.%d" % version_tuple
106106
__author__ = "jasonacox"
107107

0 commit comments

Comments
 (0)