jasonacox / tuyapower

Python module to read status and energy monitoring data from Tuya based WiFi smart devices. This includes state (on/off), current (mA), voltage (V), and power (wattage).
MIT License
136 stars 20 forks source link

Tuya CO2 PM2.5 temp humidity sensor #14

Open grigvlad opened 3 years ago

grigvlad commented 3 years ago

I have the CO2 PM2.5 temp humidity sensor. Despite developing in Labview and not in Python, I hope you could help. My device works well with app, and also I have managed to communicate with it using my code in Labview using Tuya APIs. I get its device id and category, but it says the reading of measurements is not supported. In wireshark, I got its UDP payload, 172 bytes length. How can I decode it? what is the structure of the payload ? 1.txt hope for your help

jasonacox commented 3 years ago

I don't have a Tuya sensor to help you troubleshoot. What device did you get?

The tinytuya or tuyapower scan functions should decrypt the UDP payload:

python -m tinytuya

Do you see the sensor in that list? If so, grab the Device ID and IP address. You can get the LOCAL KEY via the method described here: https://github.com/jasonacox/tinytuya#get-the-tuya-device-local-key

In case it helps, the data points (DPS) mapping for the sensor should be here: https://github.com/jasonacox/tinytuya#version-33---sensor-type

I suspect sensors may not respond to polling, but if it does, you could try something like:

import tinytuya

deviceID = "xxxxxxxx"
ipAddress = "xxxxxxxx"
localKey = "xxxxxxxx"

d = tinytuya.OutletDevice(deviceID, ipAddress, local_key)
d.set_version(3.3)
data = d.status() 

print('Response from Sensor: %r' % data)

If that doesn't work, we would need to decrypt the UDP payload. It would be something similar to how we decode the response payload (see https://github.com/jasonacox/tinytuya/blob/master/tinytuya/__init__.py)


        log.debug('status received data=%r', data)

        result = data[20:-8]  # hard coded offsets
        if self.dev_type != 'default':
            result = result[15:]

        log.debug('result=%r', result)

        if result.startswith(b'{'):
            # this is the regular expected code path
            if not isinstance(result, str):
                result = result.decode()
            result = json.loads(result)
        elif result.startswith(PROTOCOL_VERSION_BYTES_31):
            # got an encrypted payload
            # expect resulting json to look similar to:: {"devId":"ID","dps":{"1":true,"2":0},"t":EPOCH_SECS,"s":3_DIGIT_NUM}
            result = result[len(PROTOCOL_VERSION_BYTES_31):]  # remove version header
            result = result[16:]  # Remove 16-bytes appears to be MD5 hexdigest of payload
            cipher = AESCipher(self.local_key)
            result = cipher.decrypt(result)
            log.debug('decrypted result=%r', result)
            if not isinstance(result, str):
                result = result.decode()
            result = json.loads(result)
        elif self.version == 3.3:
            cipher = AESCipher(self.local_key)
            result = cipher.decrypt(result, False)
            log.debug('decrypted result=%r', result)
            if not isinstance(result, str):
                result = result.decode()
            result = json.loads(result)
        else:
            log.error('Unexpected status() payload=%r', result)

        return result
grigvlad commented 3 years ago

hi thanks for your advice. as i said,I am working with labview, not Python, so unable to use your code directly. The device responses well to GET https://openapi.tuyaeu.com/v1.0/devices/#deviceid# https://openapi.tuyaeu.com/v1.0/devices/#deviceid# and returns it category and name and online status, which is [image: image.png] but when I try to use the GET https://openapi.tuyaeu.com/v1.0/devices/#deviceid#/status https://openapi.tuyaeu.com/v1.0/devices/#deviceid#/status or similar commands (statistics, log, etc) , the device returns error message, like [image: image.png] Is that what you mean that sensor don't response to polling ?

I know the IP of the sensor, as it is connected to my home router.

The UDP payload recorded by wireshark is (alo attached in a file)

0000 00 00 55 aa 00 00 00 00 00 00 00 13 00 00 00 9c ..U..... ........ 0010 00 00 00 00 d0 97 66 67 6f 33 69 eb 10 b5 e9 f1 ......fg o3i..... 0020 32 fd 80 2a 51 b4 1a 6c b8 79 4d d0 1f 28 ca 5e 2..*Q..l .yM..(.^ 0030 45 ed cb 20 38 ba 18 05 44 31 0a 27 91 c1 60 59 E.. 8... D1.'..`Y 0040 ab 44 0e 92 9e 57 8a 1a ba 06 b8 2b 52 24 4f fb .D...W.. ...+R$O. 0050 cc f8 57 c8 72 56 f9 92 ef 0b b3 c9 94 6f 6c a8 ..W.rV.. .....ol. 0060 e2 e1 48 53 2e 0d bc 9a 92 c3 31 72 86 eb f2 38 ..HS.... ..1r...8 0070 be 57 97 86 7f d5 18 2f 0d c1 94 9b fe 08 16 f2 .W...../ ........ 0080 f9 db 35 80 7f 1d 5d 33 0b 04 95 e8 94 e3 fb 57 ..5...]3 .......W 0090 71 e9 be 66 7a 23 e3 b2 24 9f 5a df 48 a7 14 f8 q..fz#.. $.Z.H... 00A0 51 aa f4 39 85 c4 56 44 00 00 aa 55 Q..9..VD ...U

The device reports its data to the SMART LIFE APP, so if I just could decode the fields of the payload I will have all I need. Attached the picture of the device screen and its page in the APP. You see , there are a lot of measurements.

Hope you understand something from what I am writing here :)

Thanks

Vlad

ולדי גריגורוביץ נייד 050-7756282

On Mon, Nov 23, 2020 at 8:33 PM Jason Cox notifications@github.com wrote:

I don't have a Tuya sensor to help you troubleshoot. What device did you get?

The tinytuya or tuyapower scan functions should decrypt the UDP payload:

python -m tinytuya

Do you see the sensor in that list? If so, grab the Device ID and IP address. You can get the LOCAL KEY via the method described here: https://github.com/jasonacox/tinytuya#get-the-tuya-device-local-key

In case it helps, the data points (DPS) mapping for the sensor should be here: https://github.com/jasonacox/tinytuya#version-33---sensor-type

I suspect sensors may not respond to polling, but if it does, you could try something like:

import tinytuya deviceID = "xxxxxxxx"ipAddress = "xxxxxxxx"localKey = "xxxxxxxx" d = tinytuya.OutletDevice(deviceID, ipAddress, local_key)d.set_version(3.3)data = d.status() print('Response from Sensor: %r' % data)

If that doesn't work, we would need to decrypt the UDP payload. It would be something similar to how we decode the response payload (see https://github.com/jasonacox/tinytuya/blob/master/tinytuya/__init__.py)

    log.debug('status received data=%r', data)

    result = data[20:-8]  # hard coded offsets
    if self.dev_type != 'default':
        result = result[15:]

    log.debug('result=%r', result)

    if result.startswith(b'{'):
        # this is the regular expected code path
        if not isinstance(result, str):
            result = result.decode()
        result = json.loads(result)
    elif result.startswith(PROTOCOL_VERSION_BYTES_31):
        # got an encrypted payload
        # expect resulting json to look similar to:: {"devId":"ID","dps":{"1":true,"2":0},"t":EPOCH_SECS,"s":3_DIGIT_NUM}
        result = result[len(PROTOCOL_VERSION_BYTES_31):]  # remove version header
        result = result[16:]  # Remove 16-bytes appears to be MD5 hexdigest of payload
        cipher = AESCipher(self.local_key)
        result = cipher.decrypt(result)
        log.debug('decrypted result=%r', result)
        if not isinstance(result, str):
            result = result.decode()
        result = json.loads(result)
    elif self.version == 3.3:
        cipher = AESCipher(self.local_key)
        result = cipher.decrypt(result, False)
        log.debug('decrypted result=%r', result)
        if not isinstance(result, str):
            result = result.decode()
        result = json.loads(result)
    else:
        log.error('Unexpected status() payload=%r', result)

    return result

— You are receiving this because you authored the thread. Reply to this email directly, view it on GitHub https://github.com/jasonacox/tuyapower/issues/14#issuecomment-732347546, or unsubscribe https://github.com/notifications/unsubscribe-auth/AR3QACIBZMABHRH5WRQ6P3TSRKTIJANCNFSM4T7LTKOA .

0000 00 00 55 aa 00 00 00 00 00 00 00 13 00 00 00 9c ..U..... ........ 0010 00 00 00 00 d0 97 66 67 6f 33 69 eb 10 b5 e9 f1 ......fg o3i..... 0020 32 fd 80 2a 51 b4 1a 6c b8 79 4d d0 1f 28 ca 5e 2..*Q..l .yM..(.^ 0030 45 ed cb 20 38 ba 18 05 44 31 0a 27 91 c1 60 59 E.. 8... D1.'..`Y 0040 ab 44 0e 92 9e 57 8a 1a ba 06 b8 2b 52 24 4f fb .D...W.. ...+R$O. 0050 cc f8 57 c8 72 56 f9 92 ef 0b b3 c9 94 6f 6c a8 ..W.rV.. .....ol. 0060 e2 e1 48 53 2e 0d bc 9a 92 c3 31 72 86 eb f2 38 ..HS.... ..1r...8 0070 be 57 97 86 7f d5 18 2f 0d c1 94 9b fe 08 16 f2 .W...../ ........ 0080 f9 db 35 80 7f 1d 5d 33 0b 04 95 e8 94 e3 fb 57 ..5...]3 .......W 0090 71 e9 be 66 7a 23 e3 b2 24 9f 5a df 48 a7 14 f8 q..fz#.. $.Z.H... 00A0 51 aa f4 39 85 c4 56 44 00 00 aa 55 Q..9..VD ...U

grigvlad commented 3 years ago

hi I am again on wireshark

[image: image.png]

192.168.1.213 is the IP of the device [image: image.png]

ולדי גריגורוביץ נייד 050-7756282

On Mon, Nov 23, 2020 at 9:43 PM Vlad Grigorovitch grigvlad@gmail.com wrote:

hi thanks for your advice. as i said,I am working with labview, not Python, so unable to use your code directly. The device responses well to GET https://openapi.tuyaeu.com/v1.0/devices/#deviceid# https://openapi.tuyaeu.com/v1.0/devices/#deviceid%23 and returns it category and name and online status, which is [image: image.png] but when I try to use the GET https://openapi.tuyaeu.com/v1.0/devices/#deviceid#/status https://openapi.tuyaeu.com/v1.0/devices/#deviceid%23/status or similar commands (statistics, log, etc) , the device returns error message, like [image: image.png] Is that what you mean that sensor don't response to polling ?

I know the IP of the sensor, as it is connected to my home router.

The UDP payload recorded by wireshark is (alo attached in a file)

0000 00 00 55 aa 00 00 00 00 00 00 00 13 00 00 00 9c ..U..... ........ 0010 00 00 00 00 d0 97 66 67 6f 33 69 eb 10 b5 e9 f1 ......fg o3i..... 0020 32 fd 80 2a 51 b4 1a 6c b8 79 4d d0 1f 28 ca 5e 2..*Q..l .yM..(.^ 0030 45 ed cb 20 38 ba 18 05 44 31 0a 27 91 c1 60 59 E.. 8... D1.'..`Y 0040 ab 44 0e 92 9e 57 8a 1a ba 06 b8 2b 52 24 4f fb .D...W.. ...+R$O. 0050 cc f8 57 c8 72 56 f9 92 ef 0b b3 c9 94 6f 6c a8 ..W.rV.. .....ol. 0060 e2 e1 48 53 2e 0d bc 9a 92 c3 31 72 86 eb f2 38 ..HS.... ..1r...8 0070 be 57 97 86 7f d5 18 2f 0d c1 94 9b fe 08 16 f2 .W...../ ........ 0080 f9 db 35 80 7f 1d 5d 33 0b 04 95 e8 94 e3 fb 57 ..5...]3 .......W 0090 71 e9 be 66 7a 23 e3 b2 24 9f 5a df 48 a7 14 f8 q..fz#.. $.Z.H... 00A0 51 aa f4 39 85 c4 56 44 00 00 aa 55 Q..9..VD ...U

The device reports its data to the SMART LIFE APP, so if I just could decode the fields of the payload I will have all I need. Attached the picture of the device screen and its page in the APP. You see , there are a lot of measurements.

Hope you understand something from what I am writing here :)

Thanks

Vlad

ולדי גריגורוביץ נייד 050-7756282

On Mon, Nov 23, 2020 at 8:33 PM Jason Cox notifications@github.com wrote:

I don't have a Tuya sensor to help you troubleshoot. What device did you get?

The tinytuya or tuyapower scan functions should decrypt the UDP payload:

python -m tinytuya

Do you see the sensor in that list? If so, grab the Device ID and IP address. You can get the LOCAL KEY via the method described here: https://github.com/jasonacox/tinytuya#get-the-tuya-device-local-key

In case it helps, the data points (DPS) mapping for the sensor should be here: https://github.com/jasonacox/tinytuya#version-33---sensor-type

I suspect sensors may not respond to polling, but if it does, you could try something like:

import tinytuya deviceID = "xxxxxxxx"ipAddress = "xxxxxxxx"localKey = "xxxxxxxx" d = tinytuya.OutletDevice(deviceID, ipAddress, local_key)d.set_version(3.3)data = d.status() print('Response from Sensor: %r' % data)

If that doesn't work, we would need to decrypt the UDP payload. It would be something similar to how we decode the response payload (see https://github.com/jasonacox/tinytuya/blob/master/tinytuya/__init__.py)

    log.debug('status received data=%r', data)

    result = data[20:-8]  # hard coded offsets
    if self.dev_type != 'default':
        result = result[15:]

    log.debug('result=%r', result)

    if result.startswith(b'{'):
        # this is the regular expected code path
        if not isinstance(result, str):
            result = result.decode()
        result = json.loads(result)
    elif result.startswith(PROTOCOL_VERSION_BYTES_31):
        # got an encrypted payload
        # expect resulting json to look similar to:: {"devId":"ID","dps":{"1":true,"2":0},"t":EPOCH_SECS,"s":3_DIGIT_NUM}
        result = result[len(PROTOCOL_VERSION_BYTES_31):]  # remove version header
        result = result[16:]  # Remove 16-bytes appears to be MD5 hexdigest of payload
        cipher = AESCipher(self.local_key)
        result = cipher.decrypt(result)
        log.debug('decrypted result=%r', result)
        if not isinstance(result, str):
            result = result.decode()
        result = json.loads(result)
    elif self.version == 3.3:
        cipher = AESCipher(self.local_key)
        result = cipher.decrypt(result, False)
        log.debug('decrypted result=%r', result)
        if not isinstance(result, str):
            result = result.decode()
        result = json.loads(result)
    else:
        log.error('Unexpected status() payload=%r', result)

    return result

— You are receiving this because you authored the thread. Reply to this email directly, view it on GitHub https://github.com/jasonacox/tuyapower/issues/14#issuecomment-732347546, or unsubscribe https://github.com/notifications/unsubscribe-auth/AR3QACIBZMABHRH5WRQ6P3TSRKTIJANCNFSM4T7LTKOA .

jasonacox commented 3 years ago

Hi @grigvlad - Keep in mind that Tuya devices are designed to communicate with the TuyaCloud. Once you add the device to the SmartLife App, it creates a LocalKey that is used to encrypt the data payloads. Without that key you can't read the payloads directly from the App. However, you may be able to use the TuyaCloud to get that information. It requires that you use your credential to get a token to request the data directly. I suggest you check out the developer information on Tuya's website: https://developer.tuya.com/en/docs/iot .

grigvlad commented 3 years ago

Hi I have the key and I am able to get a token. The developer section on Tuya website is intended for hardware developing , while I am trying to develop software using existing hardware. Thank you anyway for trying.

ולדי גריגורוביץ נייד 050-7756282

On Tue, Nov 24, 2020 at 3:31 AM Jason Cox notifications@github.com wrote:

Hi @grigvlad https://github.com/grigvlad - Keep in mind that Tuya devices are designed to communicate with the TuyaCloud. Once you add the device to the SmartLife App, it creates a LocalKey that is used to encrypt the data payloads. Without that key you can't read the payloads directly from the App. However, you may be able to use the TuyaCloud to get that information. It requires that you use your credential to get a token to request the data directly. I suggest you check out the developer information on Tuya's website: https://developer.tuya.com/en/docs/iot .

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/jasonacox/tuyapower/issues/14#issuecomment-732526315, or unsubscribe https://github.com/notifications/unsubscribe-auth/AR3QACJAC5AEKR4QKDLUNUDSRMEGLANCNFSM4T7LTKOA .

jasonacox commented 3 years ago

Thanks Vladi, I would very much like to hear how your research goes.

Looking at the payload you are getting back, I do see the Tuya prefix and suffix:

        "prefix": "000055aa00000000000000",
        # Next byte is command "hexByte" + length of remaining payload + command + suffix
        # (unclear if multiple bytes used for length, zero padding implies could be more
        # than one byte)
        "suffix": "000000000000aa55"

I don't know the labview equivalent, but a python approach could be:

        # data contains the payload
        # KEY is the Tuya local key you have

        result = data[20:-8]  # trim off the first 20 bytes (header) and last 8 (crc?) 
        cipher = AESCipher(KEY)
        result = cipher.decrypt(result, False)
        print('decrypted result=%r', result)
grigvlad commented 3 years ago

hi If 13 is the hex byte, 0x0000009C = 156 bytes is the length of data. if I count from address 0xB (the first zero after 13) to A7 (last byte before suffix), I have exactly 0x9C = 156 bytes. Does it mean that encrypted data includes 156 bytes starting actually from the zeros just after the '13' hexbyte ?

[image: image.png]

Tuya uses two steps SHA-256 encryption. At first step to get the token, while the string to encode includes client ID + secret key + timestamp. At the second step, the access signature is calculated again, this time by client ID + secret key + token + timestamp. I have all the info, but what about then timestamp. Does UDP payload report the timestamp in seconds or milliseconds ? I doubt what of the above info (client ID + secret key + token + timestamp) is required to decrypt the payload ? Vlad

ולדי גריגורוביץ נייד 050-7756282

On Tue, Nov 24, 2020 at 8:23 PM Jason Cox notifications@github.com wrote:

Thanks Vladi, I would very much like to hear how your research goes.

Looking at the payload you are getting back, I do see the Tuya prefix and suffix:

    "prefix": "000055aa00000000000000",
    # Next byte is command "hexByte" + length of remaining payload + command + suffix
    # (unclear if multiple bytes used for length, zero padding implies could be more
    # than one byte)
    "suffix": "000000000000aa55"

I don't know the labview equivalent, but a python approach could be:

    # data contains the payload
    # KEY is the Tuya local key you have

    result = data[20:-8]  # trim off the first 20 bytes (header) and last 8 (crc?)
    cipher = AESCipher(KEY)
    result = cipher.decrypt(result, False)
    print('decrypted result=%r', result)

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/jasonacox/tuyapower/issues/14#issuecomment-733154465, or unsubscribe https://github.com/notifications/unsubscribe-auth/AR3QACLSVTUC6N4KWZRAISTSRP2ZPANCNFSM4T7LTKOA .