|
12 | 12 |
|
13 | 13 | Functions |
14 | 14 | 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) |
18 | 44 |
|
19 | 45 | Inherited |
20 | 46 | json = status() # returns json payload |
|
46 | 72 | class CoverDevice(Device): |
47 | 73 | """ |
48 | 74 | Represents a Tuya based Smart Window Cover. |
| 75 | + |
| 76 | + Supports 8 different command types with automatic detection. |
49 | 77 | """ |
50 | 78 |
|
51 | 79 | DPS_INDEX_MOVE = "1" |
52 | 80 | DPS_INDEX_BL = "101" |
| 81 | + DEFAULT_COVER_TYPE = 1 # Default to Type 1 (most common) |
53 | 82 |
|
54 | 83 | DPS_2_STATE = { |
55 | 84 | "1": "movement", |
56 | 85 | "101": "backlight", |
57 | 86 | } |
58 | 87 |
|
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) |
62 | 265 |
|
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) |
66 | 283 |
|
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) |
0 commit comments