From 8b7daecba628be25752b67d267028db7ce753b1b Mon Sep 17 00:00:00 2001 From: Jason Cox Date: Sat, 11 Oct 2025 22:42:58 -0700 Subject: [PATCH 1/5] Enhance CoverDevice with automatic command type detection #653 --- README.md | 35 ++++++++++++++++++++-- RELEASE.md | 4 +++ tinytuya/CoverDevice.py | 64 +++++++++++++++++++++++++++++++++++++---- tinytuya/core/core.py | 2 +- 4 files changed, 96 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 27961085..bd286469 100644 --- a/README.md +++ b/README.md @@ -236,9 +236,14 @@ BulbDevice Additional Functions result = state(): CoverDevice Additional Functions - open_cover(switch=1): - close_cover(switch=1): - stop_cover(switch=1): + open_cover(switch=1, nowait=False): + close_cover(switch=1, nowait=False): + stop_cover(switch=1, nowait=False): + set_cover_command_type(use_open_close=True): # Manually set command type ("open"/"close" vs "on"/"off") + + Note: CoverDevice automatically detects whether the device uses "open"/"close" or + "on"/"off" commands by checking the device status on first use. You can manually + override this detection using set_cover_command_type() if needed. Cloud Functions setregion(apiRegion) @@ -349,6 +354,30 @@ d.set_mode('scene') # Scene Example: Set Color Rotation Scene d.set_value(25, '07464602000003e803e800000000464602007803e803e80000000046460200f003e803e800000000464602003d03e803e80000000046460200ae03e803e800000000464602011303e803e800000000') +""" +Cover Device (Window Shade) +""" +c = tinytuya.CoverDevice('DEVICE_ID_HERE', 'IP_ADDRESS_HERE', 'LOCAL_KEY_HERE') +c.set_version(3.3) +data = c.status() + +# Show status +print('Dictionary %r' % data) + +# Open the cover +c.open_cover() + +# Close the cover +c.close_cover() + +# Stop the cover +c.stop_cover() + +# Manually set command type if auto-detection doesn't work +# Some devices use "open"/"close", others use "on"/"off" +c.set_cover_command_type(True) # Use "open"/"close" commands +c.set_cover_command_type(False) # Use "on"/"off" commands + ``` ### Example Device Monitor diff --git a/RELEASE.md b/RELEASE.md index c16c71a5..ef274aac 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,5 +1,9 @@ # RELEASE NOTES +## v1.17.5 - CoverDevice Command Detection + +* CoverDevice: Add automatic detection of device command type ("open"/"close" vs "on"/"off") by lazy polling device status on first use. This allows CoverDevice to work with both device types without manual configuration. Defaults to "on"/"off" for backward compatibility. + ## 1.17.4 - Cloud Config - 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 diff --git a/tinytuya/CoverDevice.py b/tinytuya/CoverDevice.py index ed43d580..0d65eb50 100644 --- a/tinytuya/CoverDevice.py +++ b/tinytuya/CoverDevice.py @@ -12,9 +12,17 @@ Functions CoverDevice: - open_cover(switch=1): - close_cover(switch=1): - stop_cover(switch=1): + open_cover(switch=1, nowait=False) # Open the cover + close_cover(switch=1, nowait=False) # Close the cover + stop_cover(switch=1, nowait=False) # Stop the cover motion + set_cover_command_type(use_open_close=True) # Manually set command type + + Notes + CoverDevice will automatically detect the command type used by the device: + - Some devices use "open"/"close" commands + - Other devices use "on"/"off" commands + Detection occurs on first open_cover() or close_cover() call by checking + the device status. Defaults to "on"/"off" for backward compatibility. Inherited json = status() # returns json payload @@ -56,13 +64,59 @@ class CoverDevice(Device): "101": "backlight", } + def __init__(self, *args, **kwargs): + super(CoverDevice, self).__init__(*args, **kwargs) + self._cover_commands_detected = False + self._use_open_close = False # Default to "on"/"off" + + def _detect_cover_commands(self): + """ + Lazy detection of cover command type by checking device status. + Some devices use "open"/"close", others use "on"/"off". + This method is called automatically on first open/close command. + """ + if self._cover_commands_detected: + return + + try: + result = self.status() + if result and 'dps' in result: + dps_value = result['dps'].get(self.DPS_INDEX_MOVE) + if dps_value in ['open', 'close']: + self._use_open_close = True + # else: keep default False (use "on"/"off") + except Exception: + # If status check fails, stick with default "on"/"off" + pass + + self._cover_commands_detected = True + + def set_cover_command_type(self, use_open_close=True): + """ + Manually set the cover command type. + + Args: + use_open_close (bool): If True, uses "open"/"close" commands. + If False, uses "on"/"off" commands. + + Example: + cover.set_cover_command_type(True) # Use "open"/"close" + cover.set_cover_command_type(False) # Use "on"/"off" + """ + self._use_open_close = use_open_close + self._cover_commands_detected = True # Prevent auto-detection + def open_cover(self, switch=1, nowait=False): """Open the cover""" - self.set_status("on", switch, nowait=nowait) + self._detect_cover_commands() + command = "open" if self._use_open_close else "on" + self.set_status(command, switch, nowait=nowait) def close_cover(self, switch=1, nowait=False): """Close the cover""" - self.set_status("off", switch, nowait=nowait) + self._detect_cover_commands() + command = "close" if self._use_open_close else "off" + self.set_status(command, switch, nowait=nowait) def stop_cover(self, switch=1, nowait=False): """Stop the motion of the cover""" diff --git a/tinytuya/core/core.py b/tinytuya/core/core.py index e8122223..52974fd2 100644 --- a/tinytuya/core/core.py +++ b/tinytuya/core/core.py @@ -101,7 +101,7 @@ if HAVE_COLORAMA: init() -version_tuple = (1, 17, 4) # Major, Minor, Patch +version_tuple = (1, 17, 5) # Major, Minor, Patch version = __version__ = "%d.%d.%d" % version_tuple __author__ = "jasonacox" From 732a3040d88fd081d51d3ae83a7cbfd621942b47 Mon Sep 17 00:00:00 2001 From: Jason Cox Date: Sat, 11 Oct 2025 22:57:53 -0700 Subject: [PATCH 2/5] Refactor CoverDevice methods to use DPS_INDEX_MOVE as default switch value --- tinytuya/CoverDevice.py | 40 +++++++++++++++++++++++++++------------- 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/tinytuya/CoverDevice.py b/tinytuya/CoverDevice.py index 0d65eb50..a4de7d08 100644 --- a/tinytuya/CoverDevice.py +++ b/tinytuya/CoverDevice.py @@ -12,15 +12,15 @@ Functions CoverDevice: - open_cover(switch=1, nowait=False) # Open the cover - close_cover(switch=1, nowait=False) # Close the cover - stop_cover(switch=1, nowait=False) # Stop the cover motion - set_cover_command_type(use_open_close=True) # Manually set command type + open_cover(switch=None, nowait=False) # Open the cover (switch defaults to DPS_INDEX_MOVE) + close_cover(switch=None, nowait=False) # Close the cover (switch defaults to DPS_INDEX_MOVE) + stop_cover(switch=None, nowait=False) # Stop the cover motion (switch defaults to DPS_INDEX_MOVE) + set_cover_command_type(use_open_close=True) # Manually set command type Notes CoverDevice will automatically detect the command type used by the device: - Some devices use "open"/"close" commands - - Other devices use "on"/"off" commands + - Other devices use "on"/"off" commands Detection occurs on first open_cover() or close_cover() call by checking the device status. Defaults to "on"/"off" for backward compatibility. @@ -69,26 +69,34 @@ def __init__(self, *args, **kwargs): self._cover_commands_detected = False self._use_open_close = False # Default to "on"/"off" - def _detect_cover_commands(self): + def _detect_cover_commands(self, switch=None): """ Lazy detection of cover command type by checking device status. Some devices use "open"/"close", others use "on"/"off". This method is called automatically on first open/close command. + + Args: + switch (str/int): The DPS index to check for command type detection. + Defaults to DPS_INDEX_MOVE if not specified. """ if self._cover_commands_detected: return + if switch is None: + switch = self.DPS_INDEX_MOVE + try: result = self.status() if result and 'dps' in result: - dps_value = result['dps'].get(self.DPS_INDEX_MOVE) + dps_key = str(switch) + dps_value = result['dps'].get(dps_key) if dps_value in ['open', 'close']: self._use_open_close = True # else: keep default False (use "on"/"off") except Exception: # If status check fails, stick with default "on"/"off" pass - + self._cover_commands_detected = True def set_cover_command_type(self, use_open_close=True): @@ -106,18 +114,24 @@ def set_cover_command_type(self, use_open_close=True): self._use_open_close = use_open_close self._cover_commands_detected = True # Prevent auto-detection - def open_cover(self, switch=1, nowait=False): + def open_cover(self, switch=None, nowait=False): """Open the cover""" - self._detect_cover_commands() + if switch is None: + switch = self.DPS_INDEX_MOVE + self._detect_cover_commands(switch) command = "open" if self._use_open_close else "on" self.set_status(command, switch, nowait=nowait) - def close_cover(self, switch=1, nowait=False): + def close_cover(self, switch=None, nowait=False): """Close the cover""" - self._detect_cover_commands() + if switch is None: + switch = self.DPS_INDEX_MOVE + self._detect_cover_commands(switch) command = "close" if self._use_open_close else "off" self.set_status(command, switch, nowait=nowait) - def stop_cover(self, switch=1, nowait=False): + def stop_cover(self, switch=None, nowait=False): """Stop the motion of the cover""" + if switch is None: + switch = self.DPS_INDEX_MOVE self.set_status("stop", switch, nowait=nowait) From 70810f61f60457101172b2930c4fbd9fe691e7fe Mon Sep 17 00:00:00 2001 From: Jason Cox Date: Wed, 29 Oct 2025 22:10:40 -0700 Subject: [PATCH 3/5] Enhance CoverDevice with support for 8 command types and automatic detection #653 --- README.md | 35 +++++-- RELEASE.md | 18 +++- tinytuya/CoverDevice.py | 223 +++++++++++++++++++++++++++++++++------- 3 files changed, 223 insertions(+), 53 deletions(-) diff --git a/README.md b/README.md index bd286469..238ed7bb 100644 --- a/README.md +++ b/README.md @@ -236,14 +236,23 @@ BulbDevice Additional Functions result = state(): CoverDevice Additional Functions - open_cover(switch=1, nowait=False): - close_cover(switch=1, nowait=False): - stop_cover(switch=1, nowait=False): - set_cover_command_type(use_open_close=True): # Manually set command type ("open"/"close" vs "on"/"off") + open_cover(switch=None, nowait=False): + close_cover(switch=None, nowait=False): + stop_cover(switch=None, nowait=False): + continue_cover(switch=None, nowait=False): + set_cover_type(cover_type): # Manually set cover type (1-8) - Note: CoverDevice automatically detects whether the device uses "open"/"close" or - "on"/"off" commands by checking the device status on first use. You can manually - override this detection using set_cover_command_type() if needed. + CoverDevice automatically detects one of 8 device types by checking status: + Type 1: ["open", "close", "stop", "continue"] - Most curtains, blinds, roller shades + Type 2: [true, false] - Simple relays, garage doors, locks + Type 3: ["0", "1", "2"] - String-numeric position/state + Type 4: [1, 2, 3] - Integer-numeric position/state + Type 5: ["fopen", "fclose"] - Directional binary (no stop) + Type 6: ["on", "off", "stop"] - Switch-lexicon open/close (default) + Type 7: ["up", "down", "stop"] - Vertical-motion (lifts, hoists) + Type 8: ["ZZ", "FZ", "STOP"] - Vendor-specific (Abalon-style) + + You can manually override detection using set_cover_type(type_id) if needed. Cloud Functions setregion(apiRegion) @@ -364,6 +373,9 @@ data = c.status() # Show status print('Dictionary %r' % data) +# CoverDevice will automatically detect the device type (1-8) +# and use the appropriate commands + # Open the cover c.open_cover() @@ -373,10 +385,11 @@ c.close_cover() # Stop the cover c.stop_cover() -# Manually set command type if auto-detection doesn't work -# Some devices use "open"/"close", others use "on"/"off" -c.set_cover_command_type(True) # Use "open"/"close" commands -c.set_cover_command_type(False) # Use "on"/"off" commands +# Continue cover motion (if supported by device type) +c.continue_cover() + +# Manually set cover type if auto-detection doesn't work +c.set_cover_type(1) # Force Type 1 (open/close/stop/continue) ``` ### Example Device Monitor diff --git a/RELEASE.md b/RELEASE.md index ef274aac..a4d09896 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,8 +1,20 @@ # RELEASE NOTES -## v1.17.5 - CoverDevice Command Detection - -* CoverDevice: Add automatic detection of device command type ("open"/"close" vs "on"/"off") by lazy polling device status on first use. This allows CoverDevice to work with both device types without manual configuration. Defaults to "on"/"off" for backward compatibility. +## v1.17.5 - CoverDevice Enhanced Type Detection + +* CoverDevice: Major rewrite to support 8 different device command types with automatic detection (credit for discovery: @make-all): + * Type 1: `["open", "close", "stop", "continue"]` - Most curtains, blinds, roller shades + * Type 2: `[true, false]` - Simple relays, garage doors, locks + * Type 3: `["0", "1", "2"]` - String-numeric position/state + * Type 4: `[1, 2, 3]` - Integer-numeric position/state + * Type 5: `["fopen", "fclose"]` - Directional binary (no stop) + * Type 6: `["on", "off", "stop"]` - Switch-lexicon (default) + * Type 7: `["up", "down", "stop"]` - Vertical-motion (lifts, hoists) + * Type 8: `["ZZ", "FZ", "STOP"]` - Vendor-specific (Abalon-style) +* Added `continue_cover()` method for device types that support it (Types 1 and 4) +* Added `set_cover_type(type_id)` method to manually override auto-detection +* Device type is automatically detected on first command by checking device status +* Defaults to Type 6 (on/off/stop) if detection fails for backward compatibility ## 1.17.4 - Cloud Config diff --git a/tinytuya/CoverDevice.py b/tinytuya/CoverDevice.py index a4de7d08..f205fe1e 100644 --- a/tinytuya/CoverDevice.py +++ b/tinytuya/CoverDevice.py @@ -15,14 +15,24 @@ open_cover(switch=None, nowait=False) # Open the cover (switch defaults to DPS_INDEX_MOVE) close_cover(switch=None, nowait=False) # Close the cover (switch defaults to DPS_INDEX_MOVE) stop_cover(switch=None, nowait=False) # Stop the cover motion (switch defaults to DPS_INDEX_MOVE) - set_cover_command_type(use_open_close=True) # Manually set command type + continue_cover(switch=None, nowait=False) # Continue cover motion (if supported) + set_cover_type(cover_type) # Manually set cover type (1-8) Notes - CoverDevice will automatically detect the command type used by the device: - - Some devices use "open"/"close" commands - - Other devices use "on"/"off" commands - Detection occurs on first open_cover() or close_cover() call by checking - the device status. Defaults to "on"/"off" for backward compatibility. + CoverDevice automatically detects the device type (1-8) based on status response: + + Type 1: ["open", "close", "stop", "continue"] - Most curtains, blinds, roller shades + Type 2: [true, false] - Simple relays, garage doors, locks + Type 3: ["0", "1", "2"] - String-numeric position/state + Type 4: [1, 2, 3] - Integer-numeric position/state + Type 5: ["fopen", "fclose"] - Directional binary (no stop) + Type 6: ["on", "off", "stop"] - Switch-lexicon open/close + Type 7: ["up", "down", "stop"] - Vertical-motion (lifts, hoists) + Type 8: ["ZZ", "FZ", "STOP"] - Vendor-specific (Abalon-style) + + Credit for discovery: @make-all in https://github.com/jasonacox/tinytuya/issues/653 + Detection occurs on first command by checking device status. You can manually + override using set_cover_type(type_id) if needed. Inherited json = status() # returns json payload @@ -54,6 +64,8 @@ class CoverDevice(Device): """ Represents a Tuya based Smart Window Cover. + + Supports 8 different command types with automatic detection. """ DPS_INDEX_MOVE = "1" @@ -64,74 +76,207 @@ class CoverDevice(Device): "101": "backlight", } + # Cover type command mappings + COVER_TYPES = { + 1: { # Comprehensive movement class + 'open': 'open', + 'close': 'close', + 'stop': 'stop', + 'continue': 'continue', + 'detect_values': ['open', 'close', 'stop', 'continue'] + }, + 2: { # Binary on/off class + 'open': True, + 'close': False, + 'stop': None, # Not supported + 'continue': None, + 'detect_values': [True, False] + }, + 3: { # String-numeric index class + 'open': '1', + 'close': '2', + 'stop': '0', + 'continue': None, + 'detect_values': ['0', '1', '2'] + }, + 4: { # Integer-numeric index class + 'open': 1, + 'close': 2, + 'stop': 0, + 'continue': 3, + 'detect_values': [0, 1, 2, 3] + }, + 5: { # Directional binary class + 'open': 'fopen', + 'close': 'fclose', + 'stop': None, # Not supported + 'continue': None, + 'detect_values': ['fopen', 'fclose'] + }, + 6: { # Switch-lexicon class + 'open': 'on', + 'close': 'off', + 'stop': 'stop', + 'continue': None, + 'detect_values': ['on', 'off', 'stop'] + }, + 7: { # Vertical-motion class + 'open': 'up', + 'close': 'down', + 'stop': 'stop', + 'continue': None, + 'detect_values': ['up', 'down', 'stop'] + }, + 8: { # Vendor-specific class (Abalon-style) + 'open': 'ZZ', + 'close': 'FZ', + 'stop': 'STOP', + 'continue': None, + 'detect_values': ['ZZ', 'FZ', 'STOP'] + } + } + def __init__(self, *args, **kwargs): super(CoverDevice, self).__init__(*args, **kwargs) - self._cover_commands_detected = False - self._use_open_close = False # Default to "on"/"off" + self._cover_type_detected = False + self._cover_type = None # Will be set to 1-8 after detection - def _detect_cover_commands(self, switch=None): + def _detect_cover_type(self, switch=None): """ - Lazy detection of cover command type by checking device status. - Some devices use "open"/"close", others use "on"/"off". - This method is called automatically on first open/close command. + Automatically detect the cover device type (1-8) by checking device status. Args: - switch (str/int): The DPS index to check for command type detection. - Defaults to DPS_INDEX_MOVE if not specified. + switch (str/int): The DPS index to check. Defaults to DPS_INDEX_MOVE. """ - if self._cover_commands_detected: + if self._cover_type_detected: return if switch is None: switch = self.DPS_INDEX_MOVE + # Set default to Type 6 (on/off/stop) before attempting detection + self._cover_type = 6 + try: result = self.status() if result and 'dps' in result: dps_key = str(switch) dps_value = result['dps'].get(dps_key) - if dps_value in ['open', 'close']: - self._use_open_close = True - # else: keep default False (use "on"/"off") + + # Try to match the current value to a known cover type + if dps_value is not None: + for type_id, type_info in self.COVER_TYPES.items(): + if dps_value in type_info['detect_values']: + self._cover_type = type_id + break + except Exception: - # If status check fails, stick with default "on"/"off" + # If status check fails, use default Type 6 (on/off/stop) pass + + self._cover_type_detected = True - self._cover_commands_detected = True - - def set_cover_command_type(self, use_open_close=True): + def set_cover_type(self, cover_type): """ - Manually set the cover command type. + Manually set the cover device type. Args: - use_open_close (bool): If True, uses "open"/"close" commands. - If False, uses "on"/"off" commands. + cover_type (int): Cover type ID (1-8). + + Raises: + ValueError: If cover_type is not between 1 and 8. Example: - cover.set_cover_command_type(True) # Use "open"/"close" - cover.set_cover_command_type(False) # Use "on"/"off" + cover.set_cover_type(1) # Set to Type 1 (open/close/stop/continue) + cover.set_cover_type(6) # Set to Type 6 (on/off/stop) + """ + if cover_type not in self.COVER_TYPES: + raise ValueError(f"Invalid cover_type: {cover_type}. Must be between 1 and 8.") + + self._cover_type = cover_type + self._cover_type_detected = True + + def _get_command(self, action, switch=None): + """ + Get the appropriate command for the detected cover type. + + Args: + action (str): The action to perform ('open', 'close', 'stop', 'continue'). + switch (str/int): The DPS index. Defaults to DPS_INDEX_MOVE. + + Returns: + The command value for the detected cover type, or None if not supported. """ - self._use_open_close = use_open_close - self._cover_commands_detected = True # Prevent auto-detection + if not self._cover_type_detected: + self._detect_cover_type(switch) + + if self._cover_type and self._cover_type in self.COVER_TYPES: + return self.COVER_TYPES[self._cover_type].get(action) + + return None def open_cover(self, switch=None, nowait=False): - """Open the cover""" + """ + Open the cover. + + Args: + switch (str/int): The DPS index. Defaults to DPS_INDEX_MOVE. + nowait (bool): Don't wait for device response. + """ if switch is None: switch = self.DPS_INDEX_MOVE - self._detect_cover_commands(switch) - command = "open" if self._use_open_close else "on" - self.set_status(command, switch, nowait=nowait) + + command = self._get_command('open', switch) + if command is not None: + self.set_value(switch, command, nowait=nowait) def close_cover(self, switch=None, nowait=False): - """Close the cover""" + """ + Close the cover. + + Args: + switch (str/int): The DPS index. Defaults to DPS_INDEX_MOVE. + nowait (bool): Don't wait for device response. + """ if switch is None: switch = self.DPS_INDEX_MOVE - self._detect_cover_commands(switch) - command = "close" if self._use_open_close else "off" - self.set_status(command, switch, nowait=nowait) + + command = self._get_command('close', switch) + if command is not None: + self.set_value(switch, command, nowait=nowait) def stop_cover(self, switch=None, nowait=False): - """Stop the motion of the cover""" + """ + Stop the cover motion. + + Args: + switch (str/int): The DPS index. Defaults to DPS_INDEX_MOVE. + nowait (bool): Don't wait for device response. + + Note: + Not all cover types support stop. Types 2 and 5 do not have a stop command. + """ if switch is None: switch = self.DPS_INDEX_MOVE - self.set_status("stop", switch, nowait=nowait) + + command = self._get_command('stop', switch) + if command is not None: + self.set_value(switch, command, nowait=nowait) + + def continue_cover(self, switch=None, nowait=False): + """ + Continue the cover motion (if supported). + + Args: + switch (str/int): The DPS index. Defaults to DPS_INDEX_MOVE. + nowait (bool): Don't wait for device response. + + Note: + Only Type 1 and Type 4 support the continue command. + """ + if switch is None: + switch = self.DPS_INDEX_MOVE + + command = self._get_command('continue', switch) + if command is not None: + self.set_value(switch, command, nowait=nowait) From b96da804417cdbac772ffc10f58c884b25c5eb97 Mon Sep 17 00:00:00 2001 From: Jason Cox Date: Wed, 29 Oct 2025 22:28:31 -0700 Subject: [PATCH 4/5] Enhance CoverDevice documentation and detection logic for improved automatic type identification --- README.md | 7 ++++--- RELEASE.md | 10 ++++++---- tinytuya/CoverDevice.py | 21 ++++++++++++++------- 3 files changed, 24 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 238ed7bb..42ad8b85 100644 --- a/README.md +++ b/README.md @@ -243,16 +243,17 @@ CoverDevice Additional Functions set_cover_type(cover_type): # Manually set cover type (1-8) CoverDevice automatically detects one of 8 device types by checking status: - Type 1: ["open", "close", "stop", "continue"] - Most curtains, blinds, roller shades + Type 1: ["open", "close", "stop", "continue"] - Most curtains, blinds, roller shades (DEFAULT) Type 2: [true, false] - Simple relays, garage doors, locks Type 3: ["0", "1", "2"] - String-numeric position/state Type 4: [1, 2, 3] - Integer-numeric position/state Type 5: ["fopen", "fclose"] - Directional binary (no stop) - Type 6: ["on", "off", "stop"] - Switch-lexicon open/close (default) + Type 6: ["on", "off", "stop"] - Switch-lexicon open/close Type 7: ["up", "down", "stop"] - Vertical-motion (lifts, hoists) Type 8: ["ZZ", "FZ", "STOP"] - Vendor-specific (Abalon-style) - You can manually override detection using set_cover_type(type_id) if needed. + Detection uses priority ordering to handle overlapping values. Type 1 has highest priority. + Defaults to Type 1 if detection fails. Manual override: set_cover_type(type_id). Cloud Functions setregion(apiRegion) diff --git a/RELEASE.md b/RELEASE.md index a4d09896..fc4ff7f0 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -3,18 +3,20 @@ ## v1.17.5 - CoverDevice Enhanced Type Detection * CoverDevice: Major rewrite to support 8 different device command types with automatic detection (credit for discovery: @make-all): - * Type 1: `["open", "close", "stop", "continue"]` - Most curtains, blinds, roller shades + * Type 1: `["open", "close", "stop", "continue"]` - Most curtains, blinds, roller shades (DEFAULT) * Type 2: `[true, false]` - Simple relays, garage doors, locks * Type 3: `["0", "1", "2"]` - String-numeric position/state * Type 4: `[1, 2, 3]` - Integer-numeric position/state * Type 5: `["fopen", "fclose"]` - Directional binary (no stop) - * Type 6: `["on", "off", "stop"]` - Switch-lexicon (default) + * Type 6: `["on", "off", "stop"]` - Switch-lexicon * Type 7: `["up", "down", "stop"]` - Vertical-motion (lifts, hoists) * Type 8: `["ZZ", "FZ", "STOP"]` - Vendor-specific (Abalon-style) * Added `continue_cover()` method for device types that support it (Types 1 and 4) * Added `set_cover_type(type_id)` method to manually override auto-detection -* Device type is automatically detected on first command by checking device status -* Defaults to Type 6 (on/off/stop) if detection fails for backward compatibility +* Added `DEFAULT_COVER_TYPE` constant set to Type 1 (most comprehensive) +* Device type is automatically detected on first command using priority ordering to handle overlapping values +* Type 1 has highest priority in detection to avoid misidentification +* Defaults to Type 1 if detection fails for best compatibility ## 1.17.4 - Cloud Config diff --git a/tinytuya/CoverDevice.py b/tinytuya/CoverDevice.py index f205fe1e..d260314c 100644 --- a/tinytuya/CoverDevice.py +++ b/tinytuya/CoverDevice.py @@ -21,7 +21,7 @@ Notes CoverDevice automatically detects the device type (1-8) based on status response: - Type 1: ["open", "close", "stop", "continue"] - Most curtains, blinds, roller shades + Type 1: ["open", "close", "stop", "continue"] - Most curtains, blinds, roller shades (DEFAULT) Type 2: [true, false] - Simple relays, garage doors, locks Type 3: ["0", "1", "2"] - String-numeric position/state Type 4: [1, 2, 3] - Integer-numeric position/state @@ -31,8 +31,9 @@ Type 8: ["ZZ", "FZ", "STOP"] - Vendor-specific (Abalon-style) Credit for discovery: @make-all in https://github.com/jasonacox/tinytuya/issues/653 - Detection occurs on first command by checking device status. You can manually - override using set_cover_type(type_id) if needed. + Detection occurs on first command by checking device status. Uses priority ordering + to handle overlapping values (Type 1 has highest priority). Defaults to Type 1 if + detection fails. You can manually override using set_cover_type(type_id) if needed. Inherited json = status() # returns json payload @@ -70,6 +71,7 @@ class CoverDevice(Device): DPS_INDEX_MOVE = "1" DPS_INDEX_BL = "101" + DEFAULT_COVER_TYPE = 1 # Default to Type 1 (most common) DPS_2_STATE = { "1": "movement", @@ -144,6 +146,8 @@ def __init__(self, *args, **kwargs): def _detect_cover_type(self, switch=None): """ Automatically detect the cover device type (1-8) by checking device status. + Uses priority ordering to handle overlapping values (e.g., 'stop' appears in Types 1, 6, 7). + Type 1 has highest priority as it's the most comprehensive. Args: switch (str/int): The DPS index to check. Defaults to DPS_INDEX_MOVE. @@ -154,8 +158,8 @@ def _detect_cover_type(self, switch=None): if switch is None: switch = self.DPS_INDEX_MOVE - # Set default to Type 6 (on/off/stop) before attempting detection - self._cover_type = 6 + # Set default to Type 1 (most comprehensive) before attempting detection + self._cover_type = self.DEFAULT_COVER_TYPE try: result = self.status() @@ -164,14 +168,17 @@ def _detect_cover_type(self, switch=None): dps_value = result['dps'].get(dps_key) # Try to match the current value to a known cover type + # Priority order: 1, 8, 7, 5, 4, 3, 2, 6 (most specific to least specific) if dps_value is not None: - for type_id, type_info in self.COVER_TYPES.items(): + priority_order = [1, 8, 7, 5, 4, 3, 2, 6] + for type_id in priority_order: + type_info = self.COVER_TYPES[type_id] if dps_value in type_info['detect_values']: self._cover_type = type_id break except Exception: - # If status check fails, use default Type 6 (on/off/stop) + # If status check fails, use default Type 1 pass self._cover_type_detected = True From ef895fb41214a37cd356bc8cd523e3bcef0a3500 Mon Sep 17 00:00:00 2001 From: Jason Cox Date: Thu, 30 Oct 2025 22:24:33 -0700 Subject: [PATCH 5/5] Refactor CoverDevice command types and detection logic for clarity and accuracy #653 --- README.md | 7 ++++--- RELEASE.md | 9 +++++---- tinytuya/CoverDevice.py | 31 +++++++++++++++++++++---------- 3 files changed, 30 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 42ad8b85..b523ad69 100644 --- a/README.md +++ b/README.md @@ -246,14 +246,15 @@ CoverDevice Additional Functions Type 1: ["open", "close", "stop", "continue"] - Most curtains, blinds, roller shades (DEFAULT) Type 2: [true, false] - Simple relays, garage doors, locks Type 3: ["0", "1", "2"] - String-numeric position/state - Type 4: [1, 2, 3] - Integer-numeric position/state + Type 4: ["00", "01", "02", "03"] - Zero-prefixed numeric position/state Type 5: ["fopen", "fclose"] - Directional binary (no stop) Type 6: ["on", "off", "stop"] - Switch-lexicon open/close Type 7: ["up", "down", "stop"] - Vertical-motion (lifts, hoists) - Type 8: ["ZZ", "FZ", "STOP"] - Vendor-specific (Abalon-style) + Type 8: ["ZZ", "FZ", "STOP"] - Vendor-specific (Abalon-style, older standard) - Detection uses priority ordering to handle overlapping values. Type 1 has highest priority. + Detection uses priority ordering based on real-world frequency (Type 1 → Type 8 → Type 3 → others). Defaults to Type 1 if detection fails. Manual override: set_cover_type(type_id). + Common DPS IDs: 1 (most common), 101 (second most common), 4 (dual-curtain second curtain). Cloud Functions setregion(apiRegion) diff --git a/RELEASE.md b/RELEASE.md index fc4ff7f0..031eb14f 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -6,16 +6,17 @@ * Type 1: `["open", "close", "stop", "continue"]` - Most curtains, blinds, roller shades (DEFAULT) * Type 2: `[true, false]` - Simple relays, garage doors, locks * Type 3: `["0", "1", "2"]` - String-numeric position/state - * Type 4: `[1, 2, 3]` - Integer-numeric position/state + * Type 4: `["00", "01", "02", "03"]` - Zero-prefixed numeric position/state * Type 5: `["fopen", "fclose"]` - Directional binary (no stop) * Type 6: `["on", "off", "stop"]` - Switch-lexicon * Type 7: `["up", "down", "stop"]` - Vertical-motion (lifts, hoists) - * Type 8: `["ZZ", "FZ", "STOP"]` - Vendor-specific (Abalon-style) + * Type 8: `["ZZ", "FZ", "STOP"]` - Vendor-specific (Abalon-style, older standard) * Added `continue_cover()` method for device types that support it (Types 1 and 4) * Added `set_cover_type(type_id)` method to manually override auto-detection * Added `DEFAULT_COVER_TYPE` constant set to Type 1 (most comprehensive) -* Device type is automatically detected on first command using priority ordering to handle overlapping values -* Type 1 has highest priority in detection to avoid misidentification +* Device type is automatically detected on first command using priority ordering based on real-world frequency: + * Priority: Type 1 (most common) → Type 8 (second most common, older standard) → Type 3 → others + * Common DPS IDs: 1 (most common), 101 (second most common), 4 (dual-curtain second curtain) * Defaults to Type 1 if detection fails for best compatibility ## 1.17.4 - Cloud Config diff --git a/tinytuya/CoverDevice.py b/tinytuya/CoverDevice.py index d260314c..dfd1c065 100644 --- a/tinytuya/CoverDevice.py +++ b/tinytuya/CoverDevice.py @@ -24,16 +24,23 @@ Type 1: ["open", "close", "stop", "continue"] - Most curtains, blinds, roller shades (DEFAULT) Type 2: [true, false] - Simple relays, garage doors, locks Type 3: ["0", "1", "2"] - String-numeric position/state - Type 4: [1, 2, 3] - Integer-numeric position/state + Type 4: ["00", "01", "02", "03"] - Zero-prefixed numeric position/state Type 5: ["fopen", "fclose"] - Directional binary (no stop) Type 6: ["on", "off", "stop"] - Switch-lexicon open/close Type 7: ["up", "down", "stop"] - Vertical-motion (lifts, hoists) - Type 8: ["ZZ", "FZ", "STOP"] - Vendor-specific (Abalon-style) + Type 8: ["ZZ", "FZ", "STOP"] - Vendor-specific (Abalon-style, older standard) Credit for discovery: @make-all in https://github.com/jasonacox/tinytuya/issues/653 Detection occurs on first command by checking device status. Uses priority ordering to handle overlapping values (Type 1 has highest priority). Defaults to Type 1 if detection fails. You can manually override using set_cover_type(type_id) if needed. + + Common DPS IDs: + - DPS 1: Most common for cover control + - DPS 101: Second most common (often backlight or secondary function) + - DPS 4: Commonly used for second curtain in dual-curtain devices + (DPS 2 and 3 typically for position write/read, DPS 5 and 6 for second curtain, + with configuration and timers starting from DPS 7 onward) Inherited json = status() # returns json payload @@ -101,12 +108,12 @@ class CoverDevice(Device): 'continue': None, 'detect_values': ['0', '1', '2'] }, - 4: { # Integer-numeric index class - 'open': 1, - 'close': 2, - 'stop': 0, - 'continue': 3, - 'detect_values': [0, 1, 2, 3] + 4: { # Zero-prefixed numeric index class + 'open': '01', + 'close': '02', + 'stop': '00', + 'continue': '03', + 'detect_values': ['00', '01', '02', '03'] }, 5: { # Directional binary class 'open': 'fopen', @@ -168,9 +175,13 @@ def _detect_cover_type(self, switch=None): dps_value = result['dps'].get(dps_key) # Try to match the current value to a known cover type - # Priority order: 1, 8, 7, 5, 4, 3, 2, 6 (most specific to least specific) + # Priority order: 1, 8, 3, 4, 5, 7, 2, 6 (most common to least common) + # Type 1: Most common (comprehensive standard) + # Type 8: Second most common (older vendor standard) + # Type 3: Third most common (string-numeric) + # Others: Rare variations if dps_value is not None: - priority_order = [1, 8, 7, 5, 4, 3, 2, 6] + priority_order = [1, 8, 3, 4, 5, 7, 2, 6] for type_id in priority_order: type_info = self.COVER_TYPES[type_id] if dps_value in type_info['detect_values']: