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

Scene support for Light devices #347

Open pVer297 opened 1 year ago

pVer297 commented 1 year ago

Hello, not an issue, I'm just dumping here my findings, maybe someone will find it useful.

I got myself some Hombli Smart Spots, they come with tuya version 3.3. This is an awesome project, and helped me a lot to get a grasp to control the lights, but it also got very clear very early that the scene and music support/information is very limited. Spent some time on reverse engineering the scene options, and it looks quite straight-forward. I would happily contribute with these extensions but unfortunately I do not have other types of devices and I use only Python3, so my testing capabilities are limited.

Here you can find a working scene demo

If anyone else also wants to have a take on the scenes, this is the data structure of a scene that I have derived from my specific device: SD ||FT, TT, HH, SS, VV, WI, WT||:repeat max 8

SD = Scene ID: 1 byte
FT = Frame time: 2 bytes -> See below
TT = Transition type: 1 byte -> 0 Static; 1 Flash; 2 Breathe
HH = Hue: 2 bytes    ----┐
SS = Saturation: 2 bytes ┼> Same as normal color definition
VV = Value: 2 bytes  ----┘
WI = White Intensity: 2 bytes   -┐
WT = White Temperature: 2 bytes -┴> Same as normal light definition

~Frame time is a bit more tricky. 0 Frame time corresponds to 10.5s. Every increase in the frame time value is decreasing the 'base' with 1/2570 seconds. A frame time value of 25700 is 0.5s. The formula to calculate the frame time from seconds is (base - seconds) * 2570~ See the comments by @uzlonewolf on May 31.

Note on HSV/White options. It is possible to set both of the fields at the same time, the device will happily comply, but due to thermal and/or power concerns I would not try to push it too far.

Thanks again for making this project! Cheers

uzlonewolf commented 1 year ago

Thank you so much for figuring this out! It had been on my to-do list for like forever.

One question about the frame time though, are you sure that's how it's calculated? It could be just that I have different bulbs, but mine use 1-byte for frame duration and the 2nd byte is transition duration. Perhaps it changes when the "Breathe" transition type is selected?

pVer297 commented 1 year ago

I have measured the flashes with stopwatch and it was corresponding to these values from 10.5 up to 0.25. It could be that I have missed something, and breathe/flash transitions differ. I will check it out when I got some time and report back. Thanks for bringing this to my attention!

pVer297 commented 1 year ago

Looks like it could be a product difference here. For my case, the frame time is consistent* for both transition times.

*The base is 11.0 seconds for 'Breathe' and 10.5 for 'Flash'

A scene consisting 3 colors (red, lime, blue) transitioning after 0.8s in 'Breathe' mode looks like this:

10.5s as base:
00 6160 02 0000 01f4 00c8 0000 0000
   6160 02 006e 01f4 00c8 0000 0000
   6160 02 00f0 01f4 00c8 0000 0000

11.0s as base:
00 6665 02 0000 01f4 00c8 0000 0000
   6665 02 006e 01f4 00c8 0000 0000
   6665 02 00f0 01f4 00c8 0000 0000

After measuring with stopwatch it looks like the second one is correct for Breathe transition, but I have not found a way to control the transition duration. It looks like it might be relative to the total duration for my case. Could you capture some DSPs where you control the transition duration?

uzlonewolf commented 1 year ago

I had made a ton of captures with multiple devices back when I was working on this, but of course now I can't find them. I'll keep looking but so far I've only found this one from a "Type B" bulb (the spaces are me picking it apart):

                                                                                  hue sat bri
    Status: {'20': True, '21': 'scene',  '24': '0000 03e8 0064', '25': '06 464601 000003e803e8 00000000 464601 007803e803e8 00000000 464601 00f003e803e8 00000000', '26': 0}
    Status: {'20': True, '21': 'colour', '24': '0157 03e8 0185', '25': '06 464601 000003e803e8 00000000 464601 007803e803e8 00000000 464601 00f003e803e8 00000000', '26': 0}
    Status: {'20': True, '21': 'scene',  '24': '0157 03e8 0185', '25': '06 464601 000003e803e8 00000000 464601 007803e803e8 00000000 464601 00f003e803e8 00000000 464601 001f03e803e8 00000000', '26': 0}
    Status: {'20': True, '21': 'scene',  '24': '0157 03e 80185', '25': '06 464601 000003e803e8 00000000 464601 007803e803e8 00000000 464601 00f003e803e8 00000000 464601 001f03e803e8 00000000', '26': 0}
    Status: {'20': True, '21': 'scene',  '24': '0157 03e8 0185', '25': '06 464601 0000026c00aa 00000000 464601 007803e803e8 00000000 464601 00f003e803e8 00000000 464601 001f03e803e8 00000000', '26': 0}
    Status: {'20': True, '21': 'scene',  '24': '0157 03e8 0185', '25': '06 565601 000003e800aa 00000000 565601 007803e803e8 00000000 565601 00f003e803e8 00000000 565601 001f03e803e8 00000000', '26': 0}
    Status: {'20': True, '21': 'scene',  '24': '0157 03e8 0185', '25': '06 565601 003703e800aa 00000000 565601 007803e803e8 00000000 565601 014203e803e8 00000000 565601 001f03e803e8 00000000', '26': 0}
    Status: {'20': True, '21': 'scene',  '24': '0157 03e8 0185', '25': '06 565602 003703e800aa 00000000 565602 007803e803e8 00000000 565602 014203e803e8 00000000 565602 001f03e803e8 00000000', '26': 0}
    Status: {'20': True, '21': 'scene',  '24': '0157 03e8 0185', '25': '06 464600 000003e803e8 00000000', '26': 0}
    Status: {'20': True, '21': 'scene',  '24': '0157 03e8 0185', '25': '05 464601 000003e803e8 00000000 464601 007803e803e8 00000000 464601 00f003e803e8 00000000 464601 003d03e803e8 00000000 464601 00ae03e803e8 00000000 464601 011303e803e8 00000000', '26': 0}

While working on my new MappedDevice module I noticed at least one bulb took JSON instead of binary:

        "25": {
            "name": "scene_data_v2",
            "type": "Json",
            "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
                    }
                }
            }
        },
uzlonewolf commented 1 year ago

Ok, after throwing a bunch of stuff at the bulb I have, I think I got at least some of it figured out.

It would seem only the first time byte has an effect on this bulb; I saw no discernible difference between 0x00 and 0xff for that 2nd byte. With the first byte, 0x00 is slow (roughly 11 seconds) while 0x6d is disco party. Anything above 0x6d results in it stopping and not changing color. My assumption is it is a 100ms timer that gets loaded with 110 and then the value of the first byte is subtracted. If the loop decrement is performed before the zero check then this would result in it rolling over to -1 the next time through the loop if 0x6e or greater is set.

pVer297 commented 1 year ago

This is an interesting finding. I noticed that your first capture list resembles mine. 464601 this is 3.5s in 'Flash' mode, basically the default setting in the TuyaApp scene editor. The JSON mapping looks like a lot of data for quite small "information content". For my case the max scene size is 840 byte, so sending that much JSON data makes me wonder if it has a different communication protocol? Is it a newer version? Like 3.5? As for your last comment, it would be a very good system as it makes sense and uses less bits than mine. It gave me the idea to check if mine also ignores one of the bytes. But no, for my case both of the bytes are significant and it is only a coincidence that they usually the same value.

This all makes me wonder, is this an OEM difference thing, or a communication protocol version thing?

uzlonewolf commented 1 year ago

I'm actually not convinced the protocols are different. If that 2nd byte really is part of the timer then the difference between 0 and 100 (0x64) would be 0.039 seconds and I have my doubts that that can be reliably measured with a stopwatch. I have some 5v and 12v lights coming soon that I plan on hooking up to my logic analyzer to get some nice graphs for (since I'm not comfortable hooking my USB analyzer up to 120v bulbs :upside_down_face:)

uzlonewolf commented 1 year ago

As for the JSON, it turns out those bulbs don't actually use JSON for those DPs. My guess is this is Tuya's way of describing the hex-encoded field; the JSON "values" field is just the description.

uzlonewolf commented 1 year ago

Ok, I finally got some low voltage devices in and hooked them up to my logic analyzer. All of these times were measured with a 5 MHz sample rate.

First device uses a RTL chip and has firmware v1.0.7. This is the first device I've ever seen using this chip, and I expect it to act at least a little different due to using a different SDK.

First up, the "breath" pattern. For this pattern the first timing byte controls the total length of the pattern step while the 2nd byte controls the fade in/out duration. Like for all bulbs, the app only sets the 2 timing bytes to values between 0x00 and 0x64 where 0x00 is slow and 0x64 is fast. For long-duration fades or short-duration total lengths it never quite reaches full on or full off with less than 4 pattern steps, so all of the numbers in the table below are using a 4-step pattern with the first step being full red and the next 3 steps are black. This means the "total time" column is step time * 4 and off times are spread across 3 step times while "off-dim-off" is fade in plus fade out and "transition off-on" is fade in only.

T1 T2 Total Time Off Time Full On Time Transition/Off-Dim-Off Time
00 00 8.88s 1.528 -- 7.350 off-dim-off
00 10 8.88s 2.309 -- 6.570 off-dim-off
00 20 8.88s 4.667 0.254 1.982 transition off-on
00 63 8.88s 6.287 1.852 0.371 transition off-on
00 64 8.88s 6.412 1.975 0.247 transition off-on
00 69 8.88s 6.412 1.975 0.247 transition off-on
-------- -------- -------- -------- -------- --------
64 64 0.88s 0.257 -- 0.623 off-dim-off fade
64 69 0.88s 0.44 -- 0.44 off-dim-off fade
69 69 0.48s 0.241 -- 0.239 off-dim-off fade
-------- -------- -------- -------- -------- --------
6e 69 0.08s 0.041 -- 0.039 off-dim-off fade
6f 69 20.48s 15.11 4.875 0.247 transition off-on

As you can see, the fade time does not affect the total pattern time, and the actual range for the first timing byte is 0x00 - 0x6e in steps of 0.02 seconds and the 2nd byte is 0x00 - 0x69. Setting either one to a value above those causes it to overflow and wrap back around. As it is only an 8-bit timer this overflow still results in a usable time.

Next up, the "jump" pattern. For this pattern the 2nd timing byte is ignored and not used at all. As there is no fading going on the time is that of a single pattern step (it's not for a 4-step sequence like above). T1 T2 Step Time
00 xx 2.12s
69 xx 0.02s
6a xx 5.12s

For this one the actual range for the first timing byte is 0x00 - 0x69 in steps of 0.02 seconds and exceeding that causes it to overflow to 5.12s.

uzlonewolf commented 1 year ago

The 2nd device I got uses a Beken BK7231 chip and has firmware v2.9.6. This is the chip and firmware that's in almost every smart bulb I have.

Unlike the RTL chip, this one seems to always ignore the given T2/fade time and instead uses a hardcoded value. As the transition is rather short I was able to use a 2-step pattern (full red and black) so here the "total time" is pattern step time * 2.

T1 T2 Total Time Fade In Full On Fade Out Full Off
00 00 22.000 3.990 7.012 3.986 7.013
00 64 22.000 3.990 7.012 3.986 7.013
00 ff 22.000 3.990 7.012 3.986 7.013
-------- -------- -------- -------- -------- -------- --------
64 xx 2.000 0.5 0.5 0.5 0.5
6d xx 0.200 0.048 0.053 0.046 0.052
6e xx OVF -- -- -- --

As you can see, for this one the T2 time has no effect, and the actual range for T1 is 0x00 - 0x6d in steps of 0.1s. Exceeding this again causes an overflow, but as it appears to be a 32-bit timer the resulting time might as well be "never changes."

The "jump" transition is about as you'd expect: T1 T2 Step Time
00 xx 10.5s
68 xx 0.1s
69 xx OVF

For this one the actual range for the first timing byte is 0x00 - 0x68 in steps of 0.1 and exceeding that causes it to overflow to "never changes."

pVer297 commented 1 year ago

Thanks for doing these measurements! Indeed, these are much more logical than my findings. I will update my code once I got some time for it.

My plan is to figure out the music control as well, hopefully this gives a base for that.

uzlonewolf commented 1 year ago

For the music control, are you talking about the app-driven music mode or the device-based sound activated mode? I suspect app-driven mode simply uses local control and DPS 28 to send a string of "change mode: direct" commands.

            "28": {
                "code": "control_data",
                "type": "Json",
                "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
                    }
                }

Device-based sound-activated mode for devices with built-in microphones seem to use a "magic" scene value with T1+T2+transition set to 0e0d00 and everything else ignored. I only played around with it briefly but {"25":"000e0d0000000000000000c80000"} and {"25":"010e0d0000000000000003e801f4"} both had the exact same result.