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

Add support for config flows for switches #6

Merged
merged 4 commits into from
Sep 13, 2020
Merged

Add support for config flows for switches #6

merged 4 commits into from
Sep 13, 2020

Conversation

postlund
Copy link
Collaborator

@postlund postlund commented Sep 6, 2020

This PR adds initial support for config flows, but only for switches at this point. We should start with one platform and create one PR per platform, which should be pretty easy after this.

Summary of what is supported and not:

  • Only switches as mentioned
  • Arbitrary number of subswitches are supported, e.g. single or double or even triple gang switches
  • Settings cannot be changed after a device has been added (but can be fixed by adding options support later)
  • icon was removed as it can be overriden as a customization and it also didn't work with more than one subswitch
  • YAML support has not been added I won't be doing that either (direction of Home Assistant is to remove it for setting up Integrations and I would suggest that we do that here as well)

Feel free to give it a spin and give me some feedback. I have tried it with my single and double gang switches and it works fine.

@cwttdb70
Copy link

cwttdb70 commented Sep 6, 2020

Super cool, thanks for writing this functionality. Sadly, I don't have any Tuya switches, so I can't test it.

I think that the ability to edit settings after a device has been added is pretty important for a nontrivial amount of use cases. For example, someone might have their ID and Key, but not their dps information yet. They might want to start running localtuya with logging enabled to see the output of their dps values. At that point, they would not be able to edit their settings. I can't speak for switches, but for covers, there are several dps keys that go in the configuration. Perhaps we want that functionality added before this is merged? Or, at the very least, add a "warning" that informs the user that they will not be able to edit the configuration after adding a device, and would instead have to delete and re-add the device. Or, as an alternate approach, do we want to add something in the README explaining that you must already have your dps values before installing localtuya?

Unfortunately, my familiarity with Home Assistant is not at the level where I would be able to write the functionality to edit the configuration after a device is set up. That said, once this pull is merged with localtuya, I would be happy to try adapting this for cover devices.

@postlund
Copy link
Collaborator Author

postlund commented Sep 6, 2020

Thanks, you should get some devices 😉

While I do agree that it would be good to support editing settings, I don't see it as a blocker. It's better to start small and extend. The solution I would like to see is querying all DPS parameters and expose those values (as dropdown lists). That way you don't have to go anywhere else. It's trivial to implement, assuming I can figure out how to query all DPS values and not just "the first one". I've tried to compare with tuyapi, but I can really spot what's wrong. Will have to look into that further.

For now it would probably be best to just write something in the README. The process of setting everything up, e.g. getting local id and key, is quite tedious as-is. No one will be able to set the integration up via either config flow or YAML without resorting to the README anyways.

@cwttdb70
Copy link

cwttdb70 commented Sep 6, 2020

Thanks, you should get some devices 😉

While I do agree that it would be good to support editing settings, I don't see it as a blocker. It's better to start small and extend. The solution I would like to see is querying all DPS parameters and expose those values (as dropdown lists). That way you don't have to go anywhere else. It's trivial to implement, assuming I can figure out how to query all DPS values and not just "the first one". I've tried to compare with tuyapi, but I can really spot what's wrong. Will have to look into that further.

For now it would probably be best to just write something in the README. The process of setting everything up, e.g. getting local id and key, is quite tedious as-is. No one will be able to set the integration up via either config flow or YAML without resorting to the README anyways.

Yes, I agree. Let's just put something in the README for now noting the limitations, and as more functionality is added, we can update the README to reflect the current install/configuration/usage instructions.

How exactly are you going about trying to query all DPS values? I've been able to query my DPS values across multiple libraries and repositories. Can you query the DPS using the test.py debug script in the rospogrigio:master repo? Happy to help you debug this.

@postlund
Copy link
Collaborator Author

postlund commented Sep 6, 2020

How exactly are you going about trying to query all DPS values? I've been able to query my DPS values across multiple libraries and repositories. Can you query the DPS using the test.py debug script in the rospogrigio:master repo? Happy to help you debug this.

At least with tuyacli, it's possible to get a raw dump of all DPS values from a device. Usually you pass a dictionary with the DPS:es you are interested in, but supposedly you should be able to send an empty dict and get all. You of course just get IDs and values without any real meaning, so you still need to decide what they mean. Some of them are quite simple to deduce though.

Tried the test script and it worked for one type of the sockets I have, but not the other. Something fishy is going on there. For the one that worked I got all DPS values:


DEBUG:localtuya.pytuya:decrypted result='{"devId":"xxx","dps":{"1":false,"9":0,"18":0,"19":0,"20":2331,"21":1,"22":1700,"23":33193,"24":100000,"25":210}}'

@cwttdb70
Copy link

cwttdb70 commented Sep 6, 2020

How exactly are you going about trying to query all DPS values? I've been able to query my DPS values across multiple libraries and repositories. Can you query the DPS using the test.py debug script in the rospogrigio:master repo? Happy to help you debug this.

At least with tuyacli, it's possible to get a raw dump of all DPS values from a device. Usually you pass a dictionary with the DPS:es you are interested in, but supposedly you should be able to send an empty dict and get all. You of course just get IDs and values without any real meaning, so you still need to decide what they mean. Some of them are quite simple to deduce though.

Tried the test script and it worked for one type of the sockets I have, but not the other. Something fishy is going on there. For the one that worked I got all DPS values:


DEBUG:localtuya.pytuya:decrypted result='{"devId":"xxx","dps":{"1":false,"9":0,"18":0,"19":0,"20":2331,"21":1,"22":1700,"23":33193,"24":100000,"25":210}}'

Yup, with tuya-cli, you can add --all at the end and it should dump all of the dps. It's also important that you properly specify the protocol version (--protocol-version 3.3 or --protocol-version 3.1). I'm sure you know that, but just documenting it here so others can follow along.

Let's call "Device A" the one that does work with the test.py script.
Let's call "Device B" the one that does not work the test.py script.

A few questions:

(1) Can you post the output of trying to dump Device B with tuya-cli using the following command:
DEBUG=* tuya-cli get --id DEVICEBID --key DEVICEBKEY --protocol-version DEVICEBPROTOCOLVERSION --ip DEVICEBIP --all
(2) How many characters long is Device A's device ID?
(3) How many characters long is Device B's device ID?

@postlund
Copy link
Collaborator Author

postlund commented Sep 7, 2020

Yup, with tuya-cli, you can add --all at the end and it should dump all of the dps. It's also important that you properly specify the protocol version (--protocol-version 3.3 or --protocol-version 3.1). I'm sure you know that, but just documenting it here so others can follow along.

Let's call "Device A" the one that does work with the test.py script.
Let's call "Device B" the one that does not work the test.py script.

A few questions:

(1) Can you post the output of trying to dump Device B with tuya-cli using the following command:
DEBUG=* tuya-cli get --id DEVICEBID --key DEVICEBKEY --protocol-version DEVICEBPROTOCOLVERSION --ip DEVICEBIP --all
(2) How many characters long is Device A's device ID?
(3) How many characters long is Device B's device ID?

(1) It seems like when specifying protocol version it works as expected with tuya-cli:

$ DEBUG=* tuya-cli get --id af211b0a437a845a75hhh3 --key 888ee0c1d4885c9f --protocol-version 3.3 --ip 10.0.10.111 --all
  TuyAPI IP and ID are already both resolved. +0ms
  TuyAPI Connecting to 10.0.10.111... +4ms
  TuyAPI Socket connected. +36ms
  TuyAPI GET Payload: +3ms
  TuyAPI {
  TuyAPI   gwId: 'af211b0a437a845a75hhh3',
  TuyAPI   devId: 'af211b0a437a845a75hhh3',
  TuyAPI   t: '1599454383',
  TuyAPI   dps: {},
  TuyAPI   uid: 'af211b0a437a845a75hhh3'
  TuyAPI } +0ms
  TuyAPI GET Payload: +8ms
  TuyAPI {
  TuyAPI   gwId: 'af211b0a437a845a75hhh3',
  TuyAPI   devId: 'af211b0a437a845a75hhh3',
  TuyAPI   t: '1599454383',
  TuyAPI   dps: {},
  TuyAPI   uid: 'af211b0a437a845a75hhh3'
  TuyAPI } +0ms
  TuyAPI Received data: 000055aa000000010000000a0000008c0000000012ee8914931d7a425f0998c0892c3b1bc104ac56c25806ba1643077e1ee95b0aa6c9cb4cb92d0e079ac6def4d63e934b59abcc0a391d7091f7c5a0969725e9867933ede32ac53e7b3575cad29ef1ef68c71aa1b0d64698cc693788d35da8193d0b9d9795c9bd9393e5f30121deb3f31fd3b0775d61ec1715dc0631202fed27ab793ec4930000aa55 +10ms
  TuyAPI Parsed: +1ms
  TuyAPI {
  TuyAPI   payload: {
  TuyAPI     dps: {
  TuyAPI       '1': false,
  TuyAPI       '2': false,
  TuyAPI       '9': 0,
  TuyAPI       '10': 0,
  TuyAPI       '18': 0,
  TuyAPI       '19': 0,
  TuyAPI       '20': 2327,
  TuyAPI       '21': 1,
  TuyAPI       '22': 602,
  TuyAPI       '23': 27189,
  TuyAPI       '24': 16112,
  TuyAPI       '25': 1304
  TuyAPI     }
  TuyAPI   },
  TuyAPI   leftover: false,
  TuyAPI   commandByte: 10,
  TuyAPI   sequenceN: 1
  TuyAPI } +0ms
  TuyAPI Received data: 000055aa000000020000000a0000008c0000000012ee8914931d7a425f0998c0892c3b1bc104ac56c25806ba1643077e1ee95b0aa6c9cb4cb92d0e079ac6def4d63e934b59abcc0a391d7091f7c5a0969725e9867933ede32ac53e7b3575cad29ef1ef68c71aa1b0d64698cc693788d35da8193d0b9d9795c9bd9393e5f30121deb3f31fd3b0775d61ec1715dc0631202fed27aba3e8144e0000aa55 +9ms
  TuyAPI Parsed: +1ms
  TuyAPI {
  TuyAPI   payload: {
  TuyAPI     dps: {
  TuyAPI       '1': false,
  TuyAPI       '2': false,
  TuyAPI       '9': 0,
  TuyAPI       '10': 0,
  TuyAPI       '18': 0,
  TuyAPI       '19': 0,
  TuyAPI       '20': 2327,
  TuyAPI       '21': 1,
  TuyAPI       '22': 602,
  TuyAPI       '23': 27189,
  TuyAPI       '24': 16112,
  TuyAPI       '25': 1304
  TuyAPI     }
  TuyAPI   },
  TuyAPI   leftover: false,
  TuyAPI   commandByte: 10,
  TuyAPI   sequenceN: 2
  TuyAPI } +0ms
  TuyAPI Disconnect +2ms
{
  dps: {
    '1': false,
    '2': false,
    '9': 0,
    '10': 0,
    '18': 0,
    '19': 0,
    '20': 2327,
    '21': 1,
    '22': 602,
    '23': 27189,
    '24': 16112,
    '25': 1304
  }
}
  TuyAPI Socket closed: 10.0.10.111 +3ms

(2) It's 20.
(3) It's 22.

So I guess it's some kind of incompatibility in the pytuya library used here. I haven't looked at the protocol nor the implementation much, so I can't really tell. If you or someone else knows more and can help me fix this, it would be great!

@postlund
Copy link
Collaborator Author

postlund commented Sep 7, 2020

Here's a prototype (not done yet) of what can be done if DPS values can be fetched correctly:
localtuya_dps

@cwttdb70
Copy link

cwttdb70 commented Sep 7, 2020

Interesting, my expectation was that the device ID of length 20 would be the one that doesn't work. rospogrigio helped me debug my cover device that wasn't working a few days ago. We discovered that localtuya/pytuya/__init__.py automatically set self.dev_type = 'device22' around line 170. We edited that line to instead say the below, which fixed my problem and let me successfully execute the test.py file from the rospogrigio repository.

         if len(dev_id) == 22:
            self.dev_type = 'device22'
        else:
            self.dev_type = 'device20'

rospogrigio then merged the change into the master.

Anyways, you're telling me that your device with ID length 20 is working, and your device with ID length 22 is not working, so this is clearly not your issue. Again, I just want to provide some context for others who read through this thread.

Let's go back to your problem. I've been in touch with @jasonacox, the author of tuyapower, a python library that polls WiFi Tuya compatible Smart Plugs/Switches/Lights. tuyapower depends on pytuya, just as localtuya does. pytuya is no longer maintained, so @jasonacox has forked it into tinytuya, which he says he plans on maintaining. He's already merged in the 'device20' change.

Let's run a series of tests, only for Device B (the one that wasn't originally working with the localtuya debug scripts to pull dps data). This is going to be a bit duplicative, but I want to determine exactly where the error does and does not occur, and get as much information as possible.

Test Series 1
Download the latest version of tuyadebug (https://github.com/rospogrigio/localtuya-homeassistant/raw/master/tuyadebug.tgz).
Unarchive it and place it somewhere on your development machine.
cd to the unarchived directory on your device at .../tuyadebug.

Test (1)(a)
Run python scan.py and post your output below:

Test (1)(b)
Run DEBUG=* python test.py af211b0a437a845a75hhh3 10.0.10.111 888ee0c1d4885c9f "" DEVVERS 3.3 and post your output below:

Test Series 2
Download the latest version of tinyutya (https://github.com/jasonacox/tinytuya/archive/master.zip).
Unarchive it and place it somewhere on your development machine.
cd to ../tinytuya-master.

Test (2)(a)
Run DEBUG=* python test.py af211b0a437a845a75hhh3 10.0.10.111 888ee0c1d4885c9f 3.3 and post your output below:

Test Series 3
Copy .../tinytuya-master/tinytuya/init.py and .../tinytuya-master/tinytuya/init.pyc to .../tuyadebug/localtuya/ (replace existing files).
Copy .../tinytuya-master/test.py to .../tuyadebug/ (replace existing file).
cd to .../tuyadebug.

Test (3)(a)
Run python scan.py and post your output below:

Test (3)(b)
Run DEBUG=* python test.py af211b0a437a845a75hhh3 10.0.10.111 888ee0c1d4885c9f 3.3 and post your output below:

Test Series 4
Download the latest version of tuyapower (https://github.com/jasonacox/tuyapower/archive/master.zip). Unarchive it and place it somewhere on your development machine. cd to ../tuyapower-master.

Test (4)(a)
Run python scan.py and post your output below:

Test (4)(b)
Run DEBUG=* python test.py af211b0a437a845a75hhh3 10.0.10.111 888ee0c1d4885c9f 3.3 and post your output below:

Test Series 5
Run sudo apt-get install python-crypto python-pip
Run python3 -m pip install pycryptodome
Run python3 -m pip install tinytuya
Run python3 -m pip install tuyapower

Test (5)(a)
Run python3 -m tuyapower and post your output below:

Hopefully, this can help narrow down where the problem is (and isn't)...

Worst case, if none of this works for your device, maybe we just add tuya-cli as a dependency/import it (whatever the correct terminology is)? It seems to be the most consistent method to get dps data.

@postlund
Copy link
Collaborator Author

postlund commented Sep 7, 2020

Yeah, I read something about the different lengths earlier. But as you say, I don't think it has to do with that. Or at least not directly.

Test Series 1

The general behavior is that the device doesn't respond to requests at all, like this:

$ DEBUG=* python test.py af211b0a437a845a75hhh3 10.0.10.111 888ee0c1d4885c9f "" DEVVERS 3.3
INFO:localtuya:localtuya version 0.0.19
INFO:localtuya:Python 3.8.1 (default, Jan  8 2020, 13:13:59)
[GCC 4.9.2] on linux
INFO:localtuya:Using pytuya version '7.0.8'
SWITCH DEVICE
INFO:localtuya:Requesting status of device af211b0a437a845a75hhh3 [10.0.10.111], protocol 3.3.
DEBUG:localtuya.pytuya:status() entry (dev_type is device22)
DEBUG:localtuya.pytuya:json_payload=b'{"devId":"af211b0a437a845a75hhh3","uid":"af211b0a437a845a75hhh3","t":"1599474111","dps":{"DEVVERS":null}}'
Failed to receive data from 10.0.10.111. Raising Exception.
SWITCH DEVICE
INFO:localtuya:Requesting status of device af211b0a437a845a75hhh3 [10.0.10.111], protocol 3.3.
DEBUG:localtuya.pytuya:status() entry (dev_type is device22)
DEBUG:localtuya.pytuya:json_payload=b'{"devId":"af211b0a437a845a75hhh3","uid":"af211b0a437a845a75hhh3","t":"1599474123","dps":{"DEVVERS":null}}'
Failed to receive data from 10.0.10.111. Raising Exception.
SWITCH DEVICE
INFO:localtuya:Requesting status of device af211b0a437a845a75hhh3 [10.0.10.111], protocol 3.3.
DEBUG:localtuya.pytuya:status() entry (dev_type is device22)
DEBUG:localtuya.pytuya:json_payload=b'{"devId":"af211b0a437a845a75hhh3","uid":"af211b0a437a845a75hhh3","t":"1599474135","dps":{"DEVVERS":null}}'
Failed to receive data from 10.0.10.111. Raising Exception.
INFO:localtuya:TIMEOUT: No response from device af211b0a437a845a75hhh3 [10.0.10.111] after 2 attempts.

Scanning reveals the device correctly:

$ python scan.py
Scanning on UDP ports 6666 and 6667 for devices...

JSON {'ip': '10.0.10.111', 'gwId': 'af211b0a437a845a75hhh3', 'active': 2, 'ablilty': 0, 'encrypt': True, 'productKey': 'keyymwkvrcstea38', 'version': '3.3'}
FOUND Device [Valid payload]: 10.0.10.111
    ID = af211b0a437a845a75hhh3, productKey = keyymwkvrcstea38, Version = 3.3

An interesting thing though... I tried removing the "dps" key from json_payload altogether and in that case I do get a response:

$ DEBUG=* python test.py af211b0a437a845a75hhh3 10.0.10.111 888ee0c1d4885c9f "" DEVVERS 3.3
INFO:localtuya:localtuya version 0.0.19
INFO:localtuya:Python 3.8.1 (default, Jan  8 2020, 13:13:59)
[GCC 4.9.2] on linux
INFO:localtuya:Using pytuya version '7.0.8'
SWITCH DEVICE
INFO:localtuya:Requesting status of device af211b0a437a845a75hhh3 [10.0.10.111], protocol 3.3.
DEBUG:localtuya.pytuya:status() entry (dev_type is device22)
DEBUG:localtuya.pytuya:json_payload=b'{"devId":"af211b0a437a845a75hhh3","uid":"af211b0a437a845a75hhh3","t":"1599474255"}'
DEBUG:localtuya.pytuya:status received data=b"\x00\x00U\xaa\x00\x00\x00\x00\x00\x00\x00\r\x00\x00\x00,\x00\x00\x00\x01\rv\x9f\xc1v\x81\xbb\xe6\xff\xd4T\xedWX(R\xbaA7{d\x8cM\x1f`v\xf9\x8f\xfa\xa8'>d\x04\xff\xc0\x00\x00\xaaU"
DEBUG:localtuya.pytuya:result=b"R\xbaA7{d\x8cM\x1f`v\xf9\x8f\xfa\xa8'>"
SWITCH DEVICE
INFO:localtuya:Requesting status of device af211b0a437a845a75hhh3 [10.0.10.111], protocol 3.3.
DEBUG:localtuya.pytuya:status() entry (dev_type is device22)
DEBUG:localtuya.pytuya:json_payload=b'{"devId":"af211b0a437a845a75hhh3","uid":"af211b0a437a845a75hhh3","t":"1599474257"}'
DEBUG:localtuya.pytuya:status received data=b"\x00\x00U\xaa\x00\x00\x00\x00\x00\x00\x00\r\x00\x00\x00,\x00\x00\x00\x01\rv\x9f\xc1v\x81\xbb\xe6\xff\xd4T\xedWX(R\xbaA7{d\x8cM\x1f`v\xf9\x8f\xfa\xa8'>d\x04\xff\xc0\x00\x00\xaaU"
DEBUG:localtuya.pytuya:result=b"R\xbaA7{d\x8cM\x1f`v\xf9\x8f\xfa\xa8'>"
SWITCH DEVICE
INFO:localtuya:Requesting status of device af211b0a437a845a75hhh3 [10.0.10.111], protocol 3.3.
DEBUG:localtuya.pytuya:status() entry (dev_type is device22)
DEBUG:localtuya.pytuya:json_payload=b'{"devId":"af211b0a437a845a75hhh3","uid":"af211b0a437a845a75hhh3","t":"1599474259"}'
DEBUG:localtuya.pytuya:status received data=b"\x00\x00U\xaa\x00\x00\x00\x00\x00\x00\x00\r\x00\x00\x00,\x00\x00\x00\x01\rv\x9f\xc1v\x81\xbb\xe6\xff\xd4T\xedWX(R\xbaA7{d\x8cM\x1f`v\xf9\x8f\xfa\xa8'>d\x04\xff\xc0\x00\x00\xaaU"
DEBUG:localtuya.pytuya:result=b"R\xbaA7{d\x8cM\x1f`v\xf9\x8f\xfa\xa8'>"
INFO:localtuya:TIMEOUT: No response from device af211b0a437a845a75hhh3 [10.0.10.111] after 2 attempts.

I guess the format is not what pytuya expects, so it doesn't work.

Test Series 2

Just get repeating timeouts. The exception below is printed when pressing ctrl+c. I tried modifying that line to set an empty dict (json_data['dps'] = {}) but it stil resulted in repeated timeouts and I can not kill it with ctrl+c.

TIMEOUT: No response from plug af211b0a437a845a75hhh3 [10.0.10.111] after 2 attempts.
TIMEOUT: No response from plug af211b0a437a845a75hhh3 [10.0.10.111] after 2 attempts.
TIMEOUT: No response from plug af211b0a437a845a75hhh3 [10.0.10.111] after 2 attempts.
TIMEOUT: No response from plug af211b0a437a845a75hhh3 [10.0.10.111] after 2 attempts.
TIMEOUT: No response from plug af211b0a437a845a75hhh3 [10.0.10.111] after 2 attempts.
^CTraceback (most recent call last):
  File "test.py", line 46, in <module>
    data = d.status()
  File "/home/postlund/pyatv_dev/localtuya-homeassistant/tmp/tinytuya-master/tinytuya/__init__.py", line 308, in status
    payload = self.generate_payload('status')
  File "/home/postlund/pyatv_dev/localtuya-homeassistant/tmp/tinytuya-master/tinytuya/__init__.py", line 261, in generate_payload
    json_data['dps'] = self.dpsUsed
AttributeError: 'OutletDevice' object has no attribute 'dpsUsed'

Test Series 3

Will have to do this later as I'm in the middle of stuff.

Test Series 4

Scanning:

$ python scan.py
Scanning on UDP ports 6666 and 6667 for devices...

FOUND Device [Valid payload]: 10.0.10.111
    ID = af211b0a437a845a75hhh3, productKey = keyymwkvrcstea38, Version = 3.3

Scan Complete!  Found 1 devices.

Output from test.py:

DEBUG=* python test.py af211b0a437a845a75hhh3 10.0.10.111 846eebc0d4884c9a 3.3f
^C^C ERROR: User Interrupt

TuyaPower (Tuya Power Stats) [0.0.24] tinytuya [1.0.0]

Device af211b0a437a845a75hhh3 at 10.0.10.111 key 846eebc0d4884c9a protocol 3.3f:
    Response Data: False
    Switch On: False
    Power (W): -99.000000
    Current (mA): -99.000000
    Voltage (V): -99.000000
    Projected usage (kWh):  Day: -2.376000 Week: -16.632000  Month: -72.072000

{ "datetime": "2020-09-07T10:39:26Z", "switch": "False", "power": "-99", "current": "-99", "voltage": "-99" }

The script hangs until i press ctrl+c, which prints the output above. Values doesn't seem right, so I don't think DPS values were retrieved correctly.

Test Series 5

$ python -m tuyapower
TuyaPower (Tuya compatible smart plug scanner) [0.0.24] tinytuya [1.0.0]

Scanning on UDP ports 6666 and 6667 for devices (15 retries)...

FOUND Device [Valid payload]: 10.0.10.111
    ID = af211b0a437a845a75hhh3, product = keyymwkvrcstea38, Version = 3.3
    Device Key required to poll for stats

Scan Complete!  Found 1 devices.

Based on all of this it seems like excluding the dps field and handling is what works. This however does not seem to work with my other (device20) sockets. So it's a pickle. One way forward could be to check what corresponding json_data that tuyapi generates and spot any differences.

@rospogrigio
Copy link
Owner

Guys, I would make it more simple. As far as I understoodk, "device22" devices require the list of dps you want to get in the response to be specified in the payload of the request. I would simply send a list of, let's say, all numbers from 1 to 25 plus from 101 to 120, and we would probably get all the dps involved.
It would surprise me if a device provided dps values outside this range. I know this approach is not very elegant but it should be quite effective.
I'll try to add this PR, and I am considering replacing pytuya with tinytuya, since it's actively maintained and the code also seems sleeker. @andrewmeepos , do you know if jasonacox has reported any improvement in the performances (i.e., response times) with his library?

@postlund
Copy link
Collaborator Author

postlund commented Sep 7, 2020

@rospogrigio That sounds like a very hacky-last-way-out kinda thing to do. It tuyapi can do it without providing a list of DPS values, we should be able to replicate that.

Moving to a library that is maintained sounds like a very good idea though 👍

I'm making a few big changes to this PR, moving everything around a bit. The idea is to have a "generic part" used to connect to the device (e.g. key and id). After that entities can be added based on the supported platforms. This basically means that you can create any kind of entity and attach it to any tuya device. We could for instance create a sensor platform that uses a specific DPS value as state. It will become more clear once I am done...

@rospogrigio
Copy link
Owner

@postlund , you seem to be quite more skilled than I am... would you like to take over this repo and become the maintainer? I don't know if I can add other developers but I believe so, you also seem to have much more time to spend :-D
Let me know your thoughts, bye

@rospogrigio
Copy link
Owner

Moreover, I gave a look at tinytuya... it is substantially the same exact code of pytuya, with some commented debug instructions removed BUT without something that in my experience is fundamental! I am talking about a second receive that has to be issued in certain cases: some devices in some cases respond with an empty message (tipically 28 bytes long), and when this happens you have to wait some time (I use 100ms) and try another receive call, which finally returns the correct data.
Since tinytuya does not add anything (but actually, is lacking something vital!) I will not import it and stick to pytuya, maybe I'll just do some cleaning here and there.

@postlund
Copy link
Collaborator Author

postlund commented Sep 7, 2020

Let's see where this goes first, but sure, if you want to get rid of the repo I can probably take over. Time is something I don't really have, but kinda take anyways... 😉

Do we know why the devices sometime respond with that message? Or what that message means?

There are a few things in the code, mainly the integration, that needs to change over time. One is that updating is done per entity instead of per device. This means that when you have a double gang socket for instance, two requests are made but only one is really needed. This can likely cause problems as only one TCP connection can be active to a device at the time. The second issue I know of is kinda related as well. There's no synchronization with regards to only establishing one connection at the time. So if an update is performed at the same time as doing something else (turning of a switch for instance), we again get two connections at the same time. Turning on and off rapidly surely causes problems as well. This is somewhat mitigated via the "cache" (which is something that isn't really needed either as everything is suppose to be cached in the entity) since it performs retries. Using a manager that keeps track of the connections and only allowing one at the time would be better. Over time I will try to fix these things as clean up the code. Cleaning up the code will likely be the first thing I do after getting this PR merged.

@rospogrigio
Copy link
Owner

What message? The one that requests a double receive you mean? It's actually an empty message (once decoded, you get a {} json). Don't know why this happens, but as far as I remember it is mostly (if not only) a "device22" behavior.
This had caused me strong headaches when I tried to make them work, since I had to understand:

  1. that device22 and device20 were different
  2. that device22 used a different "getstatus" command, and that it needed the list of dps in the payload
  3. finally, that sometimes they also needed a double receive...
    It took more than one week to figure out all of the above, but finally had everything working.

OK, let's keep this contributor-and-maintainer approach for the moment, then you might take over the project when you'll want. Or, I could add you to the developers, when I figure out how to to it (maybe you know).
Thanks!

@rospogrigio
Copy link
Owner

PS, I also imagined that having 2 different devices for the same 2-gang switch was something that could be optimized, but would not know how to do it.
Moreover, my covers also provide a backlight of the control panel that can be switched on and off. I tried to create a switch within the cover.py but could never make it work, so the final solution was to create a separate switch for this function... but again, I have 2 different devices (and connections) while it's actually the same device which exposes 2 different entities. Don't know if you can fix also this situation.

@postlund
Copy link
Collaborator Author

postlund commented Sep 7, 2020

Sounds messy, I might have to take a look at that later. But it probably works good enough for now as-is 👍

Once cover has config flow support you will be able to add that configuration. But it will not solve the double-connection issue, it's a different a different problem. I will fix that as well though.

PS. You should probably enable issue in this repo, so people can report bugs.

@rospogrigio
Copy link
Owner

rospogrigio commented Sep 7, 2020

Mmm... how do I enable issues?
And, I am testing this PR... what am I supposed to notice?

@postlund
Copy link
Collaborator Author

postlund commented Sep 7, 2020

Click Settings at the top of the page and check under Features.

Go to Configuration -> Integrations, press the +-sign down to the right and look for "LocalTuya integration". You should be able to follow from there.

@jasonacox
Copy link

@rospogrigio:

Moreover, I gave a look at tinytuya... it is substantially the same exact code of pytuya, with some commented debug instructions removed BUT without something that in my experience is fundamental! I am talking about a second receive that has to be issued in certain cases: some devices in some cases respond with an empty message (tipically 28 bytes long), and when this happens you have to wait some time (I use 100ms) and try another receive call, which finally returns the correct data.
Since tinytuya does not add anything (but actually, is lacking something vital!) I will not import it and stick to pytuya, maybe I'll just do some cleaning here and there.

Thanks @rospogrigio - You are correct, the Tuya devices will often behave this way. There are two schools of thought on this:

  1. API should give the true response and not mask behavior anomalies in case the client of the API wants raw control or
  2. The API should mask as much of the anomalies behavior by adding retry logic on the interface side. I went back and forth on this and finally landed on keeping it like the core pytuya API.

Having said that, it occurs to me there is a 3rd option. I can add a setting to disable retry if the client code wants "true response" behavior but default with adding the retry logic. I'll look at your retry logic to see if that makes the most sense. I'll also check TuyaAPI to see if there are logic benefits we can glean from there as well.

@postlund
Copy link
Collaborator Author

postlund commented Sep 7, 2020

@andrewmeepos I did an override on the device type and forced it to use device20 despite being a device22 and now it works. I also compared to what tuyapi does it it's the same thing.

@postlund
Copy link
Collaborator Author

postlund commented Sep 7, 2020

Can anyone tell me or point to the source of the "device20 vs device22"-thing? Doesn't seem to be a special case in tuyapi, so I'm suspecting there's something 🐠 going on.

@cwttdb70
Copy link

cwttdb70 commented Sep 7, 2020

Can anyone tell me or point to the source of the "device20 vs device22"-thing? Doesn't seem to be a special case in tuyapi, so I'm suspecting there's something 🐠 going on.

I also just went through all of tuyapi's codebase and don't see any delineation between devices with IDs of varying lengths (20 vs 22, or otherwise), nor any mention of hexbytes of 0a vs 0d. Perhaps tuyapi handles device communication in a way that negates the need to differentiate between devices with different ID lengths?

Having said that, it occurs to me there is a 3rd option. I can add a setting to disable retry if the client code wants "true response" behavior but default with adding the retry logic. I'll look at your retry logic to see if that makes the most sense. I'll also check TuyaAPI to see if there are logic benefits we can glean from there as well.

I support this option. Would be helpful going forward if we could generally be in line with tinytuya.

@rospogrigio
Copy link
Owner

@jasonacox I see, the fact is that some of my devices (the covers, if I remember well) happened to enter some kind of state in which there was no way to get a non-empty response using a single receive. Making a second receive made them work flawlessly. I don't know if it was a matter of firmware, just today I noticed that a fw upgrade was released and I installed it, I well check if this behavior has been fixed.

@rospogrigio
Copy link
Owner

@postlund I don't find any Features page under Settings...
To everybody: the 20-22 distinction was needed to have my covers working (which are all device22) and also my switches (some of which are 20 and some others are 22). Sending the 0d command with the dps in the payload was the only way I found to make them work. Maybe with the new firmwares I installed today this distinction is no longer needed (I will try to get rid of it) but until now it was the only way to have all my devices working. I discovered it after I started tweaking TradeFace's code, the only code that allowed to talk with my covers, at the time (but failed to work with some switches). I hope this clears the scenario.

@cwttdb70
Copy link

cwttdb70 commented Sep 7, 2020

To everybody: the 20-22 distinction was needed to have my covers working (which are all device22) and also my switches (some of which are 20 and some others are 22). Sending the 0d command with the dps in the payload was the only way I found to make them work. Maybe with the new firmwares I installed today this distinction is no longer needed (I will try to get rid of it) but until now it was the only way to have all my devices working. I discovered it after I started tweaking TradeFace's code, the only code that allowed to talk with my covers, at the time (but failed to work with some switches). I hope this clears the scenario.

Interesting, let us know the result after you update the firmware.

For everyone's reference, the original commit to TradeFace's python-tuya library that incorporated the 0a/0d change is located here:
https://github.com/TradeFace/python-tuya/commit/ac7bf21c405e6ddb86a67a08f0ba0c48af189e5f

        if command == STATUS:
            json_data['dps'] = {"1":None,"2":None,"3":None}
            if command != STATUS or (command == STATUS and payload_dict[self.dev_type][command]['hexByte'] == '0d'):
	 if bin2hex(data[11:12]) == '0A':
	 	result = cipher.decrypt(result, False)
		if result == 'json obj data unvalid':
	 		payload_dict[self.dev_type]['status']['hexByte'] = '0d'
			result = self.status()        
		if not isinstance(result, str):
			result = result.decode()
		result = json.loads(result)        
	elif bin2hex(data[11:12]) == '08':
		result = cipher.decrypt(result[15:], False)

It looks like originally, there was no specific connection between 0a/0d and device20/device22. I think it could be a coincidence that @rospogrigio 's device20 devices needed 0a as the first hexByte, and his device22 devices needed 0d as the first hexByte. Because, as @postlund just told us, he has a device22, and he had to override it to be a device20 to get localtuya to work. That seems to be a concrete example of a device22 needing 0a as opposed to 0d.

Regardless of whether there is a connection between deviceID and 0a/0d, one thing seems clear to me, which is that however tuya-cli/tuyapi approaches this seems to the most robust method, as they don't seem to delineate based on device id length and it sounds like everyone can successfully communicate with their devices using those libraries.

@postlund I don't find any Features page under Settings...

Here's where the option to enable issues is located for my fork. Let me know if this helps you find it, or if it really is missing.
Screen Shot 2020-09-07 at 2 25 35 PM

@jasonacox
Copy link

Nice discovery.

  • The 0a (DP_QUERY) command template: It is clear that this is to pull data points from the device
  • The 0d (CONTROL_NEW) command template: It is not clear to me how this should be used to pull data points but it was working for the devices mentioned above.

@rospogrigio Did you find that that TuyAPI worked with your device22 devices?

I'm cleaning up tinytuya by switching to using Tuya Command Types in the code (more comprehensive) and listing the relevant command types in the device dictionary. I've defaulted device20 for now, but you can still set it to device22 at initialization, at least until we figure this out a bit more.

@rospogrigio
Copy link
Owner

So, sorry for the long post I'm about to write.
I have these 4 types of devices, all of them now updated to the latest firmware:

  1. smart plugs (plug+USB, so 2 subswitches), device20
  2. 2-gang switches, device20
  3. 1-gang switches, device22
  4. shutter covers, device22
    Only the device20s work with tuya-cli, the others get the 'json obj data unvalid' error. This is what always happened since the beginning, and still does. Searching for that error I came to TradeFace's repo, and I found that he used the 0d command instead of 0a. Now, in the commit you posted it shows that he tries the 0d command only after the 0a command returned the "json unvalid" error. I believe we all agree that this is a waste of time, since a device that expects a 0d command will always return that error (at least, my devices do). So, I was about to introduce a custom parameter (like "status_command") to distinguish the two cases, when I noticed the fact that only my device22s are expecting the 0d command, so I introduced this distinction.
    Now, @postlund 's device22 behaving as a device20 proves that it was just a coincidence. At this point, I think we should introduce that custom parameter, or maybe better, define a different protocol (like "3.4"?) so that people can configure their device by trial-and-error: in my experience, it is likely that a device22 will need the 3.4 protocol (with the 0d command), but some of them just need the standard 3.3 (i.e., the 0a command), like postlund's one.
    Your thoughts?
    PS, thank you @andrewmeepos , I finally found and enabled the issues, I didn't scroll down enough to see it (d'oh!).

@postlund
Copy link
Collaborator Author

postlund commented Sep 8, 2020

@jasonacox I agree with you, it seems really strange that CONTROL_NEW works for this. Perhaps the datapoints are included in that request so that it works as a side-effect? Would be interesting to see the decoded payload. Perhaps you can provide that @rospogrigio?

One approach would be to provide a simple interface that tries both types and returns whichever works. Since it's static for the device, it can be cached in a configuration (e.g. the config entry with regards to this PR).

@postlund
Copy link
Collaborator Author

One thing that is really missing is to be able to fetch more metadata from the device. For instance firmware version, product name, maybe manufacturer. Not sure if that is possible but would be really good for device support.

@rospogrigio
Copy link
Owner

I hardly doubt so. BTW, I tried launching the scan.py script in tuyadebug, and it recognized all of my devices, regardless if they were type 0a or 0d. This is done by using UDP connections, and it returns these info so that they could be autodetected, without the need for the user to input them (in particular, device_id and protocol version:
JSON {'ip': '192.168.1.49', 'gwId': 'bf4ce0daaaaaaa2fgdoy', 'active': 2, 'ablilty': 0, 'encrypt': True, 'productKey': 'keys83qyuhuqrdn7', 'version': '3.3'}

I need to study a bit more how things work before I can be helpful in this process, for the moment I am comparing what you are doing with what daikin does, to understand what we can borrow from them. In the meantime, if I run your RP I find your entities under Configuration, but they are all unavailable for some reason.

@postlund
Copy link
Collaborator Author

Yeah, discovery has been in my plan all along. Biggest issue is that Home Assistant doesn't have an api/allow custom discovery schemes. So we can't integrate it with the default discovery integration as only zeroconf and ssdp are supported. I was thinking we could add it as an initial step in the config flow and pre-fill discovered information.

We need to manually register a device in the device registry and implementdevice_info in all platforms for the device support. Should be very simple.

@postlund
Copy link
Collaborator Author

Regarding the entities being unavailable, maybe you can debug what happens in the update() method and make sure it works?

@rospogrigio
Copy link
Owner

Yeah, I believe the easiest thing would be to have the user input the IP address, and have devID and protocol autodetected in this way.

I was also having a look at what the Tuya integration does, and I am pretty disappointed to see that they have just made an entity for each subswitch (without regrouping them for physical devices), and they don't even provide the voltage/current data... so we are aiming to code something better than it is done there.

@postlund
Copy link
Collaborator Author

We can do even better and just detect all devices and present them in a dropdown menu. Will make it super easy to use. But we need some kind of fail-safe in case discovery doesn't work.

It's probably the least amount of work to get something up and running. Integrations usually evolve over time. But yeah, we will be aiming for a higher goal 😊

@rospogrigio
Copy link
Owner

OK I finished my time for today, will try something tomorrow... if you have something new to push in the meantime, please do.
Bye!

@rospogrigio
Copy link
Owner

rospogrigio commented Sep 11, 2020

@postlund , I've been studying a bit how things work, and I am getting more into the situation. I am planning a big refactoring of pytuya library since I really don't understand why there is a 3-level class inheritance, while I believe 1 is more than enough. Moreover, class specialization is partly done in the components script and part in the library, which has no sense because the library should just provide generic device handling.

EDIT: done. I strongly suggest you to merge from the "pytuya_refactoring" branch I pushed, I made an important cleanup of pytuya and things are much clearer now. Now I'll proceed with the device-subswitches thingy.

@rospogrigio
Copy link
Owner

OK, I think I'm starting to understand how things work and I've accomplished something. In my branch "physical_device", you can find a merge of your PR with my refactored pytuya. Moreover, I was able to create the Device (under Configuration-Devices) which has the entities associated. It works both for config flow and for YAML files, even if I have to think which is the best way to handle the "names" and "friendly_names" (need a discussion here). Next steps:

  1. unify connections, and maybe get rid of the TuyaCache classes (which add not needed complexity, I believe)
  2. associate voltage/current dps to the Device, and not to the subswitch entities, and maybe create related sensors (instead of using template sensors)
  3. autodetection of devices when adding integration in config flow
  4. extend new features to the other platforms (covers, fans...)

@postlund
Copy link
Collaborator Author

Sounds like you are making good progress too 😊 One thing that is important now is that we are structured, because there are a lot of work ongoing now that depends on each other. So we need to make sure that things are possible to merge without too much hassle. Your stuff, the pytuya rewrite and device registry stuff would be great if you made into two separate PRs. I can review them as a second opinion. Once they are merged I can start rebasing my PRs on top. I'm also working on support for options, so that will come soon.

I believe that we should go even further with pytuya and implement proper support for the protocol. By doing so we can maintain a connection to the device at all time, instead of connecting, doing something and immediately disconnecting (which is not a good approach). I would also strongly suggest that we convey to an asyncio interface instead, so we can convert the integration to be fully async. The cache-stuff that exists today should be possible to remove, we might wanna keep some retry-logic somewhere. But it should be re-usable.

We can add a generic sensor platform to extract power usage if we like. That is one way of doing it. Auto-detect should be easy to add as well, we just need to decide how to Ui should work.

@rospogrigio
Copy link
Owner

Yeah, sorry for not using 2 different branches. My suggestion now is that you merge with my latest work ("physical_device"), so that we have a common base to start on, and then continue the work from there. Then we need to decide who does what in order to avoid wasting time and energy.
I'd also close this PR and start working on a different one because this chat has gone waaaaay too long 😄
I would also publish a new release with just the autedetection because it's an important bugfix.
Agree?

@postlund
Copy link
Collaborator Author

My recommendations based on experience:

Never push or merge to master directly. Always use a PR.
Rationale: It's easy to push things by accident. Using a PR provides both visibility (since everyone watching the project will receive an email about a changes) and traceability (since there can be discussion about a change and it is saved forever). This obviously also helps if you want someone to confirm that a change works. Just point to the PR.

Use branch permissions and lock pushing to master at all.
Rationale: Basically everything above.

Make small changes and push them as PRs.
Rationale: It's a lot easier to review a small change then a large one. A lot. It's also easier to follow the history and see what has happened over time.

Stick to one topic and change one thing per PR. Rationale: It's for instance better to make one PR with refactoring and another one with the actual change, in case refactoring/restructuring is needed. Again, it's easier to review and history ("blame") makes more sense.

Avoid feature branches and large merge Integrations.
Rationale: If there's one thing to take from this, It's that branches are hell. More branches equals more work to keep all of them in sync, especially with GitHub which handles branches extremely poorly. In all professional work I have done, "one track" (i.e. one main branch) with continuous integration to that branch has been a defacto standard. It goes well in line with my first points, about making small PRs too.

Avoid merge commits altogether and force rebasing.
Rationale: Merge commits drops history. Rebasing keeps a straight history, which is easier to follow. This can be controlled from the settings page. Using "squash merge", where all commits in a PR is squashed into one commit can be ok too, but it's better if the entity history can be kept.

I could probably add a few more things, but what I'm after is basically: lock down master, one track (no feature branches, etc) and one PR per changed thing. So I still suggest that you break up your branch into multiple branches and create one PR per branch.

@postlund
Copy link
Collaborator Author

I believe it's gonna be quite a challenge to sort this out TBH.

@cwttdb70
Copy link

cwttdb70 commented Sep 11, 2020

I believe it's gonna be quite a challenge to sort this out TBH.

Let me know if there's anything I can do to help move things along.

@rospogrigio
Copy link
Owner

Thank you for the precious suggestions @postlund . I didn't know I could create a PR on my own repo, good to know. I'll try to do what you suggested, in the end I made very specific changes so it should not take that long. Also I have just noticed all your other PR, so I"ll merge everything and we can then restart from there.
Couple of questions:

  1. when you say "one track" you mean master, or create another one? In my company we usually have a master and a debug branch for testing, not sure if this is what you mean
  2. I did not understand the rebase part, can you be more specific?

I think I'll release the autodetect part in the meantime, and in the next day I'll proceed with the mergings.
Thank you!

@postlund
Copy link
Collaborator Author

Yeah, you can submit PRs to any branch in any repo (even your own), which is nice 😊 Maybe we can do it like this. You extract your changes into a PR or two, we review them and merge. Then you make a release. After that we start a new "development cycle" in order to release something along the lines of 1.0. There are a few breaking changes I would like to make in the YAMl config, I.e. remove fields which are not used. So it's probably better that we try to get as much stuff in as possible to figure out all the breaking changes we need/want to make to not leave any technical debt. Does that sound like a decent plan?

With "one track" I mean that there's only one branch to work on, I.e. master. We don't branch off for features or new release. Just keep merging and make release to/from that branch.

Rebasing is how you keep up-to-date with another branch. You basically change the "base" of your branch to another branch, applying all your commits to the new base. When applying this to a PR, it basically means that instead of taking all commits in your branch and squashing them together into one merge it, all commits will be applied on top of the branch you are submitting your PR to (e.g. master). So if you make three commits, there will be three commits on master.

@rospogrigio rospogrigio mentioned this pull request Sep 12, 2020
@rospogrigio
Copy link
Owner

Done, see my PRs #11 and #12.
#11 is independent, #12 is actually the result of merging this PR with #11, plus has the Device added.
Please review them and let me know what you think and if we can proceed with the BIG MERGE 😆
Bye!

@postlund
Copy link
Collaborator Author

Did did a review of #11, left some comments. Overall a well needed change! #12 is too big for me to review and too much noise from #11, so I'm gonna miss stuff. It would be a lot better if you removed the stuff from my PR and kept the device support only, so I don't have to review my own stuff. Once #11 is merged and #12 is rebased, I can check again.

@rospogrigio
Copy link
Owner

OK, I'm about to merge PRs from #6 to #11... some troubles may come now. Let's cross our fingers and hope not to break too many things... 😄

@rospogrigio rospogrigio merged commit 61fd48c into rospogrigio:master Sep 13, 2020
@rospogrigio
Copy link
Owner

Well, looks like the multiple merge broke almost everything... anybody can help me debug what's happening in master??
Thank you...

@postlund
Copy link
Collaborator Author

Yeah, I was kinda hoping we could take one at the time and make sure it worked before merging. I can take a look but not until tonight.

@rospogrigio
Copy link
Owner

So, the merge of #6 went pretty well. Then, #7 broke everything. You wrote it was dependent on #6 but they look very different. Can you please check? I see a lot of changes...

i-am-shodan pushed a commit to i-am-shodan/localtuya that referenced this pull request Aug 18, 2022
Add question regarding blocked internet access
SheaSmith pushed a commit to SheaSmith/localtuya that referenced this pull request Dec 28, 2022
PaulCavill pushed a commit to PaulCavill/localtuya that referenced this pull request May 9, 2024
Fixed initialization with API but no internet.
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.

4 participants