jasonacox / tinytuya

Python API for Tuya WiFi smart devices using a direct local area network (LAN) connection or the cloud (TuyaCloud API).
MIT License
879 stars 159 forks source link

MappedDevice Phase II #370

Open uzlonewolf opened 1 year ago

uzlonewolf commented 1 year ago

Draft for now as there are still a few FIXME's and errant print()'s, but I wanted some feedback on the direction I was going.

The scanner has also been updated to use it. Instead of

### Thermostat   Product ID = sqkxklkleeasfk8w  [Valid Broadcast]:
    Address = ###   Device ID = ### (len:22)  Local Key = ###  Version = 3.3  Type = default, MAC = ###
    Status: {'2': 'cool', '16': 2650, '17': 80, '23': 'f', '24': 2400, '29': 75, '34': 51, '45': 0, '107': '4', '108': 2650, '109': 1500, '110': 80, '111': 59, '115': 'auto', '116': '1', '119': False, '120': 'followschedule', '123': 20, '129': 'alloff'}

you now get

### Thermostat   Product ID = sqkxklkleeasfk8w  [Valid Broadcast]:
    Address = ###   Device ID = ### (len:22)  Local Key = ###  Version = 3.3  Type = default, MAC = ###
    Status: {'mode': 'cool', 'temp_set': 26.5, 'temp_set_f': 80, 'temp_unit_convert': 'f', 'temp_current': 23.0, 'temp_current_f': 73, 'humidity': 50, 'fault': (), 'hvacsystemtype': '4', 'cool_set_temp': 26.5, 'heat_temp_set': 15.0, 'cool_set_temp_f': 80, 'heat_temp_set_f': 59, 'fanmode': 'auto', 'homemode': '1', 'schedule_enable': False, 'setpointholdmode': 'followschedule', 'fancylcetime': 20, 'relaystatus': 'alloff'}

As you can see, scaled ints are automatically turned into floats, enums are changed to their labels, and bitmasks are now lists of labels. When setting, everything is converted back as needed.

When updates are received either via a status() response or an async update, the dps dict keys are mapped to the DP names. If the received value is different than the last received value then the name is also added to a 'changed' list:

DEBUG:decoded results='{"dps":{"1":false},"t":1687500932}'
{'dps': {'switch_1': False}, 't': 1687500932, 'changed': ['switch_1']}
...
DEBUG:decoded results='{"dps":{"1":false},"t":1687500933}'
{'dps': {'switch_1': False}, 't': 1687500933, 'changed': []} # same value as last time, so not in 'changed'

In addition to this, the last received value for any particular DP can be retrieved at any time:

d = MappedDevice(...)
d.status()
...
last_value = d['switch_1']
last_value = d.dps['switch_1'].value # same as above

The d.dps[...] object can also be used to get the int_min/int_max/enum_range/bitmask values for those data types as well as the DP ID and any name(s) associated with that DP, in addition to the last known (parsed) value and raw value.

Example d.dps[...] object for a bitmap:

print(d.dps['fault'])
{'dp': '19', 'name': 'fault', 'names': ['19', 'fault', 'fault_alt_name'], 'raw_value': 1, 'value': ('cooling_fault',), 'enum_range': None, 'int_min': None, 'int_max': None, 'int_step': None, 'int_scale': None, 'bitmap': ('cooling_fault', 'heating_fault', 'temp_dif_fault'), 'bitmap_maxlen': 3}

Setting new values can be done in a similar dict-like manner; all of these are equivalent:

d = MappedDevice(...)
...
d['switch_1'] = True
d[1] = True
d.dps['switch_1'].value = True
d.dps[1].value = True
d.set_value( 'switch_1', True )
d.set_value( 1, True )
d.turn_on( 'switch_1' )
d.turn_on() # works since default is switch=1

Since dict-like access cannot pass the nowait flag, there is now a device-level switch for it:

d.set_nowait( True )
# or
d.set_nowait( False )

Todo: Implement the 'Json' data type, implement d.set_multiple_values(), implement d.set_timer(), and cleanup and documentation.

Closes #185

jasonacox commented 1 year ago

I love this! Going to play with it a bit more and drop any comments in-line...

uzlonewolf commented 1 year ago

Got almost everything implemented. Json/array handling got a bit convoluted as things like bulb scene data are integers inside a dict inside a list inside another dict, but it seems to be functional. Still needs a lot more testing and cleanup.

jasonacox commented 1 year ago

Scanner Run

SmartBulb   Product ID = keycuag84ttsx3fm  [Valid Broadcast]:
    Address = 10.0.1.83   Device ID = x (len:20)  Local Key = x  Version = 3.3  Type = default, MAC = b8:f0:09:01:3c:c3
    Status: {'switch_led': True, 'work_mode': 'white', 'bright_value_v2': 1000, 'colour_data_v2': {'h': 0, 's': 0, 'v': 3}, 'scene_data_v2': {'scene_num': 0, 'scene_units': [{'unit_change_mode': 7, 'unit_switch_duration': 4, 'unit_gradient_duration': 6, 'bright': 70, 'temperature': 2, 'h': 0, 's': 0, 'v': 3}]}, 'countdown_1': 0}
Dining Room   Product ID = MShdslm9Uw7Q59nN  [Valid Broadcast]:
    Address = 10.0.1.45   Device ID = x (len:20)  Local Key = x  Version = 3.3  Type = default, MAC = 2c:f4:32:a1:87:91
    Status: {'switch_1': False, 'countdown_1': 0}
Plug   Product ID = jllxx3xzvgweahib  [Valid Broadcast]:
    Address = 10.0.1.48   Device ID = x (len:20)  Local Key = x  Version = 3.3  Type = default, MAC = bc:dd:c2:3d:1b:11
    Status: {'switch': True, 'countdown_1': 0, 'cur_current': 20, 'cur_power': 13, 'cur_voltage': 1192}

❤️ I absolutely love seeing the translated (human readable) DPS keys. I do wonder if we should provide an option to show DPS index numbers instead, maybe -raw or -dps?

In a scanner run... likely debug, but some lines like this show up:

read_data() failed, retrying 10.0.1.96     
read_data() failed, retrying 10.0.1.32             
read_data() failed, retrying 10.0.1.46

And...

parsing JSON json {'code': 'colour_data_v2', 'type': 'Json', 'raw_values': '{"h":{"min":0,"scale":0,"unit":"","max":360,"step":1},"s":{"min":0,"scale":0,"unit":"","max":1000,"step":1},"v":{"min":0,"scale":0,"unit":"","max":1000,"step":1}}', 'values': {'h': {'min': 0, 'scale': 0, 'unit': '', 'max': 360, 'step': 1}, 's': {'min': 0, 'scale': 0, 'unit': '', 'max': 1000, 'step': 1}, 'v': {'min': 0, 'scale': 0, 'unit': '', 'max': 1000, 'step': 1}}}
JSON key h subtype Integer
JSON key s subtype Integer
JSON key v subtype Integer
Value len: 6 {'code': 'colour_data_v2', 'type': 'Json', 'raw_values': '{"h":{"min":0,"scale":0,"unit":"","max":360,"step":1},"s":{"min":0,"scale":0,"unit":"","max":1000,"step":1},"v":{"min":0,"scale":0,"unit":"","max":1000,"step":1}}', 'values': {'h': {'min': 0, 'scale': 0, 'unit': '', 'max': 360, 'step': 1}, 's': {'min': 0, 'scale': 0, 'unit': '', 'max': 1000, 'step': 1}, 'v': {'min': 0, 'scale': 0, 'unit': '', 'max': 1000, 'step': 1}}}
parsing JSON json {'code': 'scene_data_v2', 'type': 'Json', 'raw_values': '{"scene_num":{"min":1,"scale":0,"max":8,"step":1},"scene_units": {"unit_change_mode":{"range":["static","jump","gradient"]},"unit_switch_duration":{"min":0,"scale":0,"max":100,"step":1},"unit_gradient_duration":{"min":0,"scale":0,"max":100,"step":1},"bright":{"min":0,"scale":0,"max":1000,"step":1},"temperature":{"min":0,"scale":0,"max":1000,"step":1},"h":{"min":0,"scale":0,"unit":"","max":360,"step":1},"s":{"min":0,"scale":0,"unit":"","max":1000,"step":1},"v":{"min":0,"scale":0,"unit":"","max":1000,"step":1}}}', 'values': {'scene_num': {'min': 1, 'scale': 0, 'max': 8, 'step': 1}, 'scene_units': {'unit_change_mode': {'range': ['static', 'jump', 'gradient']}, 'unit_switch_duration': {'min': 0, 'scale': 0, 'max': 100, 'step': 1}, 'unit_gradient_duration': {'min': 0, 'scale': 0, 'max': 100, 'step': 1}, 'bright': {'min': 0, 'scale': 0, 'max': 1000, 'step': 1}, 'temperature': {'min': 0, 'scale': 0, 'max': 1000, 'step': 1}, 'h': {'min': 0, 'scale': 0, 'unit': '', 'max': 360, 'step': 1}, 's': {'min': 0, 'scale': 0, 'unit': '', 'max': 1000, 'step': 1}, 'v': {'min': 0, 'scale': 0, 'unit': '', 'max': 1000, 'step': 1}}}}
JSON key scene_num subtype Integer
JSON key scene_units subtype Array
parsing Array array {'type': 'Array', 'values': {'unit_change_mode': {'range': ['static', 'jump', 'gradient']}, 'unit_switch_duration': {'min': 0, 'scale': 0, 'max': 100, 'step': 1}, 'unit_gradient_duration': {'min': 0, 'scale': 0, 'max': 100, 'step': 1}, 'bright': {'min': 0, 'scale': 0, 'max': 1000, 'step': 1}, 'temperature': {'min': 0, 'scale': 0, 'max': 1000, 'step': 1}, 'h': {'min': 0, 'scale': 0, 'unit': '', 'max': 360, 'step': 1}, 's': {'min': 0, 'scale': 0, 'unit': '', 'max': 1000, 'step': 1}, 'v': {'min': 0, 'scale': 0, 'unit': '', 'max': 1000, 'step': 1}}}
parsing JSON json {'type': 'Json', 'values': {'unit_change_mode': {'range': ['static', 'jump', 'gradient']}, 'unit_switch_duration': {'min': 0, 'scale': 0, 'max': 100, 'step': 1}, 'unit_gradient_duration': {'min': 0, 'scale': 0, 'max': 100, 'step': 1}, 'bright': {'min': 0, 'scale': 0, 'max': 1000, 'step': 1}, 'temperature': {'min': 0, 'scale': 0, 'max': 1000, 'step': 1}, 'h': {'min': 0, 'scale': 0, 'unit': '', 'max': 360, 'step': 1}, 's': {'min': 0, 'scale': 0, 'unit': '', 'max': 1000, 'step': 1}, 'v': {'min': 0, 'scale': 0, 'unit': '', 'max': 1000, 'step': 1}}}
JSON key unit_change_mode subtype Enum_Integer
JSON key unit_switch_duration subtype Integer
JSON key unit_gradient_duration subtype Integer
JSON key bright subtype Integer
JSON key temperature subtype Integer
JSON key h subtype Integer
JSON key s subtype Integer
JSON key v subtype Integer
Value len: 13 {'type': 'Json', 'values': {'unit_change_mode': {'range': ['static', 'jump', 'gradient']}, 'unit_switch_duration': {'min': 0, 'scale': 0, 'max': 100, 'step': 1}, 'unit_gradient_duration': {'min': 0, 'scale': 0, 'max': 100, 'step': 1}, 'bright': {'min': 0, 'scale': 0, 'max': 1000, 'step': 1}, 'temperature': {'min': 0, 'scale': 0, 'max': 1000, 'step': 1}, 'h': {'min': 0, 'scale': 0, 'unit': '', 'max': 360, 'step': 1}, 's': {'min': 0, 'scale': 0, 'unit': '', 'max': 1000, 'step': 1}, 'v': {'min': 0, 'scale': 0, 'unit': '', 'max': 1000, 'step': 1}}}
Value len: 14 {'code': 'scene_data_v2', 'type': 'Json', 'raw_values': '{"scene_num":{"min":1,"scale":0,"max":8,"step":1},"scene_units": {"unit_change_mode":{"range":["static","jump","gradient"]},"unit_switch_duration":{"min":0,"scale":0,"max":100,"step":1},"unit_gradient_duration":{"min":0,"scale":0,"max":100,"step":1},"bright":{"min":0,"scale":0,"max":1000,"step":1},"temperature":{"min":0,"scale":0,"max":1000,"step":1},"h":{"min":0,"scale":0,"unit":"","max":360,"step":1},"s":{"min":0,"scale":0,"unit":"","max":1000,"step":1},"v":{"min":0,"scale":0,"unit":"","max":1000,"step":1}}}', 'values': {'scene_num': {'min': 1, 'scale': 0, 'max': 8, 'step': 1}, 'scene_units': {'unit_change_mode': {'range': ['static', 'jump', 'gradient']}, 'unit_switch_duration': {'min': 0, 'scale': 0, 'max': 100, 'step': 1}, 'unit_gradient_duration': {'min': 0, 'scale': 0, 'max': 100, 'step': 1}, 'bright': {'min': 0, 'scale': 0, 'max': 1000, 'step': 1}, 'temperature': {'min': 0, 'scale': 0, 'max': 1000, 'step': 1}, 'h': {'min': 0, 'scale': 0, 'unit': '', 'max': 360, 'step': 1}, 's': {'min': 0, 'scale': 0, 'unit': '', 'max': 1000, 'step': 1}, 'v': {'min': 0, 'scale': 0, 'unit': '', 'max': 1000, 'step': 1}}}}
parsing JSON json {'code': 'music_data', 'type': 'Json', 'raw_values': '{"change_mode":{"range":["direct","gradient"]}, "bright":{"min":0,"scale":0,"unit":"","max":1000,"step":1}, "temperature":{"min":0,"scale":0,"unit":"","max":1000,"step":1}, "h":{"min":0,"scale":0,"unit":"","max":360,"step":1},"s":{"min":0,"scale":0,"unit":"","max":255,"step":1},"v":{"min":0,"scale":0,"unit":"","max":255,"step":1}}', 'values': {'change_mode': {'range': ['direct', 'gradient']}, 'bright': {'min': 0, 'scale': 0, 'unit': '', 'max': 1000, 'step': 1}, 'temperature': {'min': 0, 'scale': 0, 'unit': '', 'max': 1000, 'step': 1}, 'h': {'min': 0, 'scale': 0, 'unit': '', 'max': 360, 'step': 1}, 's': {'min': 0, 'scale': 0, 'unit': '', 'max': 255, 'step': 1}, 'v': {'min': 0, 'scale': 0, 'unit': '', 'max': 255, 'step': 1}}}
JSON key change_mode subtype Enum_Integer
JSON key bright subtype Integer
JSON key temperature subtype Integer
JSON key h subtype Integer
JSON key s subtype Integer
JSON key v subtype Integer
Value len: 9 {'code': 'music_data', 'type': 'Json', 'raw_values': '{"change_mode":{"range":["direct","gradient"]}, "bright":{"min":0,"scale":0,"unit":"","max":1000,"step":1}, "temperature":{"min":0,"scale":0,"unit":"","max":1000,"step":1}, "h":{"min":0,"scale":0,"unit":"","max":360,"step":1},"s":{"min":0,"scale":0,"unit":"","max":255,"step":1},"v":{"min":0,"scale":0,"unit":"","max":255,"step":1}}', 'values': {'change_mode': {'range': ['direct', 'gradient']}, 'bright': {'min': 0, 'scale': 0, 'unit': '', 'max': 1000, 'step': 1}, 'temperature': {'min': 0, 'scale': 0, 'unit': '', 'max': 1000, 'step': 1}, 'h': {'min': 0, 'scale': 0, 'unit': '', 'max': 360, 'step': 1}, 's': {'min': 0, 'scale': 0, 'unit': '', 'max': 255, 'step': 1}, 'v': {'min': 0, 'scale': 0, 'unit': '', 'max': 255, 'step': 1}}}
parsing JSON json {'code': 'control_data', 'type': 'Json', 'raw_values': '{"change_mode":{"range":["direct","gradient"]}, "bright":{"min":0,"scale":0,"unit":"","max":1000,"step":1}, "temperature":{"min":0,"scale":0,"unit":"","max":1000,"step":1}, "h":{"min":0,"scale":0,"unit":"","max":360,"step":1},"s":{"min":0,"scale":0,"unit":"","max":255,"step":1},"v":{"min":0,"scale":0,"unit":"","max":255,"step":1}}', 'values': {'change_mode': {'range': ['direct', 'gradient']}, 'bright': {'min': 0, 'scale': 0, 'unit': '', 'max': 1000, 'step': 1}, 'temperature': {'min': 0, 'scale': 0, 'unit': '', 'max': 1000, 'step': 1}, 'h': {'min': 0, 'scale': 0, 'unit': '', 'max': 360, 'step': 1}, 's': {'min': 0, 'scale': 0, 'unit': '', 'max': 255, 'step': 1}, 'v': {'min': 0, 'scale': 0, 'unit': '', 'max': 255, 'step': 1}}}
JSON key change_mode subtype Enum_Integer
JSON key bright subtype Integer
JSON key temperature subtype Integer
JSON key h subtype Integer
JSON key s subtype Integer
JSON key v subtype Integer
Value len: 9 {'code': 'control_data', 'type': 'Json', 'raw_values': '{"change_mode":{"range":["direct","gradient"]}, "bright":{"min":0,"scale":0,"unit":"","max":1000,"step":1}, "temperature":{"min":0,"scale":0,"unit":"","max":1000,"step":1}, "h":{"min":0,"scale":0,"unit":"","max":360,"step":1},"s":{"min":0,"scale":0,"unit":"","max":255,"step":1},"v":{"min":0,"scale":0,"unit":"","max":255,"step":1}}', 'values': {'change_mode': {'range': ['direct', 'gradient']}, 'bright': {'min': 0, 'scale': 0, 'unit': '', 'max': 1000, 'step': 1}, 'temperature': {'min': 0, 'scale': 0, 'unit': '', 'max': 1000, 'step': 1}, 'h': {'min': 0, 'scale': 0, 'unit': '', 'max': 360, 'step': 1}, 's': {'min': 0, 'scale': 0, 'unit': '', 'max': 255, 'step': 1}, 'v': {'min': 0, 'scale': 0, 'unit': '', 'max': 255, 'step': 1}}}
parsing JSON json {'code': 'colour_data_v2', 'type': 'Json', 'raw_values': '{"h":{"min":0,"scale":0,"unit":"","max":360,"step":1},"s":{"min":0,"scale":0,"unit":"","max":1000,"step":1},"v":{"min":0,"scale":0,"unit":"","max":1000,"step":1}}', 'values': {'h': {'min': 0, 'scale': 0, 'unit': '', 'max': 360, 'step': 1}, 's': {'min': 0, 'scale': 0, 'unit': '', 'max': 1000, 'step': 1}, 'v': {'min': 0, 'scale': 0, 'unit': '', 'max': 1000, 'step': 1}}}
JSON key h subtype Integer
JSON key s subtype Integer
JSON key v subtype Integer
Value len: 6 {'code': 'colour_data_v2', 'type': 'Json', 'raw_values': '{"h":{"min":0,"scale":0,"unit":"","max":360,"step":1},"s":{"min":0,"scale":0,"unit":"","max":1000,"step":1},"v":{"min":0,"scale":0,"unit":"","max":1000,"step":1}}', 'values': {'h': {'min': 0, 'scale': 0, 'unit': '', 'max': 360, 'step': 1}, 's': {'min': 0, 'scale': 0, 'unit': '', 'max': 1000, 'step': 1}, 'v': {'min': 0, 'scale': 0, 'unit': '', 'max': 1000, 'step': 1}}}
parsing JSON json {'code': 'scene_data_v2', 'type': 'Json', 'raw_values': '{"scene_num":{"min":1,"scale":0,"max":8,"step":1},"scene_units": {"unit_change_mode":{"range":["static","jump","gradient"]},"unit_switch_duration":{"min":0,"scale":0,"max":100,"step":1},"unit_gradient_duration":{"min":0,"scale":0,"max":100,"step":1},"bright":{"min":0,"scale":0,"max":1000,"step":1},"temperature":{"min":0,"scale":0,"max":1000,"step":1},"h":{"min":0,"scale":0,"unit":"","max":360,"step":1},"s":{"min":0,"scale":0,"unit":"","max":1000,"step":1},"v":{"min":0,"scale":0,"unit":"","max":1000,"step":1}}}', 'values': {'scene_num': {'min': 1, 'scale': 0, 'max': 8, 'step': 1}, 'scene_units': {'unit_change_mode': {'range': ['static', 'jump', 'gradient']}, 'unit_switch_duration': {'min': 0, 'scale': 0, 'max': 100, 'step': 1}, 'unit_gradient_duration': {'min': 0, 'scale': 0, 'max': 100, 'step': 1}, 'bright': {'min': 0, 'scale': 0, 'max': 1000, 'step': 1}, 'temperature': {'min': 0, 'scale': 0, 'max': 1000, 'step': 1}, 'h': {'min': 0, 'scale': 0, 'unit': '', 'max': 360, 'step': 1}, 's': {'min': 0, 'scale': 0, 'unit': '', 'max': 1000, 'step': 1}, 'v': {'min': 0, 'scale': 0, 'unit': '', 'max': 1000, 'step': 1}}}}
JSON key scene_num subtype Integer
JSON key scene_units subtype Array
parsing Array array {'type': 'Array', 'values': {'unit_change_mode': {'range': ['static', 'jump', 'gradient']}, 'unit_switch_duration': {'min': 0, 'scale': 0, 'max': 100, 'step': 1}, 'unit_gradient_duration': {'min': 0, 'scale': 0, 'max': 100, 'step': 1}, 'bright': {'min': 0, 'scale': 0, 'max': 1000, 'step': 1}, 'temperature': {'min': 0, 'scale': 0, 'max': 1000, 'step': 1}, 'h': {'min': 0, 'scale': 0, 'unit': '', 'max': 360, 'step': 1}, 's': {'min': 0, 'scale': 0, 'unit': '', 'max': 1000, 'step': 1}, 'v': {'min': 0, 'scale': 0, 'unit': '', 'max': 1000, 'step': 1}}}
parsing JSON json {'type': 'Json', 'values': {'unit_change_mode': {'range': ['static', 'jump', 'gradient']}, 'unit_switch_duration': {'min': 0, 'scale': 0, 'max': 100, 'step': 1}, 'unit_gradient_duration': {'min': 0, 'scale': 0, 'max': 100, 'step': 1}, 'bright': {'min': 0, 'scale': 0, 'max': 1000, 'step': 1}, 'temperature': {'min': 0, 'scale': 0, 'max': 1000, 'step': 1}, 'h': {'min': 0, 'scale': 0, 'unit': '', 'max': 360, 'step': 1}, 's': {'min': 0, 'scale': 0, 'unit': '', 'max': 1000, 'step': 1}, 'v': {'min': 0, 'scale': 0, 'unit': '', 'max': 1000, 'step': 1}}}
JSON key unit_change_mode subtype Enum_Integer
JSON key unit_switch_duration subtype Integer
JSON key unit_gradient_duration subtype Integer
JSON key bright subtype Integer
JSON key temperature subtype Integer
JSON key h subtype Integer
JSON key s subtype Integer
JSON key v subtype Integer
Value len: 13 {'type': 'Json', 'values': {'unit_change_mode': {'range': ['static', 'jump', 'gradient']}, 'unit_switch_duration': {'min': 0, 'scale': 0, 'max': 100, 'step': 1}, 'unit_gradient_duration': {'min': 0, 'scale': 0, 'max': 100, 'step': 1}, 'bright': {'min': 0, 'scale': 0, 'max': 1000, 'step': 1}, 'temperature': {'min': 0, 'scale': 0, 'max': 1000, 'step': 1}, 'h': {'min': 0, 'scale': 0, 'unit': '', 'max': 360, 'step': 1}, 's': {'min': 0, 'scale': 0, 'unit': '', 'max': 1000, 'step': 1}, 'v': {'min': 0, 'scale': 0, 'unit': '', 'max': 1000, 'step': 1}}}
Value len: 14 {'code': 'scene_data_v2', 'type': 'Json', 'raw_values': '{"scene_num":{"min":1,"scale":0,"max":8,"step":1},"scene_units": {"unit_change_mode":{"range":["static","jump","gradient"]},"unit_switch_duration":{"min":0,"scale":0,"max":100,"step":1},"unit_gradient_duration":{"min":0,"scale":0,"max":100,"step":1},"bright":{"min":0,"scale":0,"max":1000,"step":1},"temperature":{"min":0,"scale":0,"max":1000,"step":1},"h":{"min":0,"scale":0,"unit":"","max":360,"step":1},"s":{"min":0,"scale":0,"unit":"","max":1000,"step":1},"v":{"min":0,"scale":0,"unit":"","max":1000,"step":1}}}', 'values': {'scene_num': {'min': 1, 'scale': 0, 'max': 8, 'step': 1}, 'scene_units': {'unit_change_mode': {'range': ['static', 'jump', 'gradient']}, 'unit_switch_duration': {'min': 0, 'scale': 0, 'max': 100, 'step': 1}, 'unit_gradient_duration': {'min': 0, 'scale': 0, 'max': 100, 'step': 1}, 'bright': {'min': 0, 'scale': 0, 'max': 1000, 'step': 1}, 'temperature': {'min': 0, 'scale': 0, 'max': 1000, 'step': 1}, 'h': {'min': 0, 'scale': 0, 'unit': '', 'max': 360, 'step': 1}, 's': {'min': 0, 'scale': 0, 'unit': '', 'max': 1000, 'step': 1}, 'v': {'min': 0, 'scale': 0, 'unit': '', 'max': 1000, 'step': 1}}}}
parsing JSON json {'code': 'music_data', 'type': 'Json', 'raw_values': '{"change_mode":{"range":["direct","gradient"]}, "bright":{"min":0,"scale":0,"unit":"","max":1000,"step":1}, "temperature":{"min":0,"scale":0,"unit":"","max":1000,"step":1}, "h":{"min":0,"scale":0,"unit":"","max":360,"step":1},"s":{"min":0,"scale":0,"unit":"","max":255,"step":1},"v":{"min":0,"scale":0,"unit":"","max":255,"step":1}}', 'values': {'change_mode': {'range': ['direct', 'gradient']}, 'bright': {'min': 0, 'scale': 0, 'unit': '', 'max': 1000, 'step': 1}, 'temperature': {'min': 0, 'scale': 0, 'unit': '', 'max': 1000, 'step': 1}, 'h': {'min': 0, 'scale': 0, 'unit': '', 'max': 360, 'step': 1}, 's': {'min': 0, 'scale': 0, 'unit': '', 'max': 255, 'step': 1}, 'v': {'min': 0, 'scale': 0, 'unit': '', 'max': 255, 'step': 1}}}
JSON key change_mode subtype Enum_Integer
JSON key bright subtype Integer
JSON key temperature subtype Integer
JSON key h subtype Integer
JSON key s subtype Integer
JSON key v subtype Integer
Value len: 9 {'code': 'music_data', 'type': 'Json', 'raw_values': '{"change_mode":{"range":["direct","gradient"]}, "bright":{"min":0,"scale":0,"unit":"","max":1000,"step":1}, "temperature":{"min":0,"scale":0,"unit":"","max":1000,"step":1}, "h":{"min":0,"scale":0,"unit":"","max":360,"step":1},"s":{"min":0,"scale":0,"unit":"","max":255,"step":1},"v":{"min":0,"scale":0,"unit":"","max":255,"step":1}}', 'values': {'change_mode': {'range': ['direct', 'gradient']}, 'bright': {'min': 0, 'scale': 0, 'unit': '', 'max': 1000, 'step': 1}, 'temperature': {'min': 0, 'scale': 0, 'unit': '', 'max': 1000, 'step': 1}, 'h': {'min': 0, 'scale': 0, 'unit': '', 'max': 360, 'step': 1}, 's': {'min': 0, 'scale': 0, 'unit': '', 'max': 255, 'step': 1}, 'v': {'min': 0, 'scale': 0, 'unit': '', 'max': 255, 'step': 1}}}
parsing JSON json {'code': 'control_data', 'type': 'Json', 'raw_values': '{"change_mode":{"range":["direct","gradient"]}, "bright":{"min":0,"scale":0,"unit":"","max":1000,"step":1}, "temperature":{"min":0,"scale":0,"unit":"","max":1000,"step":1}, "h":{"min":0,"scale":0,"unit":"","max":360,"step":1},"s":{"min":0,"scale":0,"unit":"","max":255,"step":1},"v":{"min":0,"scale":0,"unit":"","max":255,"step":1}}', 'values': {'change_mode': {'range': ['direct', 'gradient']}, 'bright': {'min': 0, 'scale': 0, 'unit': '', 'max': 1000, 'step': 1}, 'temperature': {'min': 0, 'scale': 0, 'unit': '', 'max': 1000, 'step': 1}, 'h': {'min': 0, 'scale': 0, 'unit': '', 'max': 360, 'step': 1}, 's': {'min': 0, 'scale': 0, 'unit': '', 'max': 255, 'step': 1}, 'v': {'min': 0, 'scale': 0, 'unit': '', 'max': 255, 'step': 1}}}
JSON key change_mode subtype Enum_Integer
JSON key bright subtype Integer
JSON key temperature subtype Integer
JSON key h subtype Integer
JSON key s subtype Integer
JSON key v subtype Integer
Value len: 9 {'code': 'control_data', 'type': 'Json', 'raw_values': '{"change_mode":{"range":["direct","gradient"]}, "bright":{"min":0,"scale":0,"unit":"","max":1000,"step":1}, "temperature":{"min":0,"scale":0,"unit":"","max":1000,"step":1}, "h":{"min":0,"scale":0,"unit":"","max":360,"step":1},"s":{"min":0,"scale":0,"unit":"","max":255,"step":1},"v":{"min":0,"scale":0,"unit":"","max":255,"step":1}}', 'values': {'change_mode': {'range': ['direct', 'gradient']}, 'bright': {'min': 0, 'scale': 0, 'unit': '', 'max': 1000, 'step': 1}, 'temperature': {'min': 0, 'scale': 0, 'unit': '', 'max': 1000, 'step': 1}, 'h': {'min': 0, 'scale': 0, 'unit': '', 'max': 360, 'step': 1}, 's': {'min': 0, 'scale': 0, 'unit': '', 'max': 255, 'step': 1}, 'v': {'min': 0, 'scale': 0, 'unit': '', 'max': 255, 'step': 1}}}
uzlonewolf commented 1 year ago

I can definitely add that, but I'm not really sure it's needed for the scanner. I'm probably going to put the raw values in a "dps_raw" key and modify the "changed" list to return the dps object instead of just the name, and people can then check snapshot.json after running the scanner if they really need the raw values.

jasonacox commented 1 year ago

Sold! 😄 We can always add it later. Thanks LW! This is awesome.

uzlonewolf commented 1 year ago

Done. I also pulled out a bunch of debugging print()s to help with the flood while scanning.

spinza commented 11 months ago
#!/usr/bin/env python
import tinytuya

device = tinytuya.MappedDevice(
    dev_id="XXX", local_key="XXX", persist=True
)
device.set_version(3.3)

device["switch"] = True

Results in:

Traceback (most recent call last):
  File "./test.py", line 9, in <module>
    device["switch"] = True
  File "/home/xxx/source/tinytuya/tinytuya/MappedDevice.py", line 800, in __setitem__
    return self.set_value( key, new_value )
  File "/home/xxx/source/tinytuya/tinytuya/MappedDevice.py", line 826, in set_value
    new_value = obj.encode_value( value, False )
TypeError: encode_value() takes 2 positional arguments but 3 were given

I believe here it should be changed from this:

    def encode_value( self, new_value ):
        return self.obj.encode_value( new_value, False )

to this:

     def encode_value( self, new_value , pack = False ):
        return self.obj.encode_value( new_value, pack=pack )
uzlonewolf commented 11 months ago

Thank you for the bug report, @spinza ! Yes, that does look like a good way to fix it.

spinza commented 11 months ago

I've written a Tuya MQTT device bridge using the MappedDevice class in this PR. It works on any device found in a local devices.json file and publishes the device information to MQTT using the Homie convention. It listens for messages for the fields that are settable. So also allows MQTT control of Tuya devices. This bridge publishes Tuya messages as they are received to MQTT and vice versa. It also regularly polls the status of Tuya devices and publishes them to MQTT.

I am not sure if bitmaps are settable but that's the only function that won't work (if possible). I.e. you cannot set bitmap values from MQTT to Tuya but Tuya to MQTT flows work.

This class was very useful to generalise this.

My use case for this is OpenHAB. OpenHAB now automatically discovers Tuya devices as Things on the MQTT OpenHab integration. It also sets up appropriate channels with the right units etc.

Some of the unit functionality won't work as well but I don't have enough devices (I only have two) to test units.

uzlonewolf commented 11 months ago

Hmm, I wonder if it would be a good idea to expand bitmasks into an array of true/false values which you could set like d["somebitmask"].someflag = True or d["somebitmask"]["someflag"] = False (setting everything in 1 go with d["somebitmask"] = ["someflag", "anotherflag"] would still work too of course). I would make each flag its own top-level item as if it were a DP, but then we run the risk of having bit names collide with actual DP names or a different bitmask DP with the same bit names. Maybe if we used an unused character as a separator such as d["somebitmask.someflag"] = False?

spinza commented 11 months ago

That is essentially what I've done in the MQTT bridge. It takes the bitmap and publishes it as a series of boolean topics. With the bridge there is a risk of name collisions though.

spinza commented 11 months ago

The code I mention is available here

spinza commented 11 months ago

Can you make the above fix to your code, so if someone install it they can at least get my server working?

uzlonewolf commented 11 months ago

@spinza Fixed.

It's not functional yet, but I also started on expanding bitmaps. d = MappedDevice( ..., expand_bitmaps='.' ) (the default) will expand them to "dp_name.bit_option", expand_bitmaps=True makes them just "bit_option" (watch out for name collisions with legit DPs!), and any value which evaluates to False (None, '', 0, etc) shuts it off.

spinza commented 11 months ago

Cool. Is the default behaviour unchanged with regard to bitmaps?

uzlonewolf commented 11 months ago

Kinda? I haven't finished implementing it yet so at the moment nothing has changed. When I'm done the "parent" DP bitmap will remain the same as it is now, but there will be additional pseudo-DPs added for each bit option. To shut that off (thereby keeping everything exactly the same as it is now) set d = MappedDevice( ..., expand_bitmaps=False )

spinza commented 11 months ago

Can a bitmap be settable?

uzlonewolf commented 11 months ago

Assuming the device allows it, yes. Currently you can set it either with an int (i.e. d['some_bitmap'] = 0xC0) or with a list (i.e. d['some_bitmap'] = ['degrees_f', 'fan_auto']). It will throw a ValueError if the int is too large or an unknown bit flag is included in the list.