Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MappedDevice Phase II #370

Draft
wants to merge 10 commits into
base: master
Choose a base branch
from
Draft

Conversation

uzlonewolf
Copy link
Collaborator

@uzlonewolf uzlonewolf commented Jun 23, 2023

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

include_map = not bool( answer[0:1].lower() == 'n' )
if answer[0:1].lower() == 'a':
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! Good addition.

print(output)
print( json.dumps(tuyadevices[:15], indent=4) )
if len(tuyadevices) > 15:
print("%s(%d more devices hidden)" % (normal, (len(tuyadevices) - 15)))
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hum... interesting. I'm not sure we want to truncate the list. It may be better to make the JSON output optional (via command line?) and just notify the user that the device files is written.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure why we need to display all off them. A few are nice to verify the download worked, but with the addition of device mappings they're absolutely flooding the screen (I'm up to 130 devices and every time I ran the wizard it was dumping all 14,793 lines of devices.json to my screen). Perhaps add another prompt?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a prompt. If you have <= 15 devices it asks:

3 devices downloaded, display? (Y/n):

and more than that gets:

130 devices downloaded, display? ([Y]es/[n]o/[s]ome):

As 15 was still flooding my screen, if you answer "s" it then displays just the first 10.

@jasonacox
Copy link
Owner

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

@uzlonewolf
Copy link
Collaborator Author

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
Copy link
Owner

jasonacox commented Jul 2, 2023

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
Copy link
Collaborator Author

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
Copy link
Owner

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

@uzlonewolf
Copy link
Collaborator Author

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

or
MappedDevice.dps.some_dp_name = new_value
or
MappedDevice.dps['some_dp_name'] = new_value
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is brilliant. It elevates ease-of-use. I'm thinking how we best add this to README...

@spinza
Copy link

spinza commented Jul 26, 2023

#!/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
Copy link
Collaborator Author

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

@spinza
Copy link

spinza commented Jul 28, 2023

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
Copy link
Collaborator Author

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
Copy link

spinza commented Jul 28, 2023

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
Copy link

spinza commented Jul 28, 2023

The code I mention is available here

@spinza
Copy link

spinza commented Jul 28, 2023

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

@uzlonewolf
Copy link
Collaborator Author

@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
Copy link

spinza commented Jul 29, 2023

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

@uzlonewolf
Copy link
Collaborator Author

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
Copy link

spinza commented Jul 31, 2023

Can a bitmap be settable?

@uzlonewolf
Copy link
Collaborator Author

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add local dp_x handling based on known product ids
3 participants