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

Mapping for DP IDs #353

Closed mschlenstedt closed 1 year ago

mschlenstedt commented 1 year ago

Hi all,

I added a first "Proof of Concept" for doing a DPS <-> Name/Code mapping to have a more comfortable way to set and read the DPS.

At a first step I included mappings from IOBroker and FHEM project (they already use the DPS<->Code/Name mapping). The mapping is read once at startup after reading the devices.json and after saving a new deivces.json. Only those mappings are kept in memory (in a dict) which are currently used. This keeps things small and almost fast.

/status/{DEVICE_ID} now additionally give back the mapping scheme if one exists for the Product_Key in mappings/mappings.json:

{"devId": "05047xxxxxx923ca8", "dps": {"1": false, "2": 21, "3": 13, "4": "hot", "9": 256}, "dps_scheme": [{"id": 1, "code": "Power"}, {"id": 2, "code": "temp_set"}, {"id": 3, "code": "temp_current"}, {"id": 4, "code": "mode"}, {"id": 9, "code": "fault"}]}

At a first step I included the mapping in the WebUI:

image

What's still missing:

jasonacox commented 1 year ago

Very cool @mschlenstedt !

@uzlonewolf - I know we don't want to burden the library with a large json mapping payload (it is TINYtuya after all), but this seems reasonable for the server tool. Thoughts? 😄

mschlenstedt commented 1 year ago

I agree for the Lib to not include mapping - this should be done on the application side. But as the server is an application, I would really like mapping here :-)

uzlonewolf commented 1 year ago

I'm actually about half done with a DP mapping thing myself. Instead of manually assigning names, it uses the cloud.getdps() to pull all of them after grouping by product_id and saves them to mappings.json. I'll see if I can get it cleaned up later today and get a PR in.

mschlenstedt commented 1 year ago

Grabbing the mapping "live" is cool - I wasn't successfull with it to be honest. But it's more "tiny" than delivering a 5 MB json database...

Important for me is that the /status call delivers the scheme for other software which uses the API. I would like to use it from my MQTT server script.

uzlonewolf commented 1 year ago

I wasn't successful with it to be honest

What problem did you run into?

mschlenstedt commented 1 year ago

I played a little bit around with https://eu.iot.tuya.com/cloud/explorer But I haven't found any Cloud call which gives me back name/code and DP ID. Best results I found was with https://openapi.tuyaeu.com/v1.2/iot-03/devices/{device_id}/specification and/or https://openapi.tuyaeu.com/v1.0/iot-03/devices/{device_id}/functions. But both just give me the names and codes, but not the corresponding DPS.

{
  "result": {
    "category": "rs",
    "functions": [
      {
        "code": "Power",
        "desc": "{}",
        "name": "开关",
        "type": "Boolean",
        "values": "{}"
      },
      {
        "code": "temp_set",
        "desc": "{\"unit\":\"℃\",\"min\":8,\"max\":40,\"scale\":0,\"step\":1}",
        "name": "设置温度",
        "type": "Integer",
        "values": "{\"unit\":\"℃\",\"min\":8,\"max\":40,\"scale\":0,\"step\":1}"
      },
      {
        "code": "mode",
        "desc": "{\"range\":[\"auto\",\"cold\",\"hot\"]}",
        "name": "模式",
        "type": "Enum",
        "values": "{\"range\":[\"auto\",\"cold\",\"hot\"]}"
      }
    ],
    "status": [
      {
        "code": "Power",
        "name": "开关",
        "type": "Boolean",
        "values": "{}"
      },
      {
        "code": "temp_set",
        "name": "设置温度",
        "type": "Integer",
        "values": "{\"unit\":\"℃\",\"min\":8,\"max\":40,\"scale\":0,\"step\":1}"
      },
      {
        "code": "temp_current",
        "name": "当前温度",
        "type": "Integer",
        "values": "{\"unit\":\"℃\",\"min\":-10,\"max\":100,\"scale\":0,\"step\":1}"
      },
      {
        "code": "mode",
        "name": "模式",
        "type": "Enum",
        "values": "{\"range\":[\"auto\",\"cold\",\"hot\"]}"
      },
      {
        "code": "fault",
        "name": "故障告警",
        "type": "Bitmap",
        "values": "{\"label\":[\"P1\",\"P2\",\"P3\",\"P4\",\"P6\",\"P7\",\"P8\",\"P9\",\"PL\",\"PC\",\"E3\",\"E4\",\"E8\",\"CS\",\"E6\"],\"maxlen\":15}"
      }
    ]
  },
  "success": true,
  "t": 1684949165736,
  "tid": "105ee88bfa5811edbfbfa24bf1d21c99"
}
uzlonewolf commented 1 year ago

Does tinytuya's cloud.getdps(dev_id) (which calls /v1.1/devices/<device>/specifications) not list them for you? It lists them for my devices:

{
  "result": {
    "category": "kg",
    "functions": [
      {
        "code": "switch_1",
        "dp_id": 1,
        "type": "Boolean",
        "values": "{}"
      },
      {
        "code": "countdown_1",
        "dp_id": 7,
        "type": "Integer",
        "values": "{\"unit\":\"s\",\"min\":0,\"max\":86400,\"scale\":0,\"step\":1}"
      },
      {
        "code": "sense_0",
        "dp_id": 101,
        "type": "Enum",
        "values": "{\"range\":[\"sense\"]}"
      },
      {
        "code": "sense_1",
        "dp_id": 102,
        "type": "Enum",
        "values": "{\"range\":[\"sense\"]}"
      },
...
      {
        "code": "sense_13",
        "dp_id": 114,
        "type": "Enum",
        "values": "{\"range\":[\"sense\"]}"
      }
    ],
    "status": [
      {
        "code": "switch_1",
        "dp_id": 1,
        "type": "Boolean",
        "values": "{}"
      },
      {
        "code": "countdown_1",
        "dp_id": 7,
        "type": "Integer",
        "values": "{\"unit\":\"s\",\"min\":0,\"max\":86400,\"scale\":0,\"step\":1}"
      },
      {
        "code": "sense_0",
        "dp_id": 101,
        "type": "Enum",
        "values": "{\"range\":[\"sense\"]}"
      },
      {
        "code": "sense_1",
        "dp_id": 102,
        "type": "Enum",
        "values": "{\"range\":[\"sense\"]}"
      },
      {
        "code": "sense_2",
        "dp_id": 103,
        "type": "Enum",
        "values": "{\"range\":[\"sense\"]}"
      },
...
      {
        "code": "sense_13",
        "dp_id": 114,
        "type": "Enum",
        "values": "{\"range\":[\"sense\"]}"
      }
    ]
  },
  "success": true,
  "t": 1684951183675,
  "tid": "c3273fc2fa5c11ed8adfb6b4e10a8863"
}

My mappings downloader in #356 turns that into:

{
    "xjnax8te3tdkoq9z": {
        "1": {
            "name": "switch_1",
            "type": "Boolean",
            "values": {}
        },
        "7": {
            "name": "countdown_1",
            "type": "Integer",
            "values": {
                "unit": "s",
                "min": 0,
                "max": 86400,
                "scale": 0,
                "step": 1
            }
        },
        "101": {
            "name": "sense_0",
            "type": "Enum",
            "values": {
                "range": [
                    "sense"
                ]
            }
        },
        "102": {
            "name": "sense_1",
            "type": "Enum",
            "values": {
                "range": [
                    "sense"
                ]
            }
        },
...
        "114": {
            "name": "sense_13",
            "type": "Enum",
            "values": {
                "range": [
                    "sense"
                ]
            }
        }
    }
}
mschlenstedt commented 1 year ago

Yes, I can confirm that I get same result as you with cloud.getdps(dev_id)`. This is great - never tried this!

Maybe you should use "code" instead of "name" in your mappings.json - as this is the standard. When your PR is included, I can change my functions to use the format of your mappings.json.

{
    "result": {
        "category": "rs",
        "functions": [
            {
                "code": "Power",
                "dp_id": 1,
                "type": "Boolean",
                "values": "{}"
            },
            {
                "code": "temp_set",
                "dp_id": 2,
                "type": "Integer",
                "values": "{\"unit\":\"\u2103\",\"min\":8,\"max\":40,\"scale\":0,\"step\":1}"
            },
            {
                "code": "mode",
                "dp_id": 4,
                "type": "Enum",
                "values": "{\"range\":[\"auto\",\"cold\",\"hot\"]}"
            }
        ],
        "lang_config": {},
        "status": [
            {
                "code": "Power",
                "dp_id": 1,
                "type": "Boolean",
                "values": "{}"
            },
            {
                "code": "temp_set",
                "dp_id": 2,
                "type": "Integer",
                "values": "{\"unit\":\"\u2103\",\"min\":8,\"max\":40,\"scale\":0,\"step\":1}"
            },
            {
                "code": "temp_current",
                "dp_id": 3,
                "type": "Integer",
                "values": "{\"unit\":\"\u2103\",\"min\":-10,\"max\":100,\"scale\":0,\"step\":1}"
            },
            {
                "code": "mode",
                "dp_id": 4,
                "type": "Enum",
                "values": "{\"range\":[\"auto\",\"cold\",\"hot\"]}"
            },
            {
                "code": "fault",
                "dp_id": 9,
                "type": "Bitmap",
                "values": "{\"label\":[\"P1\",\"P2\",\"P3\",\"P4\",\"P6\",\"P7\",\"P8\",\"P9\",\"PL\",\"PC\",\"E3\",\"E4\",\"E8\",\"CS\",\"E6\"],\"maxlen\":15}"
            }
        ]
    },
    "success": true,
    "t": 1684958124149,
    "tid": "ec01691cfa6c11edbfbfa24bf1d21c99"
}
mschlenstedt commented 1 year ago

I now changed over to the DPS mapping from Tuya Cloud now. The server now uses the mappings found in devices.json (if any). It's shown in the WebUI and also used in the set API call, e. g.

/set/pool%20heat%20pump/temp_set/26

is possible now.

jasonacox commented 1 year ago

This is great @mschlenstedt !! Love this...

image image
jasonacox commented 1 year ago

Thanks @mschlenstedt !! This is a great enhancement to the server!

mschlenstedt commented 1 year ago

Thanks for removing the obsolete stuff ;-)