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 v3.4 devices #179

Merged
merged 11 commits into from Sep 11, 2022
Merged

Add support for v3.4 devices #179

merged 11 commits into from Sep 11, 2022

Conversation

uzlonewolf
Copy link
Collaborator

@uzlonewolf uzlonewolf commented Sep 9, 2022

Finally got a 3.4 device and got this working. I modeled it after harryzz's code at harryzz/tuyapi@ef4473b . The modifications were a lot more extensive than I initially anticipated due to the switch from CRC32 to HMAC checksums.

Closes #148
Closes #178

@jasonacox
Copy link
Owner

@uzlonewolf this is brilliant! I just ordered what I hope to be a 3.4 device to help with testing (send me the one you found). I also love the bits you cleaned up.

@uzlonewolf
Copy link
Collaborator Author

I just picked up the one linked in #178 as well as a couple others listed as "new model" from that same brand. I have no idea what they actually are, I'll post links if they show up with v3.4 on them.

@uzlonewolf
Copy link
Collaborator Author

Ha! I finally got around to opening the relays I got in last week, and... they're v3.4. I got the 4-channel version: https://www.aliexpress.com/item/3256803462490229.html

There were 2 problems with this PR code: 1) the AES decrypt was blowing up because it was trying to .decode() binary data, and 2) session key generation cannot be done in _get_socket() because the payload is already encrypted by this point. I already got 1) fixed but am still working on a clean way of fixing 2).

@uzlonewolf
Copy link
Collaborator Author

Oh yeah, a 3rd issue was the device rejects seqno 0, so that was updated to start at 1.

I ended up moving the session key generation into _generate_message() (which is called from generate_payload()) and made it also open the socket when the version is 3.4. Not the prettiest, but I don't see any better option with the key needing to be negotiated before the payload is encrypted. This also means I had to disable socket closing/reopening in _send_receive() as it has no way of re-encrypting the payload. Moving the encryption into _send_receive() may be something to think about for a future update.

@uzlonewolf
Copy link
Collaborator Author

When I updated the version bytes detection in _decode_payload() I originally removed the device22 check as I didn't think it was needed, but as I have no device22 devices I don't know for sure and so I added it back in with a slight modification so it only triggers if it also has a non-mod-16 payload length.

With that, I think it's ready. Closes #178 and #148

python3 v4-test.py 
DEBUG:TinyTuya [1.6.6]

DEBUG:status() entry (dev_type is v3.4)
DEBUG:building command 16 payload=b'{"data":{"devId":"eb6a946d9829d5aeb7dkvv","uid":"eb6a946d9829d5aeb7dkvv","t":"1662858248"},"protocol":5,"t":"1662858248"}'
DEBUG:payload generated=b'000055aa000000010000000300000044a810ff13bbf370cf0aba4628c9aff7b0c21600a8f8a9d09645c51888825cdb02816bb316150339faebb2da28cce00c3f7b597a8121925cf5808bc9f0299776fe0000aa55'
DEBUG:sending payload
DEBUG:received data=b'000055aa0000527c000000040000006800000000f5dc71bba39b3c6bf71083216d2ffaf38af576260f9e5b74ea79e66decf317e2d32f4b5a946b6aa383a59f587e6cbcc7c21600a8f8a9d09645c51888825cdb02001f1ad33bcbb6e42c66bf57ec8ca64547638f0c36723bb8582da35d419cfad50000aa55'
DEBUG:received message=TuyaMessage(seqno=21116, cmd=4, retcode=(0,), payload=b'\xf5\xdcq\xbb\xa3\x9b<k\xf7\x10\x83!m/\xfa\xf3\x8a\xf5v&\x0f\x9e[t\xeay\xe6m\xec\xf3\x17\xe2\xd3/KZ\x94kj\xa3\x83\xa5\x9fX~l\xbc\xc7\xc2\x16\x00\xa8\xf8\xa9\xd0\x96E\xc5\x18\x88\x82\\\xdb\x02', crc=b'\x00\x1f\x1a\xd3;\xcb\xb6\xe4,f\xbfW\xec\x8c\xa6EGc\x8f\x0c6r;\xb8X-\xa3]A\x9c\xfa\xd5', crc_good=True)
DEBUG:decrypting=b'\xf5\xdcq\xbb\xa3\x9b<k\xf7\x10\x83!m/\xfa\xf3\x8a\xf5v&\x0f\x9e[t\xeay\xe6m\xec\xf3\x17\xe2\xd3/KZ\x94kj\xa3\x83\xa5\x9fX~l\xbc\xc7\xc2\x16\x00\xa8\xf8\xa9\xd0\x96E\xc5\x18\x88\x82\\\xdb\x02'
DEBUG:decrypted session key negotiation step 2 payload=b"16255b75574358c4\xa6\xa1 \xba\xb3}\xc82a}T\xb6\xd9a'\xe5%\xda\xe5_a\xc4}\xc8\xee2\r\\\xf3\x1bS6"
DEBUG:payload type = <class 'bytes'> len = 48
DEBUG:session local nonce: b'0123456789abcdef' remote nonce: b'16255b75574358c4'
DEBUG:payload generated=b'000055aa0000000200000005000000545431df7c1d6718d792036a21c55d3d40889876f8f36af120c28f90926d94064dc21600a8f8a9d09645c51888825cdb026d1e54518fe9217cc3bdcbeebb157e3d709dffa0ec4a2cec0657a39f5822e1150000aa55'
DEBUG:sending payload
DEBUG:Session nonce XOR'd: b'\x01\x07\x00\x06\x01W\x01\x02\r\x0eUQV\\\x06R'
DEBUG:Session key negotiate success! session key: b'\x0c\xee\x92k\xc6\x04AC\x82\xd5z\x1c\xb4\xb5\xf3\xf1'
DEBUG:payload generated=b'000055aa0000000300000010000000a43a17e99bfcdebaffe76971af41adfeb7dfb8c5676aa58b0775b2047bf4f3e9b08f93a66830c7e0f17ba96ac69934387e36ae2291663819fa360e2407284262841fd9c212f0d513a81b0bec1b451a84e524f82fde66e7d5576205a4ca7f5e0537fffe6b48b822c4dc1f37a45f4260386de0f4b0c09134a77c80bd8967a142c233482e746e6d661f56c7be7d5ce032bce2fc2900258eb280c4c31db495dabc8d7f0000aa55'
DEBUG:sending payload
DEBUG:received data=b'000055aa0000527d00000010000000a800000000665885f7b8d46f18fb655095aa3b2dcc68b4a5f2f3dc997ac7c0b7cf6d0cee04b8e4b801a3bd6b7ab33742e8e0a3ece5a1c2649c50f03eb6039b7696cdce63a95c787f64559c187b8fb41b5a47597c2fafacbe8175505dd54419306bfb4b49d1ff84c70549c4a17bcab45f701b21334ae97b4443c602adf792f5f81aadfbef575fba4e991775a462e69bc6f0627fd53da3411a37e9ffc40497259dcd7df783850000aa55'
DEBUG:received message=TuyaMessage(seqno=21117, cmd=16, retcode=(0,), payload=b'fX\x85\xf7\xb8\xd4o\x18\xfbeP\x95\xaa;-\xcch\xb4\xa5\xf2\xf3\xdc\x99z\xc7\xc0\xb7\xcfm\x0c\xee\x04\xb8\xe4\xb8\x01\xa3\xbdkz\xb37B\xe8\xe0\xa3\xec\xe5\xa1\xc2d\x9cP\xf0>\xb6\x03\x9bv\x96\xcd\xcec\xa9\\x\x7fdU\x9c\x18{\x8f\xb4\x1bZGY|/\xaf\xac\xbe\x81uP]\xd5D\x190k\xfbKI\xd1\xff\x84\xc7\x05I\xc4\xa1{\xca\xb4_p\x1b!3J\xe9{DC\xc6\x02\xad\xf7\x92\xf5\xf8\x1a\xad\xfb\xefW', crc=b'_\xbaN\x99\x17u\xa4b\xe6\x9b\xc6\xf0b\x7f\xd5=\xa3A\x1a7\xe9\xff\xc4\x04\x97%\x9d\xcd}\xf7\x83\x85', crc_good=True)
DEBUG:raw unpacked message = TuyaMessage(seqno=21117, cmd=16, retcode=(0,), payload=b'fX\x85\xf7\xb8\xd4o\x18\xfbeP\x95\xaa;-\xcch\xb4\xa5\xf2\xf3\xdc\x99z\xc7\xc0\xb7\xcfm\x0c\xee\x04\xb8\xe4\xb8\x01\xa3\xbdkz\xb37B\xe8\xe0\xa3\xec\xe5\xa1\xc2d\x9cP\xf0>\xb6\x03\x9bv\x96\xcd\xcec\xa9\\x\x7fdU\x9c\x18{\x8f\xb4\x1bZGY|/\xaf\xac\xbe\x81uP]\xd5D\x190k\xfbKI\xd1\xff\x84\xc7\x05I\xc4\xa1{\xca\xb4_p\x1b!3J\xe9{DC\xc6\x02\xad\xf7\x92\xf5\xf8\x1a\xad\xfb\xefW', crc=b'_\xbaN\x99\x17u\xa4b\xe6\x9b\xc6\xf0b\x7f\xd5=\xa3A\x1a7\xe9\xff\xc4\x04\x97%\x9d\xcd}\xf7\x83\x85', crc_good=True)
DEBUG:decode payload=b'fX\x85\xf7\xb8\xd4o\x18\xfbeP\x95\xaa;-\xcch\xb4\xa5\xf2\xf3\xdc\x99z\xc7\xc0\xb7\xcfm\x0c\xee\x04\xb8\xe4\xb8\x01\xa3\xbdkz\xb37B\xe8\xe0\xa3\xec\xe5\xa1\xc2d\x9cP\xf0>\xb6\x03\x9bv\x96\xcd\xcec\xa9\\x\x7fdU\x9c\x18{\x8f\xb4\x1bZGY|/\xaf\xac\xbe\x81uP]\xd5D\x190k\xfbKI\xd1\xff\x84\xc7\x05I\xc4\xa1{\xca\xb4_p\x1b!3J\xe9{DC\xc6\x02\xad\xf7\x92\xf5\xf8\x1a\xad\xfb\xefW'
DEBUG:decrypting=b'fX\x85\xf7\xb8\xd4o\x18\xfbeP\x95\xaa;-\xcch\xb4\xa5\xf2\xf3\xdc\x99z\xc7\xc0\xb7\xcfm\x0c\xee\x04\xb8\xe4\xb8\x01\xa3\xbdkz\xb37B\xe8\xe0\xa3\xec\xe5\xa1\xc2d\x9cP\xf0>\xb6\x03\x9bv\x96\xcd\xcec\xa9\\x\x7fdU\x9c\x18{\x8f\xb4\x1bZGY|/\xaf\xac\xbe\x81uP]\xd5D\x190k\xfbKI\xd1\xff\x84\xc7\x05I\xc4\xa1{\xca\xb4_p\x1b!3J\xe9{DC\xc6\x02\xad\xf7\x92\xf5\xf8\x1a\xad\xfb\xefW'
DEBUG:decrypted 3.x payload='{"dps":{"1":false,"2":false,"3":false,"4":false,"9":0,"10":0,"11":0,"12":0,"38":"off","42":"","43":"","44":"","47":"button"}}'
DEBUG:payload type = <class 'str'>
DEBUG:decoded results='{"dps":{"1":false,"2":false,"3":false,"4":false,"9":0,"10":0,"11":0,"12":0,"38":"off","42":"","43":"","44":"","47":"button"}}'
DEBUG:status() received data={'dps': {'1': False, '2': False, '3': False, '4': False, '9': 0, '10': 0, '11': 0, '12': 0, '38': 'off', '42': '', '43': '', '44': '', '47': 'button'}}
{'dps': {'1': False, '2': False, '3': False, '4': False, '9': 0, '10': 0, '11': 0, '12': 0, '38': 'off', '42': '', '43': '', '44': '', '47': 'button'}}

@uzlonewolf uzlonewolf marked this pull request as ready for review September 11, 2022 01:06
@uzlonewolf uzlonewolf changed the title Try to support v3.4 devices Add support for v3.4 devices Sep 11, 2022
@uzlonewolf
Copy link
Collaborator Author

Well that thought was short-lived. Seems it does not like setting DPS values. Looking into it...

@uzlonewolf
Copy link
Collaborator Author

uzlonewolf commented Sep 11, 2022

I ended up writing a decoder to decode packet captures from between the app and device in local mode. To use:

  1. Packet capture local traffic (offline mode) from between the app and the device.
  2. Open it in Wireshark, find the stream, right-click a packet from it and select "Follow -> TCP Stream"
  3. Change "Show and save data as" to "YAML" and then "Save as..." to somewhere
  4. Install the YAML module for python. pip could not find it for me, so I had to apt install python3-yaml to get it
  5. Run this, after changing the lkey= to your key:
import sys
import yaml

import tinytuya

lkey = b'0123456789abcdef'

if( len(sys.argv) is not 2 ):
    print( 'Usage:', sys.argv[0], 'somefile.yaml' )
else:
    with open( sys.argv[1] ) as f:
        data = yaml.load(f)#, Loader=yaml.FullLoader)

    skey = lkey

    for pid in data:
        k = pid.split('_')
        packet_id = int(k[1])
        peer = int(k[0][-1])

        pkts = data[pid].split(b'\x00\x00U\xaa')
        for pkt in pkts:
            if len(pkt) == 0:
                continue
            pkt = b'\x00\x00U\xaa' + pkt
            if peer == 0:
                msg = tinytuya.unpack_message(pkt, hmac_key=skey, no_retcode=True)
            else:
                msg = tinytuya.unpack_message(pkt, hmac_key=skey)

            if len(msg.payload) == 0:
                #print('empty payload')
                continue

            if msg.cmd == 5:
                print('peer %d cmd %d seqno %d len %d - cmd 5 ignored' % (peer,msg.cmd,msg.seqno,len(msg.payload)))
                continue

            payload = msg.payload
            extra = len(payload) & 0x0F
            if extra != 0:
                print('payload len wrong!', len(payload), payload )
                print(pkt)
                payload = payload[:-extra]
                #payload += b'\0' * (16-extra)

            cipher = tinytuya.AESCipher(skey)
            payload = cipher.decrypt(payload, False, decode_text=False)
            print('peer %d cmd %d seqno %d raw len %d decoded len %d crc %r payload %r' % (peer,msg.cmd,msg.seqno,len(msg.payload),len(payload), msg.crc_good, payload))

            if msg.cmd == 3:
                skey = lkey
                lnonce = payload
            elif msg.cmd == 4:
                rnonce = payload[:16]
                skey = bytes( [ a^b for (a,b) in zip(lnonce,rnonce) ] )
                cipher = tinytuya.AESCipher(lkey)
                skey = cipher.encrypt(skey, False, pad=False)
                print( 'local nonce: %r remote nonce: %r session key: %r' % (lnonce, rnonce, skey) )
            else:
                pass

When run on a capture I made it said:

python3 wireshark-yaml-dec.py br10-20220910-67-only.yaml 
peer 0 cmd 3 seqno 12101 raw len 32 decoded len 16 crc True payload b'$\x1e\xf1\x10\xc8`+\x93\x12\xf0J?\xfeJ^\x98'
peer 1 cmd 4 seqno 22593 raw len 64 decoded len 48 crc True payload b'afd5d71d1711a42eX\x93Z=|\xa0\x93\x80\x84z\x10B\xf7\x1d%\xfbK(sn\xff@Lx\xa9;\xa5_\x8b\x87\\V'
local nonce: b'$\x1e\xf1\x10\xc8`+\x93\x12\xf0J?\xfeJ^\x98' remote nonce: b'afd5d71d1711a42e' session key: b'\x1b\x94\x06\xdd\xa6\x17\x15J\x01\xc2\xf8\x14\xae\xdc\xe2\x83'
peer 0 cmd 5 seqno 12102 len 48 - cmd 5 ignored
peer 0 cmd 16 seqno 12103 raw len 16 decoded len 2 crc True payload b'{}'
peer 0 cmd 64 seqno 12104 raw len 64 decoded len 57 crc True payload b'{"data":{"cids":[]},"reqType":"subdev_online_stat_query"}'
peer 1 cmd 16 seqno 22594 raw len 128 decoded len 124 crc True payload b'{"dps":{"1":true,"2":false,"3":false,"4":false,"9":0,"10":0,"11":0,"12":0,"38":"off","42":"","43":"","44":"","47":"button"}}'
peer 0 cmd 13 seqno 12105 raw len 80 decoded len 70 crc True payload b'3.4\x00\x00\x00\x00\x00\x00\x00\x07\x00\r\xfc\\{"data":{"dps":{"2":true}},"protocol":5,"t":1662871092}'
peer 1 cmd 8 seqno 22596 raw len 80 decoded len 70 crc True payload b'3.4\x00\x00\x00\x00\x00\x00\xfbo\x00\x00\x00\x01{"protocol":4,"t":1662871093,"data":{"dps":{"2":true}}}'
peer 0 cmd 13 seqno 12106 raw len 80 decoded len 70 crc True payload b'3.4\x00\x00\x00\x00\x00\x00\x00\x08\x00\r\xfc\\{"data":{"dps":{"3":true}},"protocol":5,"t":1662871093}'
peer 1 cmd 8 seqno 22598 raw len 80 decoded len 70 crc True payload b'3.4\x00\x00\x00\x00\x00\x00\xfbp\x00\x00\x00\x01{"protocol":4,"t":1662871094,"data":{"dps":{"3":true}}}'
peer 0 cmd 13 seqno 12107 raw len 80 decoded len 70 crc True payload b'3.4\x00\x00\x00\x00\x00\x00\x00\t\x00\r\xfc\\{"data":{"dps":{"4":true}},"protocol":5,"t":1662871094}'
peer 1 cmd 8 seqno 22600 raw len 80 decoded len 70 crc True payload b'3.4\x00\x00\x00\x00\x00\x00\xfbq\x00\x00\x00\x01{"protocol":4,"t":1662871095,"data":{"dps":{"4":true}}}'
peer 0 cmd 13 seqno 12108 raw len 80 decoded len 71 crc True payload b'3.4\x00\x00\x00\x00\x00\x00\x00\n\x00\r\xfc\\{"data":{"dps":{"1":false}},"protocol":5,"t":1662871096}'
peer 1 cmd 8 seqno 22602 raw len 80 decoded len 71 crc True payload b'3.4\x00\x00\x00\x00\x00\x00\xfbr\x00\x00\x00\x01{"protocol":4,"t":1662871097,"data":{"dps":{"1":false}}}'
peer 0 cmd 13 seqno 12110 raw len 96 decoded len 91 crc True payload b'3.4\x00\x00\x00\x00\x00\x00\x00\x0b\x00\r\xfc\\{"data":{"dps":{"2":false,"3":false,"4":false}},"protocol":5,"t":1662871101}'
peer 1 cmd 8 seqno 22605 raw len 80 decoded len 71 crc True payload b'3.4\x00\x00\x00\x00\x00\x00\xfbs\x00\x00\x00\x01{"protocol":4,"t":1662871102,"data":{"dps":{"4":false}}}'
peer 1 cmd 8 seqno 22606 raw len 80 decoded len 71 crc True payload b'3.4\x00\x00\x00\x00\x00\x00\xfbt\x00\x00\x00\x01{"protocol":4,"t":1662871102,"data":{"dps":{"3":false}}}'
peer 1 cmd 8 seqno 22607 raw len 80 decoded len 71 crc True payload b'3.4\x00\x00\x00\x00\x00\x00\xfbu\x00\x00\x00\x01{"protocol":4,"t":1662871102,"data":{"dps":{"2":false}}}'
peer 0 cmd 13 seqno 12111 raw len 112 decoded len 97 crc True payload b'3.4\x00\x00\x00\x00\x00\x00\x00\x0c\x00\r\xfc\\{"data":{"dps":{"1":true,"2":true,"3":true,"4":true}},"protocol":5,"t":1662871104}'
peer 1 cmd 8 seqno 22609 raw len 80 decoded len 70 crc True payload b'3.4\x00\x00\x00\x00\x00\x00\xfbv\x00\x00\x00\x01{"protocol":4,"t":1662871105,"data":{"dps":{"4":true}}}'
peer 1 cmd 8 seqno 22610 raw len 80 decoded len 70 crc True payload b'3.4\x00\x00\x00\x00\x00\x00\xfbw\x00\x00\x00\x01{"protocol":4,"t":1662871105,"data":{"dps":{"3":true}}}'
peer 1 cmd 8 seqno 22611 raw len 80 decoded len 70 crc True payload b'3.4\x00\x00\x00\x00\x00\x00\xfbx\x00\x00\x00\x01{"protocol":4,"t":1662871105,"data":{"dps":{"2":true}}}'
peer 1 cmd 8 seqno 22612 raw len 80 decoded len 70 crc True payload b'3.4\x00\x00\x00\x00\x00\x00\xfby\x00\x00\x00\x01{"protocol":4,"t":1662871105,"data":{"dps":{"1":true}}}'
peer 0 cmd 13 seqno 12113 raw len 80 decoded len 73 crc True payload b'3.4\x00\x00\x00\x00\x00\x00\x00\r\x00\r\xfc\\{"data":{"dps":{"47":"flip"}},"protocol":5,"t":1662871118}'
peer 1 cmd 8 seqno 22615 raw len 80 decoded len 73 crc True payload b'3.4\x00\x00\x00\x00\x00\x00\xfbz\x00\x00\x00\x01{"protocol":4,"t":1662871119,"data":{"dps":{"47":"flip"}}}'
peer 0 cmd 13 seqno 12115 raw len 80 decoded len 75 crc True payload b'3.4\x00\x00\x00\x00\x00\x00\x00\x0e\x00\r\xfc\\{"data":{"dps":{"47":"button"}},"protocol":5,"t":1662871121}'
peer 1 cmd 8 seqno 22618 raw len 80 decoded len 75 crc True payload b'3.4\x00\x00\x00\x00\x00\x00\xfb{\x00\x00\x00\x01{"protocol":4,"t":1662871122,"data":{"dps":{"47":"button"}}}'
peer 0 cmd 13 seqno 12116 raw len 80 decoded len 71 crc True payload b'3.4\x00\x00\x00\x00\x00\x00\x00\x0f\x00\r\xfc\\{"data":{"dps":{"38":"on"}},"protocol":5,"t":1662871127}'
peer 1 cmd 8 seqno 22620 raw len 80 decoded len 71 crc True payload b'3.4\x00\x00\x00\x00\x00\x00\xfb|\x00\x00\x00\x01{"protocol":4,"t":1662871128,"data":{"dps":{"38":"on"}}}'
peer 0 cmd 13 seqno 12117 raw len 80 decoded len 75 crc True payload b'3.4\x00\x00\x00\x00\x00\x00\x00\x10\x00\r\xfc\\{"data":{"dps":{"38":"memory"}},"protocol":5,"t":1662871130}'
peer 1 cmd 8 seqno 22622 raw len 80 decoded len 75 crc True payload b'3.4\x00\x00\x00\x00\x00\x00\xfb}\x00\x00\x00\x01{"protocol":4,"t":1662871131,"data":{"dps":{"38":"memory"}}}'
peer 0 cmd 13 seqno 12119 raw len 80 decoded len 72 crc True payload b'3.4\x00\x00\x00\x00\x00\x00\x00\x11\x00\r\xfc\\{"data":{"dps":{"38":"off"}},"protocol":5,"t":1662871132}'
peer 1 cmd 8 seqno 22625 raw len 80 decoded len 72 crc True payload b'3.4\x00\x00\x00\x00\x00\x00\xfb~\x00\x00\x00\x01{"protocol":4,"t":1662871134,"data":{"dps":{"38":"off"}}}'
peer 0 cmd 13 seqno 12121 raw len 80 decoded len 73 crc True payload b'3.4\x00\x00\x00\x00\x00\x00\x00\x12\x00\r\xfc\\{"data":{"dps":{"44":"AQAB"}},"protocol":5,"t":1662871145}'
peer 1 cmd 8 seqno 22628 raw len 80 decoded len 73 crc True payload b'3.4\x00\x00\x00\x00\x00\x00\xfb\x7f\x00\x00\x00\x01{"protocol":4,"t":1662871146,"data":{"dps":{"44":"AQAB"}}}'
peer 1 cmd 8 seqno 22629 raw len 80 decoded len 71 crc True payload b'3.4\x00\x00\x00\x00\x00\x00\xfb\x80\x00\x00\x00\x01{"protocol":4,"t":1662871146,"data":{"dps":{"1":false}}}'

@uzlonewolf
Copy link
Collaborator Author

This was a friggin can-o-worms. I think I got everything now though. Unlike 3.3 devices, 3.4 devices encrypt the version header in addition to the payload, and if you send them a bad command they drop the connection and you need to close and re-open it (and renegotiate the session key) before you can send anything else.

Copy link
Owner

@jasonacox jasonacox left a comment

Choose a reason for hiding this comment

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

Thanks @uzlonewolf ! I'm running full test on the 3.4 device I just got (this EDIT). So far so good!

Only problem so far is that the scanner.py functions do not work with 3.4 - taking a look now. I suspect your draft #177 is working?

@jasonacox jasonacox merged commit 381417a into jasonacox:master Sep 11, 2022
@jasonacox
Copy link
Owner

jasonacox commented Sep 12, 2022

I see the problem... this works:

d = tinytuya.OutletDevice('id', '10.0.1.90', 'key')
d.set_version(3.4)
d.set_socketPersistent(True)
print(d.status())

But if you don't set_socketPersistent() it fails:

d = tinytuya.OutletDevice('id', '10.0.1.90', 'key')
d.set_version(3.4)
print(d.status())
Traceback (most recent call last):
  File "/Users/jason/Code/tinytuya/sandbox/test.py", line 17, in <module>
    print(d.status())
  File "/Users/jason/Code/tinytuya/sandbox/tinytuya/core.py", line 1225, in status
    payload = self.generate_payload(query_type)
  File "/Users/jason/Code/tinytuya/sandbox/tinytuya/core.py", line 1214, in generate_payload
    return self._generate_message(command_override, payload)
  File "/Users/jason/Code/tinytuya/sandbox/tinytuya/core.py", line 919, in _generate_message
    self.socket.settimeout(self.connection_timeout)
AttributeError: 'NoneType' object has no attribute 'settimeout'

@jasonacox
Copy link
Owner

Correct me if I'm wrong, but we really can't have a 3.4 device that is non persistent. It negotiates a session based key so there is no point in a non-persistent version. I need to work through implications or what I'm missing, but this is a quick fix in core.py:

image

I confirmed that this change also fixes scanner.py functions for 3.4 devices too. I'm pushing change up for now and staging it and release notes as version 1.7.0 (not pushing to PyPI yet).

@uzlonewolf
Copy link
Collaborator Author

I don't see why it couldn't be non-persistent, it's just instead of "connect -> send request -> disconnect" it will "connect -> negotiate key -> send request ->disconnect". In fact if someone has a program that waits more than ~30 seconds between requests then it must do this as otherwise the socket will be closed due to inactivity and the next _send_receive() will fail because it has no way of re-opening the socket or re-encrypting the payload.

The more I think about it the less I like how generate_payload() / _generate_message() do the encrypting. I think I'm going to fix persistence for the current code and then rework it so the encryption is done in _send_receive(). This may cause issues if people are calling _send_receive() with something other than what generate_payload() returns, but I think ultimately it will be the better solution.

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