ZoneTouch 3 by Polyaire

What firmware version is your unit?

I’m on:
Console Version: 1.0.0.6
Main Module Version: 2.0.0.2

What does the App send? Maybe we should use that instead globally?

I have exactly the same version numbers as you. The app sends:

55 55 55 aa 80 b0 11 c0 00 0c 20 00 00 00 00 04 00 01 03 80 64 00 f8 e2

Which if you account for the different ID is exactly the same as what we’ve got. Our assembler functions (unless your message structure has changed since I last used it) are based on what the app does, not what the ZT protocol specification notes as correct because it doesn’t work that way.

Interesting… I went via the spec PDF…

Mine is

55 55 55 aa 80 b0 12 c0 0a 0c 20 00 00 00 00 04 00 01 03 80 64 00 9a 02

I put in a call to Polyaire - trying to see if they have an updated API or similar - and what the story is…

If you look at the 9th byte 0a this is supposed to be 00 in the spec:
image
All the examples you gave me to test also had this value as 00.

Not sure if Polyaire will be much help as this isn’t exactly a flagship product of theirs and they’re already done the API spec. but certainly worth a try.

Ok, this is strange… I changed from:

55 55 55 aa 80 b0 12 c0 00 0c 20 00 00 00 00 04 00 01 03 80 64 00 9a 02

to:

55 55 55 aa 80 b0 11 c0 00 0c 20 00 00 00 00 04 00 01 03 80 64 00 f8 e2

Now it does adjust to 100% on that zone - ie zone 0x03 and value 0x64.

The docs state:

Message id (1 byte):
When sending message to ZoneTouch, message id can be any data. The response message should have the same message id.

So - in theory - it shouldn’t even matter what this byte is…

One of their examples:

55 55 55 aa 80 b0 01 c0 00 0c 20 00 00 00 00 01 00 04 01 02 00 00 64 fd

So they have the message id at 0x01. Does the message ID change at all from the app?

OH! - I have a feeling I might have typo’ed this - as its certainly 0x00 in the code! That’s the problem with staring at hex strings too long hahahah

I don’t know if the ID changes from the app but presume it must (from time to time).

I’ve had no end of trouble using 0x01 as the message ID, it simply doesn’t like it. Does your unit have the same behaviour?

“In theory, theory and practice are the same. In practice, they are not [unfortunately]” :upside_down_face:

Honestly, I haven’t tried. I’m certainly going to head through and test all the functionality of it using the id that the app uses - just to head off different weirdness…

However, now we get into the interesting bits that aren’t documented :slight_smile:

I notice that the ZT3 app has a temperature display - which looks to be obtained from the control panel itself. Have you managed to find that in the dumps from the app?

I honestly haven’t looked at the temperature part of the app, I’m not sure how I’ll figure out what bits make it up but I’ll take a look

This is partly why I was going to ask them for an updated API spec - if it exists :smiley:

I’m going to take a wild guess that if you’re getting the extended info, its likely a field in there. Given the rest of the protocol, I’d say its probably a simple one byte that’s just a decimal - like the zone percentage.

If you have the ability, I’d take a couple of dumps from the app - then increase the temperature by a degree or two, take a couple more dumps and compare. For confirmation, increasing the temperature again and more dumps might make it clear.

That being said, how are you dumping the traffic between your phone and the ZT3?

I tried to figure that part out myself, but I have a multi-access point setup - so it depends what has roamed where as to if I get anything or not…

I am using PCAPdroid to capture the packets. Depending on your router you can likely have it do packet captures for you if traffic needs to go through said routing, i.e. devices on different subnets.

@generically_named So another test - if you set the id to 0x11, can you no longer set zone 0x02 to 100%? :slight_smile:

I’m starting to see a lot of weird quirks the more I dig… I’m wondering if the id is actually related to something, or the latest firmware is buggy :smiley:

Annoyingly, the app seems to work fine - so they must be working around it somehow…

I suspect that we’re using too high of an ID. If I set my ID to 0x07 I am able to set percentage reliably I believe for every zone. It doesn’t seem to matter for my app because I’ve seen it using IDs > 0x07 but I think this fixes our issue, at least for my 7 zone system.

It would not surprise me if there is a bug in the software, a high value for ID could be causing issues for it or something, I’m not sure.

Interesting. I’m battling some nodered / home assistant weirdness atm too…

I’m trying to get a function to delete the auto-created entities to clean up, and also get attributes to apply correctly - which is either a bug or user error as well :smiley:

I’m quite happy with the progress at the moment though - and its just fleshing things out to be more complete - then I’ll be happy to get more feedback from folks with systems so we can find more bugs :smiley:

EDIT: Yet when I use an ID of 0x07, I can’t set zone 0x03 to 100% again… If I set it to 0xff, then I can’t set some zones to 50% either! :slight_smile:

EDIT2: Setting it at 0x00, and I can’t set zone 0x03 to 50% either… There’s certainly some weirdness…

I’ve been working on a proper custom component for this now we’ve nailed down the process. I have it able to control zones at the moment but dealing with entity updating is proving challenging as I don’t have any experience with HA integration development.

There is certainly some weirdness going on but unfortunately this kind of computing mathematics is also a bit outside my wheel house. I may just bodge a solution like I proposed above

Right now, I’m playing with:

    // Generate a random id value
    const id = Math.floor(Math.random() * 255);

    var Header = [0x55, 0x55, 0x55, 0xAA]; //This is standard, do not modify
    var SetZone = [
        0x80, 0xB0,       // Address
        id,               // ID (was 0x12)
        0xC0,             // Type
        0x00, 0x0C,       // Length
        0x20, 0x00,       // Sub Type
        0x00, 0x00,       // Common Data Length
        0x00, 0x04,       // Repeat Data Count
        0x00, 0x01,       // Repeat Data Length
        msg.zone,         // Zone (starts at 0x00)
        states[state],    // State - 0x03 = on, 0x02 = off, 0x80 = percentage
        msg.value,
        0x00
    ];

Purely with the random thought that maybe it doesn’t like duplicated ID numbers… Or that its just random luck if we happen to get an ID / zone / percentage combo that doesn’t work together…

I have no idea if it’ll help or not - but that’s why we experiment :smiley:

I think I’m pretty happy with this flow:

[{"id":"e8d1cb207f57eaf8","type":"tab","label":"ZoneTouch 3","disabled":false,"info":"","env":[]},{"id":"26205ec1423be98e","type":"websocket in","z":"e8d1cb207f57eaf8","name":"WebSocket In","server":"","client":"06e5bb654365913d","x":90,"y":100,"wires":[["329a59e8437373bc"]]},{"id":"f10acb738a5b6496","type":"websocket out","z":"e8d1cb207f57eaf8","name":"","server":"","client":"06e5bb654365913d","x":230,"y":420,"wires":[]},{"id":"899fe2e93679dd5a","type":"switch","z":"e8d1cb207f57eaf8","name":"","property":"payload.type","propertyType":"msg","rules":[{"t":"eq","v":"auth_required","vt":"str"},{"t":"eq","v":"event","vt":"str"},{"t":"eq","v":"result","vt":"str"},{"t":"else"}],"checkall":"true","repair":false,"outputs":4,"x":370,"y":100,"wires":[["d3cd01a673e31966"],["526afd2893a4b343"],[],[]]},{"id":"329a59e8437373bc","type":"json","z":"e8d1cb207f57eaf8","name":"","property":"payload","action":"obj","pretty":false,"x":250,"y":100,"wires":[["899fe2e93679dd5a"]]},{"id":"d3cd01a673e31966","type":"change","z":"e8d1cb207f57eaf8","name":"Add Access Token Here","rules":[{"t":"delete","p":"_session","pt":"msg"},{"t":"set","p":"access_token","pt":"msg","to":"CHANGEME","tot":"str"},{"t":"set","p":"payload","pt":"msg","to":"{\"type\": \"auth\", \"access_token\": access_token}","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":590,"y":80,"wires":[["fa75f5242683a341"]]},{"id":"fa75f5242683a341","type":"json","z":"e8d1cb207f57eaf8","name":"","property":"payload","action":"str","pretty":false,"x":770,"y":80,"wires":[["d18adb16ca69fa20"]]},{"id":"9b16dab46faa2008","type":"function","z":"e8d1cb207f57eaf8","name":"create discovery","func":"const i = msg.zone;\nconst messageId = (flow.get(\"messageId\") ?? 0) + 1;\n\n// create messageId to zoneId table\n// table is for when a message is recieved about value change\n// we know what zone it is with\nconst lookup = flow.get(\"lookup\") ?? {};\nconst alreadyRegisteredZones = Object.values(lookup);\n\n// default payload \nmsg.payload =  { \n    id: messageId,\n    type: \"nodered/entity\",\n    server_id: `zonetouch3_zone_${i}`,\n    node_id: `zonetouch3_zone_${i}`,\n    state: msg.percent,\n    attributes: {\n        name: `zonetouch3_zone_${i}`,\n        min: 0,\n        max: 100,\n        step: 5,\n        unit_of_measurement: \"%\",\n        mode: \"slider\",\n        icon: \"mdi:hvac\",\n    }\n};\n\n// if messageId already in lookup no need to resend discovery just update value\nif(!alreadyRegisteredZones.includes(i)) {\n    lookup[messageId] = i;\n\n    msg.payload = {\n        ...msg.payload,\n        type: \"nodered/discovery\",\n        component: \"number\",\n        attributes: {\n            name: `zonetouch3_zone_${i}`,\n            min: 0,\n            max: 100,\n            step: 5,\n            unit_of_measurement: \"%\",\n            mode: \"slider\",\n            icon: \"mdi:hvac\",\n        }\n    };\n}\n\nflow.set(\"lookup\", lookup);\nflow.set(\"messageId\", messageId);\nnode.send(msg);","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":840,"y":300,"wires":[["3ee9dfb96e70b1a0"]]},{"id":"8a7856865551d6a4","type":"status","z":"e8d1cb207f57eaf8","name":"","scope":["f10acb738a5b6496"],"x":80,"y":600,"wires":[["9db6481343ed39a6"]]},{"id":"526afd2893a4b343","type":"function","z":"e8d1cb207f57eaf8","name":"Value changed from HA","func":"const messageId = msg.payload.id;\nconst value = msg.payload.event.value;\n\nconst lookup = flow.get(\"lookup\") ?? {};\nconst zoneId = lookup[messageId];\nif(zoneId === undefined) {\n    node.error(\"Message ID not found in lookup table\");\n    return null;\n}\n\nmsg.zone = zoneId;\nmsg.value = value;\ndelete msg._session;\n\n// We send two packets in the ON case. An ON and a percentage.\n// On the OFF case, we just send one.\nvar states = [];\n\nif (msg.value == 0) {\n    states.push(0x02);\n} else {\n    states.push(0x03);\n    states.push(0x80);\n}\n\nfor (var state in states) {\n    // Generate a random id value\n    const id = Math.floor(Math.random() * 255);\n\n    var Header = [0x55, 0x55, 0x55, 0xAA]; //This is standard, do not modify\n    var SetZone = [\n        0x80, 0xB0,       // Address\n        id,             // ID (was 0x12)\n        0xC0,             // Type\n        0x00, 0x0C,       // Length\n        0x20, 0x00,       // Sub Type\n        0x00, 0x00,       // Common Data Length\n        0x00, 0x04,       // Repeat Data Count\n        0x00, 0x01,       // Repeat Data Length\n        msg.zone,         // Zone (starts at 0x00)\n        states[state],    // State - 0x03 = on, 0x02 = off, 0x80 = percentage\n        msg.value,\n        0x00\n    ];\n\n    // Work out the CRC\n    var decimal = crc16Modbus((SetZone));\n    var hexString = decimal.toString(16);\n    var fullMessage = Header.concat(SetZone);\n    var crcFirstByte = hexString.substring(0, 2);\n    var crcSecondByte = hexString.substring(2, 4);\n\n    fullMessage = fullMessage.concat(parseInt(crcFirstByte, 16)).concat(parseInt(crcSecondByte, 16));\n\n    var fullMessage = Header.concat(SetZone).concat(parseInt(crcFirstByte, 16)).concat(parseInt(crcSecondByte, 16));\n    var dataBuffer = Buffer.from(fullMessage);\n    msg.payload = dataBuffer;\n    node.send(msg);\n}\nreturn null;\n\nfunction crc16Modbus(dataHex) {\n    function calcCRC16(data, poly = 0xA001) {\n        let crc = 0xFFFF;\n        for (let i = 0; i < data.length; i++) {\n            crc ^= data[i];\n            for (let j = 0; j < 8; j++) {\n                if (crc & 0x0001) {\n                    crc = (crc >> 1) ^ poly;\n                } else {\n                    crc >>= 1;\n                }\n            }\n        }\n        return crc;\n    }\n\n    // Convert hex string to byte array\n    let dataBytes = Buffer.from(dataHex, 'hex');\n\n    // Calculate CRC16 checksum\n    let crc = calcCRC16(dataBytes);\n\n    //Return the hex value\n    return crc;\n};","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":590,"y":120,"wires":[["380120863fcfc79b"]]},{"id":"9db6481343ed39a6","type":"switch","z":"e8d1cb207f57eaf8","name":"","property":"status.event","propertyType":"msg","rules":[{"t":"eq","v":"connect","vt":"str"}],"checkall":"true","repair":false,"outputs":1,"x":230,"y":600,"wires":[["d14c2aba8e317cd0"]]},{"id":"d14c2aba8e317cd0","type":"change","z":"e8d1cb207f57eaf8","name":"reset message id and subscription lookup","rules":[{"t":"delete","p":"messageId","pt":"flow"},{"t":"delete","p":"lookup","pt":"flow"}],"action":"","property":"","from":"","to":"","reg":false,"x":480,"y":600,"wires":[[]]},{"id":"a7e0376d89163621","type":"tcp request","z":"e8d1cb207f57eaf8","name":"","server":"172.31.1.251","port":"7030","out":"sit","ret":"buffer","splitc":" ","newline":"","trim":false,"tls":"","x":320,"y":320,"wires":[["2a74605e38f0d1d3"]]},{"id":"6fc966c7341f947e","type":"function","z":"e8d1cb207f57eaf8","name":"Construct Queries","func":"// Get the zone data.\nconst Header = [0x55, 0x55, 0x55, 0xAA]; //This is standard, do not modify\nconst ZoneQuery = [\n    0x80, 0xB0,     // Address\n    0x01,           // ID (for some reason, 0x01 always works here)\n    0xC0,           // Type\n    0x00, 0x08,     // Length\n    0x21, 0x00,     // Sub-type\n    0x00, 0x00, 0x00, 0x00, 0x00, 0x00 // No other data.\n];\n\n// Work out the CRC\nvar decimal = crc16Modbus((ZoneQuery));\nvar hexString = decimal.toString(16);\nvar fullMessage = Header.concat(ZoneQuery);\nvar crcFirstByte = hexString.substring(0, 2);\nvar crcSecondByte = hexString.substring(2, 4);\nfullMessage = fullMessage.concat(parseInt(crcFirstByte, 16)).concat(parseInt(crcSecondByte, 16));\nvar fullMessage = Header.concat(ZoneQuery).concat(parseInt(crcFirstByte, 16)).concat(parseInt(crcSecondByte, 16));\nmsg.payload = Buffer.from(fullMessage);\nnode.send(msg);\n\n// Get the temperature data.\nconst TempQuery = [\n    0x80, 0xB0,     // Address\n    0x01,           // ID (for some reason, 0x01 always works here)\n    0xC0,           // Type\n    0x00, 0x08,     // Length\n    0x2b, 0x00,     // Sub-type\n    0x00, 0x00, 0x00, 0x00, 0x00, 0x00 // No other data.\n];\n// Work out the CRC\nvar decimal = crc16Modbus((TempQuery));\nvar hexString = decimal.toString(16);\nvar fullMessage = Header.concat(TempQuery);\nvar crcFirstByte = hexString.substring(0, 2);\nvar crcSecondByte = hexString.substring(2, 4);\nfullMessage = fullMessage.concat(parseInt(crcFirstByte, 16)).concat(parseInt(crcSecondByte, 16));\nvar fullMessage = Header.concat(TempQuery).concat(parseInt(crcFirstByte, 16)).concat(parseInt(crcSecondByte, 16));\nmsg.payload = Buffer.from(fullMessage);\nnode.send(msg);\n\nreturn null;\n\nfunction crc16Modbus(dataHex) {\n    function calcCRC16(data, poly = 0xA001) {\n        let crc = 0xFFFF;\n        for (let i = 0; i < data.length; i++) {\n            crc ^= data[i];\n            for (let j = 0; j < 8; j++) {\n                if (crc & 0x0001) {\n                    crc = (crc >> 1) ^ poly;\n                } else {\n                    crc >>= 1;\n                }\n            }\n        }\n        return crc;\n    }\n\n    // Convert hex string to byte array\n    let dataBytes = Buffer.from(dataHex, 'hex');\n\n    // Calculate CRC16 checksum\n    let crc = calcCRC16(dataBytes);\n\n    //Return the hex value\n    return crc;\n};","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":290,"y":220,"wires":[["60003d8c4f959fa2"]]},{"id":"ec9358d227636c16","type":"inject","z":"e8d1cb207f57eaf8","name":"Get Status","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"30","crontab":"","once":true,"onceDelay":"1","topic":"","payload":"","payloadType":"date","x":110,"y":220,"wires":[["6fc966c7341f947e"]]},{"id":"2a74605e38f0d1d3","type":"function","z":"e8d1cb207f57eaf8","name":"Parse Reply","func":"var NewMsg = {};\n\n// Only process Group Status responses.\nif ( msg.payload[10] == 0x21 && msg.payload.length == 60 ) {\n    msg.zone_count = msg.payload[17];\n\n    // Get the status for each zone.\n    // Bit 8-7 = State. 00 = Off, 01 = On, 11 = Turbo\n    // Bit 6-1 = Group Number\n    for (var i = 0; i < msg.zone_count; i++) {\n        var byte = 18 + (i * 8);\n\n        // Set number.zone_X to the retrieved value.\n        NewMsg.zone = i;\n        NewMsg.zone_status = msg.payload[byte] - i;\n        NewMsg.percent = msg.payload[byte + 1];\n\n        // zone_status == 0 means OFF\n        if ( NewMsg.zone_status == 0 ) {\n            NewMsg.percent = 0;\n        }\n        node.send(NewMsg);\n    }\n    return null;\n}\n\n// Only process temperature reports\nif ( msg.payload[18] == 0x9f && msg.payload.length == 24 ) {\n    // byte0     0x9f\n    // byte1     -                                                                                             \n    // byte2,3   Temperature = (data - 500)/10, -50.0~150.0 degree. Others temperature error.\n    var temp_value = ( ( (msg.payload[20] & 0xFF) << 8) | (msg.payload[21] & 0xFF) );\n    msg.temperature = (temp_value - 500) / 10;\n    node.send(msg);\n    return null;\n}\n\nreturn null;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":510,"y":320,"wires":[["12a1636e5773f7c4"]]},{"id":"9e5d406b701633c6","type":"inject","z":"e8d1cb207f57eaf8","name":"Delete HA Entities","props":[],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":130,"y":700,"wires":[["726cf4943fc9c028"]]},{"id":"726cf4943fc9c028","type":"ha-get-entities","z":"e8d1cb207f57eaf8","name":"","server":"adf8b17c.f6a7c","version":1,"rules":[{"property":"entity_id","logic":"starts_with","value":"number.zonetouch3_zone_","valueType":"str"}],"outputType":"split","outputEmptyResults":false,"outputLocationType":"msg","outputLocation":"payload","outputResultsCount":1,"x":330,"y":700,"wires":[["2df2274420584663"]]},{"id":"feb42266baf2ea80","type":"debug","z":"e8d1cb207f57eaf8","name":"debug 6","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":240,"y":760,"wires":[]},{"id":"2df2274420584663","type":"function","z":"e8d1cb207f57eaf8","name":"create remove","func":"const entity_id = msg.payload.entity_id;\nconst entity = msg.payload.entity_id.split(\".\")[1].replace(\"nodered_\", \"\");\n\n// default payload \nmsg.payload =  { \n    type: \"nodered/discovery\",\n    server_id: entity,\n    node_id: entity,\n    component: \"number\",\n    remove: true\n};\n\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":520,"y":700,"wires":[["f2157194d9403979"]]},{"id":"4cf60e0955916c37","type":"catch","z":"e8d1cb207f57eaf8","name":"","scope":null,"uncaught":false,"x":80,"y":760,"wires":[["feb42266baf2ea80"]]},{"id":"237283aa3101c2df","type":"comment","z":"e8d1cb207f57eaf8","name":"Send to HA WebSocket","info":"","x":120,"y":380,"wires":[]},{"id":"71e4f526aea3b09b","type":"link in","z":"e8d1cb207f57eaf8","name":"link in 7","links":["d18adb16ca69fa20","3ee9dfb96e70b1a0","f2157194d9403979"],"x":35,"y":420,"wires":[["f10acb738a5b6496"]]},{"id":"d18adb16ca69fa20","type":"link out","z":"e8d1cb207f57eaf8","name":"To WebSocket","mode":"link","links":["71e4f526aea3b09b"],"x":875,"y":80,"wires":[]},{"id":"3ee9dfb96e70b1a0","type":"link out","z":"e8d1cb207f57eaf8","name":"To WebSocket","mode":"link","links":["71e4f526aea3b09b"],"x":955,"y":300,"wires":[]},{"id":"7acd81d42c67cbae","type":"comment","z":"e8d1cb207f57eaf8","name":"Maintenance Only","info":"","x":110,"y":660,"wires":[]},{"id":"32e6a7eea6b7c96b","type":"comment","z":"e8d1cb207f57eaf8","name":"Timed Trigger for sync","info":"","x":120,"y":180,"wires":[]},{"id":"b59c226ed03b2f66","type":"comment","z":"e8d1cb207f57eaf8","name":"Data in from HA WebSocket","info":"","x":140,"y":60,"wires":[]},{"id":"f2157194d9403979","type":"link out","z":"e8d1cb207f57eaf8","name":"To WebSocket","mode":"link","links":["71e4f526aea3b09b"],"x":655,"y":700,"wires":[]},{"id":"fb7a844de702beb0","type":"comment","z":"e8d1cb207f57eaf8","name":"Access Token is Important!","info":"","x":590,"y":40,"wires":[]},{"id":"a99f0e23682e9772","type":"comment","z":"e8d1cb207f57eaf8","name":"Clean up on a fresh connect","info":"","x":140,"y":560,"wires":[]},{"id":"328aa6d6415285da","type":"delay","z":"e8d1cb207f57eaf8","name":"RateLimit","pauseType":"rate","timeout":"5","timeoutUnits":"seconds","rate":"1","nbRateUnits":"0.2","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"allowrate":false,"outputs":1,"x":140,"y":320,"wires":[["a7e0376d89163621"]]},{"id":"28678655df7ac589","type":"link in","z":"e8d1cb207f57eaf8","name":"link in 8","links":["380120863fcfc79b","60003d8c4f959fa2"],"x":35,"y":320,"wires":[["328aa6d6415285da"]]},{"id":"380120863fcfc79b","type":"link out","z":"e8d1cb207f57eaf8","name":"link out 4","mode":"link","links":["28678655df7ac589"],"x":735,"y":120,"wires":[]},{"id":"60003d8c4f959fa2","type":"link out","z":"e8d1cb207f57eaf8","name":"link out 5","mode":"link","links":["28678655df7ac589"],"x":415,"y":220,"wires":[]},{"id":"2ca8a26fac6f7f69","type":"comment","z":"e8d1cb207f57eaf8","name":"Send Packet to ZoneTouch 3","info":"","x":140,"y":280,"wires":[]},{"id":"5f6f044259bca432","type":"ha-sensor","z":"e8d1cb207f57eaf8","name":"zonetouch3_temperature","entityConfig":"4d43d43574ccd5b8","version":0,"state":"temperature","stateType":"msg","attributes":[],"inputOverride":"allow","outputProperties":[],"x":870,"y":340,"wires":[[]]},{"id":"12a1636e5773f7c4","type":"switch","z":"e8d1cb207f57eaf8","name":"","property":"temperature","propertyType":"msg","rules":[{"t":"istype","v":"undefined","vt":"undefined"},{"t":"else"}],"checkall":"true","repair":false,"outputs":2,"x":670,"y":320,"wires":[["9b16dab46faa2008"],["5f6f044259bca432"]]},{"id":"06e5bb654365913d","type":"websocket-client","path":"ws://localhost:8123/api/websocket","tls":"","wholemsg":"false","hb":"0","subprotocol":""},{"id":"adf8b17c.f6a7c","type":"server","name":"Home Assistant","addon":true,"rejectUnauthorizedCerts":true,"ha_boolean":"","connectionDelay":false,"cacheJson":true,"heartbeat":true,"heartbeatInterval":"30","areaSelector":"friendlyName","deviceSelector":"friendlyName","entitySelector":"friendlyName","statusSeparator":"","enableGlobalContextStore":true},{"id":"4d43d43574ccd5b8","type":"ha-entity-config","server":"adf8b17c.f6a7c","deviceConfig":"","name":"zonetouch3_temperature","version":"6","entityType":"sensor","haConfig":[{"property":"name","value":"zonetouch3_temperature"},{"property":"icon","value":"mdi:thermometer"},{"property":"entity_picture","value":""},{"property":"entity_category","value":""},{"property":"device_class","value":"temperature"},{"property":"unit_of_measurement","value":"°C"},{"property":"state_class","value":"measurement"}],"resend":false,"debugEnabled":false}]

It will create / correctly operate the zone touch zone sliders, and now query the temperature and update the sensor within HA every 30 seconds.

Reads seem accurate, and using the random ID value, it seems to be fine.

Remember to create an API token in HA, and put that in the Access Token section.

It should just work now…

Temperature sensor:
image