I've been looking at these Tuya Smart Zigbee Ultrasonic Water Meters for quite a long time, but as they are not so cheap I have not pulled the trigger yet ...
Does anyone have any experience with these or similar devices?
I've been looking at these Tuya Smart Zigbee Ultrasonic Water Meters for quite a long time, but as they are not so cheap I have not pulled the trigger yet ...
Does anyone have any experience with these or similar devices?
No experience, but the linked sensor's battery power with judicious reporting, and reporting when there is an alarm situation makes so much more sense than the tank level sensor with real time monitoring and a wall wart.
I have one but I cannot get it to work well with any of the currently available Tuya Zigbee drivers. It just gives partial info. The device has a simple union jack style LCD display with a particularly cryptic menu and the instructions make as little sense as any other Tuya product. The way the instructions said to put it into pairing mode did not work so I asked the manufacturer and they sent me a video demonstrating and it was a process that has no logic I could discern and I don't think could possibly be guessed.
Also I purchased it mainly to monitor a drip irrigation system and check the flow to know if there are leaks, etc., but after using the device and chatting with the manufacturer I get the impression that these are meant more for acting as a water meter for a water company billing customers for usage or something.
Still figuring it out though. I would be glad if others started trying them do there could be more info and maybe driver development.
Maybe this is the intended use of these devices, so they are not very suitable for real-time water flow monitoring...
Can you post your device Zigbee model/manufacturer, as shown at the bottom of HE device web page, Data section?
It's not zigbee, but Ecowitt has a sensor.. It needs a gateway, (or maybe a weather console?), but there is an Ecowitt integration. @sburke781 would know if this sensor works in the integration.
It doesn't at the moment and might need a bit more work than the average sensor.... Not sure....
Ecowitt seems to be branching out a bit of late, be it for better or for worse. This sensor seems to be weather related...watering the plants depending on soil moisture measurement, etc. I have a soil moisture sensor, but don't have the Ecowitt integration installed at the moment. Is that sensor (soil moisture) incorporated in the app?
The soil moisture is included in the EcoWitt drivers, I have a few myself, but don't actively use them.
I agree, looks like EcoWitt are looking to expand. Makes sense to me to move into automated gardening tech.
Hi @kkossev, are you still interested in these ultrasonic water meters? I am currently only able to use them with the Tuya hub, but would very much like to be able to use them natively on my HE. I have a spare one if that would help...
I've been trying to get a driver working for this water meter. I have been able to get the device to report its status successfully (e.g. valveState, battery_voltage, water_consumed, etc). But I've been unable to write to the device to active the valve (open or close).
Any help would be much appreciated!
Here's the driver:
metadata {
definition(
name: "TS0601 Water Valve with DPs",
namespace: "custom",
author: "OpenAI"
) {
capability "Actuator"
capability "Sensor"
capability "Refresh"
capability "Configuration"
command "openValve"
command "closeValve"
attribute "valveState", "string"
attribute "water_consumed", "number"
attribute "month_consumption", "string" // hex blob
attribute "daily_consumption", "string" // hex blob
attribute "reverse_water_consumption", "number"
attribute "instantaneous_flow_rate", "number"
attribute "temperature", "number"
attribute "battery_voltage", "number"
attribute "report_period", "string"
attribute "warning", "number"
attribute "month_and_daily_frozen_set","number"
attribute "auto_clean", "string"
attribute "something", "string" // hex blob
attribute "meter_id", "string"
}
// ← your exact fingerprint — DO NOT CHANGE
fingerprint profileId: "0104", endpointId: "01",
inClusters: "0004,0005,EF00,0000",
outClusters: "0019,000A",
manufacturer: "_TZE200_vuwtqx0t",
model: "TS0601",
deviceJoinName: "TS0601 Water Valve & Meter",
controllerType:"ZGB"
preferences {
input name: "debugLogging", type: "bool", title: "Enable debug logging", defaultValue: true
}
}
import hubitat.device.HubAction
import hubitat.device.Protocol
def installed() {
initialize()
}
def updated() {
initialize()
}
def initialize() {
if (debugLogging) runIn(1800, logsOff)
refresh()
}
def configure() {
log.debug "configure(): sending ZDO bind for EF00 → endpoint 1"
// parse the hex NWK ID to an int
int nwk = Integer.parseInt(device.deviceNetworkId, 16)
// pull in the 64-bit EUI in hex form
String eui = device.getZigbeeId()
// build the exact zdo bind command
String bindCmd = String.format(
"zdo bind 0x%04X 1 1 0xEF00 { %s } {}",
nwk,
eui
)
log.debug "► ZDO Bind → ${bindCmd}"
// dispatch it
sendHubCommand(new hubitat.device.HubAction(bindCmd, hubitat.device.Protocol.ZIGBEE))
}
def parse(String description) {
log.debug "parse() raw: $description"
def descMap = zigbee.parseDescriptionAsMap(description)
if (descMap?.clusterInt != 0xEF00 || descMap.command != "02") return
List<Integer> buf = descMap.data.collect { Integer.parseInt(it,16) }
int pos = 0
int dpCount = buf[pos++] // number of DPs
int sequence = buf[pos++] // Tuya sequence #
log.debug "Tuya sequence #${sequence}, DP count: ${dpCount}"
for (int i = 0; i < dpCount; i++) {
// need at least id(1) + type(1) + lenHi(1) + lenLo(1)
if (pos + 4 > buf.size()) {
// if we're exactly at the end or only zero‐padding remains, bail quietly
break
}
int rawDp = buf[pos++] // DP ID with flags
int dpId = rawDp & 0x3F
int dpType = buf[pos++]
int lenHi = buf[pos++] & 0xFF
int lenLo = buf[pos++] & 0xFF
int dpLen = (lenHi << 8) | lenLo
if (dpLen < 0 || pos + dpLen > buf.size()) {
log.warn "DP#${dpId} len=${dpLen} exceeds buffer, skipping"
break
}
List<Integer> rawBytes = buf.subList(pos, pos + dpLen)
pos += dpLen
log.debug "DP#${dpId} type=${dpType} len=${dpLen} data=${rawBytes}"
switch (dpId) {
case 1:
sendEvent(name:"water_consumed",
value: rawToInt(rawBytes))
break
case 2:
sendEvent(name:"month_consumption",
value: rawToHex(rawBytes))
break
case 3:
sendEvent(name:"daily_consumption",
value: rawToHex(rawBytes))
break
case 6:
sendEvent(name:"month_and_daily_frozen_set",
value: rawToInt(rawBytes))
break
case 4:
def lookup = ["1h","2h","3h","4h","6h","8h","12h","24h"]
int idx = rawToInt(rawBytes)
sendEvent(name:"report_period",
value:(idx in 0..<lookup.size()) ? lookup[idx] : "unknown")
break
case 5:
sendEvent(name:"warning",
value: rawToInt(rawBytes))
break
case 13:
sendEvent(name:"valveState",
value: rawToInt(rawBytes)==1 ? "open" : "closed")
break
case 14:
sendEvent(name:"auto_clean",
value: rawToInt(rawBytes)==1 ? "on" : "off")
break
case 15:
// treat as hex blob
sendEvent(name:"something",
value: rawToHex(rawBytes))
break
case 16:
sendEvent(name:"meter_id",
value: rawBytes ? bytesToString(rawBytes) : "")
break
case 18:
sendEvent(name:"reverse_water_consumption",
value: rawToInt(rawBytes))
break
case 21:
sendEvent(name:"instantaneous_flow_rate",
value: rawToInt(rawBytes))
break
case 22:
sendEvent(name:"temperature",
value: rawToInt(rawBytes)/100.0)
break
case 26:
sendEvent(name:"battery_voltage",
value: rawToInt(rawBytes)/100.0)
break
default:
log.warn "Unhandled DP ${dpId} (type=${dpType})"
}
}
}
// simple state‐backed sequence counter
def tuyaSeq() { state.tuyaSeq = (state.tuyaSeq ?: 0 + 1) % 256; state.tuyaSeq }
def rawToInt(List<Integer> bytes) {
if (!bytes) return 0
def hex = bytes.collect{ String.format('%02X', it) }.join()
return Integer.parseInt(hex,16)
}
def rawToHex(List<Integer> bytes) {
return bytes.collect{ String.format('%02X', it) }.join()
}
def bytesToString(List<Integer> bytes) {
return new String(bytes.collect{ it as byte } as byte[], "UTF-8")
}
def openValve() {
log.info "Sending open command (DP13=1)"
return sendTuyaCommand(13, 1)
}
def closeValve() {
log.info "Sending close command (DP13=0)"
return sendTuyaCommand(13, 0)
}
import hubitat.device.HubAction
import hubitat.device.Protocol
// simple state-backed 0–255 rolling sequence
private int nextSeq() {
state.tuyaSeq = ((state.tuyaSeq ?: 0) + 1) & 0xFF
state.tuyaSeq
}
private sendTuyaCommand(int dpId, int val) {
int seq = nextSeq()
String dpBlock = String.format(
"%02X%02X%02X%02X%02X%02X%02X",
0x01, // # of DPs
seq, // sequence #
dpId, // DP ID (0x0D)
0x01, // type = 1
0x00, // lenHi
0x01, // lenLo
val & 0xFF // the 1-byte value
)
String cmd = "he cmd 0x${device.deviceNetworkId} 0x01 0xEF00 0x01 {${dpBlock}}"
log.debug "► HubAction cmd: ${cmd}"
sendHubCommand(new HubAction(cmd, Protocol.ZIGBEE))
}
def refresh() { log.debug "refresh(): push-only Tuya, no-op" }
And here's the message log:
dev:1942025-04-30 02:39:02.506debug► HubAction cmd: he cmd 0x5C13 0x01 0xEF00 0x01 {01050D01000101}
dev:1942025-04-30 02:39:02.504infoSending open command (DP13=1)
dev:1942025-04-30 02:38:56.495debugparse() raw: catchall: 0104 EF00 01 01 0040 00 5C13 00 00 0000 0B 01 0100
dev:1942025-04-30 02:38:55.935debug► HubAction cmd: he cmd 0x5C13 0x01 0xEF00 0x01 {01040D01000100}
dev:1942025-04-30 02:38:55.933infoSending close command (DP13=0)
dev:1942025-04-30 02:38:50.725debug► ZDO Bind → zdo bind 0x5C13 1 1 0xEF00 { A4C13880EC95B7F8 } {}
dev:1942025-04-30 02:38:50.723debugconfigure(): sending ZDO bind for EF00 → endpoint 1
dev:1942025-04-30 02:38:26.267debugDP#26 type=2 len=4 data=[0, 0, 1, 116]
dev:1942025-04-30 02:38:26.266debugTuya sequence #3, DP count: 42
dev:1942025-04-30 02:38:26.264debugparse() raw: catchall: 0104 EF00 01 01 0040 00 5C13 01 00 0000 02 01 2A031A02000400000174
dev:1942025-04-30 02:38:26.093debugDP#22 type=2 len=4 data=[0, 0, 7, 18]
dev:1942025-04-30 02:38:26.092debugTuya sequence #2, DP count: 42
dev:1942025-04-30 02:38:26.090debugparse() raw: catchall: 0104 EF00 01 01 0040 00 5C13 01 00 0000 02 01 2A021602000400000712
dev:1942025-04-30 02:38:25.919debugDP#21 type=0 len=4 data=[0, 0, 0, 0]
dev:1942025-04-30 02:38:25.918debugTuya sequence #1, DP count: 42
dev:1942025-04-30 02:38:25.916debugparse() raw: catchall: 0104 EF00 01 01 0040 00 5C13 01 00 0000 02 01 2A011500000400000000
dev:1942025-04-30 02:38:25.748debugDP#18 type=0 len=4 data=[0, 0, 0, 0]
dev:1942025-04-30 02:38:25.747debugTuya sequence #0, DP count: 42
dev:1942025-04-30 02:38:25.745debugparse() raw: catchall: 0104 EF00 01 01 0040 00 5C13 01 00 0000 02 01 2A001200000400000000
dev:1942025-04-30 02:38:25.579debugDP#16 type=3 len=14 data=[48, 48, 48, 48, 48, 48, 50, 52, 48, 49, 53, 55, 57, 56]
dev:1942025-04-30 02:38:25.577debugTuya sequence #255, DP count: 41
dev:1942025-04-30 02:38:25.575debugparse() raw: catchall: 0104 EF00 01 01 0040 00 5C13 01 00 0000 02 01 29FF1003000E3030303030303234303135373938
dev:1942025-04-30 02:38:25.436debugDP#15 type=0 len=16 data=[1, 0, 1, 44, 3, 0, 0, 0, 8, 0, 0, 10, 9, 0, 0, 200]
dev:1942025-04-30 02:38:25.389debugTuya sequence #254, DP count: 41
dev:1942025-04-30 02:38:25.386debugparse() raw: catchall: 0104 EF00 01 01 0040 00 5C13 01 00 0000 02 01 29FE0F0000100100012C030000000800000A090000C8
dev:1942025-04-30 02:38:25.182debugDP#14 type=1 len=1 data=[0]
dev:1942025-04-30 02:38:25.181debugTuya sequence #253, DP count: 41
dev:1942025-04-30 02:38:25.179debugparse() raw: catchall: 0104 EF00 01 01 0040 00 5C13 01 00 0000 02 01 29FD0E01000100
dev:1942025-04-30 02:38:25.017debugDP#13 type=1 len=1 data=[1]
dev:1942025-04-30 02:38:25.016debugTuya sequence #252, DP count: 41
dev:1942025-04-30 02:38:25.014debugparse() raw: catchall: 0104 EF00 01 01 0040 00 5C13 01 00 0000 02 01 29FC0D01000101
dev:1942025-04-30 02:38:24.851debugDP#6 type=0 len=2 data=[1, 0]
dev:1942025-04-30 02:38:24.849debugTuya sequence #251, DP count: 41
dev:1942025-04-30 02:38:24.848debugparse() raw: catchall: 0104 EF00 01 01 0040 00 5C13 01 00 0000 02 01 29FB060000020100
dev:1942025-04-30 02:38:24.684debugDP#5 type=5 len=2 data=[24, 0]
dev:1942025-04-30 02:38:24.683debugTuya sequence #250, DP count: 41
dev:1942025-04-30 02:38:24.681debugparse() raw: catchall: 0104 EF00 01 01 0040 00 5C13 01 00 0000 02 01 29FA050500021800
dev:1942025-04-30 02:38:24.511debugDP#4 type=4 len=1 data=[5]
dev:1942025-04-30 02:38:24.509debugTuya sequence #249, DP count: 41
dev:1942025-04-30 02:38:24.508debugparse() raw: catchall: 0104 EF00 01 01 0040 00 5C13 01 00 0000 02 01 29F90404000105
dev:1942025-04-30 02:38:24.391debugDP#3 type=0 len=8 data=[4, 29, 4, 29, 0, 0, 0, 0]
dev:1942025-04-30 02:38:24.390debugTuya sequence #248, DP count: 41
dev:1942025-04-30 02:38:24.388debugparse() raw: catchall: 0104 EF00 01 01 0040 00 5C13 01 00 0000 02 01 29F803000008041D041D00000000
dev:1942025-04-30 02:38:24.210debugDP#2 type=0 len=8 data=[25, 4, 25, 4, 0, 0, 0, 0]
dev:1942025-04-30 02:38:24.209debugTuya sequence #247, DP count: 41
dev:1942025-04-30 02:38:24.205debugparse() raw: catchall: 0104 EF00 01 01 0040 00 5C13 01 00 0000 02 01 29F7020000081904190400000000
dev:1942025-04-30 02:38:24.028debugDP#1 type=2 len=4 data=[0, 0, 0, 0]
dev:1942025-04-30 02:38:24.027debugTuya sequence #246, DP count: 41
dev:1942025-04-30 02:38:24.023debugparse() raw: catchall: 0104 EF00 01 01 0040 00 5C13 01 00 0000 02 01 29F60102000400000000
dev:1942025-04-30 02:38:16.232debugparse() raw: read attr - raw: 5C1301000030DFFF4214993AA42F669A3AA42F12743BA42F65753BA42F12, dni: 5C13, endpoint: 01, cluster: 0000, size: 30, attrId: FFDF, encoding: 42, command: 0A, value: 14993AA42F669A3AA42F12743BA42F65753BA42F12
dev:1942025-04-30 02:38:15.659debugparse() raw: read attr - raw: 5C130100001801002048E2FF2038E4FF2000, dni: 5C13, endpoint: 01, cluster: 0000, size: 18, attrId: 0001, encoding: 20, command: 0A, value: 48E2FF2038E4FF2000
dev:1942025-04-30 02:38:15.532debugparse() raw: catchall: 0104 EF00 01 01 0040 00 5C13 01 00 0000 11 01 055E40
dev:1942025-04-30 02:38:08.705debugparse() raw: catchall: 0000 0013 00 00 0040 00 5C13 00 00 0000 00 00 51135CF8B795EC8038C1A480
dev:1942025-04-30 02:38:08.245debug► ZDO Bind → zdo bind 0x5C13 1 1 0xEF00 { A4C13880EC95B7F8 } {}
dev:1942025-04-30 02:38:08.242debugconfigure(): sending ZDO bind for EF00 → endpoint 1
Got this working well enough for my needs. Here's the code:
metadata {
definition(
name: "Tuya Water Meter & Valve",
namespace: "mryaflle",
author: "Ed Granville"
) {
capability "Actuator"
capability "Battery"
capability "Configuration"
capability "Refresh"
capability "Sensor"
capability "TemperatureMeasurement"
capability "Valve"
capability "LiquidFlowRate"
command "open"
command "close"
command "autoClean"
command "setReportPeriod", [
[
name: "period*",
type: "ENUM",
description: "Reporting interval",
constraints: ["1h","2h","3h","4h","6h","8h","12h","24h"]
]
]
attribute "valve", "string" // ENUM ["open", "closed"]
attribute "totalWaterConsumed", "number" // NUMBER, unit:m³, but DP returns m³ * 1000
attribute "consumptionMonthly", "string"
attribute "consumptionDaily", "string"
attribute "totalWaterConsumed-reverseFlow", "number"
attribute "rate", "number" // NUMBER, unit:LPM
attribute "temperature", "number" // NUMBER, unit:°C
attribute "batteryVoltage", "number" // NUMBER, unit:V
attribute "battery", "number" // NUMBER, unit:%
attribute "reportingPeriod", "string"
attribute "warning", "number"
attribute "monthAndDailyFrozenSet", "number"
attribute "valveAutoClean", "string"
attribute "something", "string"
attribute "meterId", "string"
}
fingerprint profileId: "0104", endpointId: "01",
inClusters: "0004,0005,EF00,0000",
outClusters: "0019,000A",
manufacturer: "_TZE200_vuwtqx0t",
model: "TS0601",
deviceJoinName: "TS0601 Water Valve & Meter",
controllerType:"ZGB"
preferences {
input name: "debugLogging", type: "bool", title: "Enable debug logging", defaultValue: true
}
}
import hubitat.device.HubAction
import hubitat.device.Protocol
def installed() { initialize() }
def updated() { initialize() }
def initialize() {
if (debugLogging) runIn(1800, logsOff)
refresh()
}
def logsOff() {
log.warn "Disabling debug logging"
device.updateSetting("debugLogging",[value:"false",type:"bool"])
}
def configure() {
if (debugLogging) log.debug "configure(): sending ZDO bind for EF00 → endpoint 1"
int nwk = Integer.parseInt(device.deviceNetworkId, 16)
String eui = device.getZigbeeId()
String bindCmd = String.format(
"zdo bind 0x%04X 1 1 0xEF00 { %s } {}",
nwk, eui
)
if (debugLogging) log.debug "► ZDO Bind → ${bindCmd}"
sendHubCommand(new HubAction(bindCmd, Protocol.ZIGBEE))
}
def parse(String description) {
if (debugLogging) log.debug "parse() raw: ${description}"
def descMap = zigbee.parseDescriptionAsMap(description)
if (debugLogging) log.debug "parse(): clusterInt=${descMap.clusterInt}, command=${descMap.command}"
if (descMap?.clusterInt != 0xEF00) return
// Tuya write-ack (dataResponse) → update UI
if (descMap.command == "01") {
handleTuyaAck(descMap)
return
}
// Data report
if (descMap.command != "02" || !descMap.data) return
List<Integer> buf = descMap.data.collect { Integer.parseInt(it,16) }
int pos = 0
int dpCount = buf[pos++]
int sequence = buf[pos++]
if (debugLogging) log.debug "Tuya sequence #${sequence}, DP count: ${dpCount}"
for (int i = 0; i < dpCount; i++) {
if (pos + 4 > buf.size()) break
int rawDp = buf[pos++]
int dpId = rawDp & 0x3F
int dpType = buf[pos++]
int lenHi = buf[pos++] & 0xFF
int lenLo = buf[pos++] & 0xFF
int dpLen = (lenHi << 8) | lenLo
if (dpLen < 0 || pos + dpLen > buf.size()) {
log.warn "DP#${dpId} len=${dpLen} exceeds buffer, skipping"
break
}
List<Integer> rawBytes = buf.subList(pos, pos + dpLen)
pos += dpLen
if (debugLogging) log.debug "DP#${dpId} type=${dpType} len=${dpLen} data=${rawBytes}"
switch (dpId) {
case 1: sendEvent(name:"totalWaterConsumed", value: rawToInt(rawBytes)/1000.0, unit: "m³"); break
case 2: sendEvent(name:"consumptionMonthly", value: rawToHex(rawBytes)); break
case 3: sendEvent(name:"consumptionDaily", value: rawToHex(rawBytes)); break
case 6: sendEvent(name:"monthAndDailyFrozenSet", value: rawToInt(rawBytes)); break
case 4:
def lookup = ["1h","2h","3h","4h","6h","8h","12h","24h"]
int idx = rawToInt(rawBytes)
sendEvent(name:"reportingPeriod",
value:(idx in 0..<lookup.size()) ? lookup[idx] : "unknown")
break
case 5: sendEvent(name:"warning", value: rawToInt(rawBytes)); break
case 13: sendEvent(name:"valve", value: rawToInt(rawBytes)==1 ? "open" : "closed"); break
case 14: sendEvent(name:"valveAutoClean", value: rawToInt(rawBytes)==1 ? "on" : "off"); break
case 15: sendEvent(name:"something", value: rawToHex(rawBytes)); break
case 16: sendEvent(name:"meterId", value: rawBytes ? bytesToString(rawBytes) : ""); break
case 18:
int rawInt = rawToInt(rawBytes) == 0 ? 0 : rawToInt(rawBytes)/1000.0
sendEvent(name:"totalWaterConsumed-reverseFlow", value: rawInt, unit: "m³")
break
case 21:
int rawInt = rawToInt(rawBytes) == 0 ? 0 : rawToInt(rawBytes)/0.06
sendEvent(name:"rate", value: rawInt, unit: "LPM")
break
case 22: sendEvent(name:"temperature", value: rawToInt(rawBytes)/100.0, unit: "°C"); break
case 26:
if (!rawBytes) {
log.warn "DP26: empty payload, skipping"
break
}
int rawInt = rawToInt(rawBytes) // e.g. 372
double volts = rawInt / 100.0 // 3.72V
double minV = 2.5, maxV = 3.7
double clamped = volts < minV ? minV : (volts > maxV ? maxV : volts)
int pct = (int)Math.round((clamped - minV) / (maxV - minV) * 100)
sendEvent(name: "batteryVoltage", value: volts, unit: "V")
sendEvent(name: "battery", value: pct, unit: "%")
break
default: log.warn "Unhandled DP ${dpId} (type=${dpType})"
}
}
}
private void handleTuyaAck(Map descMap) {
if (debugLogging) log.debug "handleTuyaAck(): raw data = ${descMap.data}"
if (!descMap.data || descMap.data.size() < 7) {
if (debugLogging) log.warn "handleTuyaAck(): insufficient data, skipping"
return
}
// collect into ints
List<Integer> d = descMap.data.collect { Integer.parseInt(it, 16) }
int seq = d[1]
int rawDp = d[2]
int dpId = rawDp & 0x3F
int dpType = d[3]
// big-endian (hi byte, then lo byte)
int dpLen = ((d[4] & 0xFF) << 8) | (d[5] & 0xFF)
if (d.size() < 6 + dpLen) {
if (debugLogging) log.warn "handleTuyaAck(): incomplete payload (have ${d.size()}, need ${6+dpLen}), skipping"
return
}
List<Integer> raw = d.subList(6, 6 + dpLen)
int value = rawToInt(raw)
if (debugLogging) log.debug "handleTuyaAck(): seq=${seq}, dp=${dpId}, dpType=${dpType}, dpLen=${dpLen}, value=${value}"
switch(dpId) {
case 13:
sendEvent(name:"valve", value: value == 1 ? "open" : "closed")
break
case 14:
// Cancel any previously scheduled off
unschedule("updateAutoCleanStateOff")
// immediately show “on”…
sendEvent(name: "valveAutoClean", value: "on")
// …and then schedule turning it back off in 30 seconds
runIn(20, "updateAutoCleanStateOff")
break
case 4:
def lookup = ["1h","2h","3h","4h","6h","8h","12h","24h"]
if (value < 0 || value >= lookup.size()) {
log.warn "handleTuyaAck(): unknown reportingPeriod index ${value}"
return
}
def period = lookup[value]
sendEvent(name: "reportingPeriod", value: period)
break
default:
if (debugLogging) log.debug "handleTuyaAck(): unhandled DP ${dpId}"
}
}
private Integer rawToInt(List<Integer> bytes) {
if (!bytes) return 0
String hex = bytes.collect{ String.format('%02X', it) }.join()
return Integer.parseInt(hex,16)
}
private String rawToHex(List<Integer> bytes) {
bytes.collect{ String.format('%02X', it) }.join()
}
private String bytesToString(List<Integer> bytes) {
new String(bytes.collect{ it as byte } as byte[], "UTF-8")
}
private sendTuyaCommand(int dpId, int val) {
String seq = zigbee.convertToHexString(new Random().nextInt(256), 2)
String dp = zigbee.convertToHexString(dpId & 0x3F, 2) + "01"
String fn = "0001"
String data = zigbee.convertToHexString(val & 0xFF, 2)
String payload = "00" + seq + dp + fn + data
if (debugLogging) log.debug "► Sending SETDATA → EF00 cmd=00, payload=${payload}"
zigbee.command(0xEF00, 0x00, payload)
}
def open() {
log.info "open() → DP13=1"
def cmd = sendTuyaCommand(13, 1)
// sendEvent(name:"valve", value:"open")
return cmd
}
def close() {
log.info "close() → DP13=0"
def cmd = sendTuyaCommand(13, 0)
// sendEvent(name:"valve", value:"closed")
return cmd
}
def autoClean() {
log.info "autoClean() → DP14=1"
def cmd = sendTuyaCommand(14, 1)
// sendEvent(name:"valveAutoClean", value:"on")
return cmd
}
def setReportPeriod(String period) {
def lookup = ["1h","2h","3h","4h","6h","8h","12h","24h"]
int idx = lookup.indexOf(period)
if (idx < 0) {
log.warn "setReportPeriod(): unknown period '${period}'"
return
}
log.info "setReportPeriod() → DP4='${period}' (idx ${idx})"
def cmd = sendTuyaCommand(4, idx)
// sendEvent(name:"reportingPeriod", value:period)
return cmd
}
def updateAutoCleanStateOff() {
if (debugLogging) log.debug "updateAutoCleanStateOff(): clearing temporary valveAutoClean flag"
sendEvent(name: "valveAutoClean", value: "off")
}
def refresh() {
log.debug "refresh(): push-only Tuya, no-op"
}