7 in 1 Water quality monitor

Just testing myself how to hide a text (or a picture) - the easy way is now gone .. :frowning:

What worked in the default mode with the preview is copying and pasting this text :

[details="click to expand"]
hidden text is here
[/details]


Testing again :slight_smile:

click to expand

hidden text is here second try


Make sure when copying and pasting the details snippet you are in a 'normal' edit mode,,with the preview pane on the right side :

click to see the screenshot


wow, too much complicated for me ... :frowning:

Now checked the logs again. Only temperatures and humidity seems update. All the other values are from the initial Configure.

Cheers

Petteri

Did you pair the device again with the newly assigned driver?

For many Tuya devices, it is necessary to pair them with the correct driver.

The frequently repeated rule to press the 'Configure' button after switching the drivers does not apply to the Tuya devices.

Yes. Removed device and the old driver. Did just again and only temps and humidity updates.

Cheers

Petteri

What happens if you click on the Refresh() button ?

Nothing

All those values show unchanged in debug log. Those values must change like. Tds = 0?

Cheers

Petteri

Sorry for the long log. Cannot get spoiler or details to work.

**
dev:1006
**2026-08-23 06:10:50.614

debug

(M) Vedenlaatumittari: sg unchanged (1.000)

**dev:1006**2026-08-23 06:10:50.612

debug

(M) Vedenlaatumittari: DP 132 type=2 len=4 raw=1000 bytes=000003E8

**dev:1006**2026-08-23 06:10:50.608

debug

(M) Vedenlaatumittari: descMap: [raw:catchall: 0104 EF00 01 01 0040 00 90E4 01 01 0001 02 01 033184020004000003E8, profileId:0104, clusterId:EF00, clusterInt:61184, sourceEndpoint:01, destinationEndpoint:01, options:0040, messageType:00, dni:90E4, isClusterSpecific:true, isManufacturerSpecific:true, manufacturerId:0001, command:02, direction:01, data:[03, 31, 84, 02, 00, 04, 00, 00, 03, E8]]

**dev:1006**2026-08-23 06:10:50.606

debug

(M) Vedenlaatumittari: parse: catchall: 0104 EF00 01 01 0040 00 90E4 01 01 0001 02 01 033184020004000003E8

**dev:1006**2026-08-23 06:10:50.416

debug

(M) Vedenlaatumittari: salinity unchanged (0.0)

**dev:1006**2026-08-23 06:10:50.414

debug

(M) Vedenlaatumittari: DP 131 type=2 len=4 raw=0 bytes=00000000

**dev:1006**2026-08-23 06:10:50.410

debug

(M) Vedenlaatumittari: descMap: [raw:catchall: 0104 EF00 01 01 0040 00 90E4 01 01 0001 02 01 03308302000400000000, profileId:0104, clusterId:EF00, clusterInt:61184, sourceEndpoint:01, destinationEndpoint:01, options:0040, messageType:00, dni:90E4, isClusterSpecific:true, isManufacturerSpecific:true, manufacturerId:0001, command:02, direction:01, data:[03, 30, 83, 02, 00, 04, 00, 00, 00, 00]]

**dev:1006**2026-08-23 06:10:50.408

debug

(M) Vedenlaatumittari: parse: catchall: 0104 EF00 01 01 0040 00 90E4 01 01 0001 02 01 03308302000400000000

**dev:1006**2026-08-23 06:10:50.196

debug

(M) Vedenlaatumittari: device setting ecV1 = 10000

**dev:1006**2026-08-23 06:10:50.194

debug

(M) Vedenlaatumittari: DP 129 type=2 len=4 raw=10000 bytes=00002710

**dev:1006**2026-08-23 06:10:50.190

debug

(M) Vedenlaatumittari: descMap: [raw:catchall: 0104 EF00 01 01 0040 00 90E4 01 01 0001 02 01 032F8102000400002710, profileId:0104, clusterId:EF00, clusterInt:61184, sourceEndpoint:01, destinationEndpoint:01, options:0040, messageType:00, dni:90E4, isClusterSpecific:true, isManufacturerSpecific:true, manufacturerId:0001, command:02, direction:01, data:[03, 2F, 81, 02, 00, 04, 00, 00, 27, 10]]

**dev:1006**2026-08-23 06:10:50.188

debug

(M) Vedenlaatumittari: parse: catchall: 0104 EF00 01 01 0040 00 90E4 01 01 0001 02 01 032F8102000400002710

**dev:1006**2026-08-23 06:10:50.005

debug

(M) Vedenlaatumittari: ec unchanged (0)

**dev:1006**2026-08-23 06:10:50.003

debug

(M) Vedenlaatumittari: DP 127 type=2 len=4 raw=0 bytes=00000000

**dev:1006**2026-08-23 06:10:49.999

debug

(M) Vedenlaatumittari: descMap: [raw:catchall: 0104 EF00 01 01 0040 00 90E4 01 01 0001 02 01 032E7F02000400000000, profileId:0104, clusterId:EF00, clusterInt:61184, sourceEndpoint:01, destinationEndpoint:01, options:0040, messageType:00, dni:90E4, isClusterSpecific:true, isManufacturerSpecific:true, manufacturerId:0001, command:02, direction:01, data:[03, 2E, 7F, 02, 00, 04, 00, 00, 00, 00]]

**dev:1006**2026-08-23 06:10:49.997

debug

(M) Vedenlaatumittari: parse: catchall: 0104 EF00 01 01 0040 00 90E4 01 01 0001 02 01 032E7F02000400000000

**dev:1006**2026-08-23 06:10:49.785

debug

(M) Vedenlaatumittari: DP 126 type=4 len=1 raw=0 bytes=00

**dev:1006**2026-08-23 06:10:49.781

debug

(M) Vedenlaatumittari: descMap: [raw:catchall: 0104 EF00 01 01 0040 00 90E4 01 01 0001 02 01 032D7E04000100, profileId:0104, clusterId:EF00, clusterInt:61184, sourceEndpoint:01, destinationEndpoint:01, options:0040, messageType:00, dni:90E4, isClusterSpecific:true, isManufacturerSpecific:true, manufacturerId:0001, command:02, direction:01, data:[03, 2D, 7E, 04, 00, 01, 00]]

**dev:1006**2026-08-23 06:10:49.779

debug

(M) Vedenlaatumittari: parse: catchall: 0104 EF00 01 01 0040 00 90E4 01 01 0001 02 01 032D7E04000100

**dev:1006**2026-08-23 06:10:49.578

debug

(M) Vedenlaatumittari: tds unchanged (0)

Same problem is reported in SmartThings forum :

Maybe a faulty sensor? :thinking:
The decoding in the driver is definitely correct, it is the device sending zero values …

Is the probe fully immersed in ordinary tap water?

There are three different models of the same device, which one did you pay for?

Hi

Maybe faulty one then... The one I have is ZG-109TDS that is supposed to be 7in1. Atleast that is printed on the sensor's battery cover.

Keep it - we may be missing something at the moment. I’ve also asked for more details on GitHub. This is a very new device, and there aren’t many reports yet about how it performs.

UNKNOWN Tuya DP 2 type=2 len=4 raw=47 bytes=0000002F -> that is ph. Added that but no other changes.

Finally working water values. Gemini fixed the code. Key was to parse hex values

click to expand

/**

  • Tuya Zigbee 7-in-1 Water Quality Monitor
  • Model: Excellux
  • Manufacturer: DTS1XM9
    */

metadata {
definition (name: "Tuya 7-in-1 Water Quality Monitor", namespace: "custom", author: "AI & Community") {
capability "Configuration"
capability "Refresh"
capability "Sensor"
capability "TemperatureMeasurement"
capability "RelativeHumidityMeasurement"

    attribute "ph", "number"
    attribute "tds", "number"
    attribute "ec", "number"
    attribute "salinity", "number"
    attribute "sg", "number"
    attribute "waterQualityAlert", "string" // Warning by text

    fingerprint profileId: "0104", endpointId: "01", inClusters: "0000,0003,0004,0005,EF00", outClusters: "0019,000A", model: "TS0601", manufacturer: "_TZE200_dts1xm9"
}

preferences {
    input name: "logEnable", type: "bool", title: "Enable debug logging", defaultValue: true
    
    // Warning levels (Preferences)
    input name: "minPh", type: "decimal", title: "pH Min (eg. 6.5)", defaultValue: 6.5
    input name: "maxPh", type: "decimal", title: "pH Max (eg. 8.5)", defaultValue: 8.5
    
    input name: "maxTds", type: "number", title: "TDS Max (ppm) (eg. 300)", defaultValue: 300
    input name: "maxEc", type: "number", title: "EC Max (µS/cm) (eg. 500)", defaultValue: 500
}

}

def parse(String description) {
if (logEnable) log.debug "parse raw message: ${description}"

if (description.contains("cluster: 0402") || description.contains("clusterId: 0402")) {
    try {
        def parts = description.split(",")
        def valPart = parts.find { it.contains("value:") }
        if (valPart) {
            String hexVal = valPart.split(":").trim()
            int tempRaw = Integer.parseInt(hexVal, 16)
            if (tempRaw > 10000) tempRaw = ((tempRaw & 0xFF) << 8) | ((tempRaw >> 8) & 0xFF)
            BigDecimal tempC = (BigDecimal) tempRaw / 100.0
            if (tempC > 0 && tempC < 100) {
                sendEvent(name: "temperature", value: tempC.setScale(1, BigDecimal.ROUND_HALF_UP), unit: "°C", isStateChange: true)
            }
        }
    } catch (Exception e) { }
    return
}

if (description.contains("cluster: 0405") || description.contains("clusterId: 0405")) {
    try {
        def parts = description.split(",")
        def valPart = parts.find { it.contains("value:") }
        if (valPart) {
            String hexVal = valPart.split(":").trim()
            int hum = Integer.parseInt(hexVal, 16)
            sendEvent(name: "humidity", value: (hum / 10.0), unit: "%", isStateChange: true)
        }
    } catch (Exception e) { }
    return
}

if (description.contains("EF00")) {
    def chunks = description.split(" ")
    String lastChunk = chunks[-1].trim()
    
    if (lastChunk && lastChunk.length() >= 6 && !lastChunk.contains("[") && !lastChunk.contains(":")) {
        parseRawHexValue(lastChunk)
    }
}

}

private void parseRawHexValue(String hexString) {
try {
List data =
for (int i = 0; i < hexString.length(); i += 2) {
if (i + 2 <= hexString.length()) {
data.add(Integer.parseInt(hexString.substring(i, i + 2), 16))
}
}
parseTuyaDataList(data)
} catch (Exception e) {
log.error "Error parsing hex: ${e.message}"
}
}

private void parseTuyaDataList(List data) {
if (data.size() < 5) return

for (int i = 0; i < data.size() - 4; i++) {
    int dp = data[i] & 0xFF
    int type = data[i+1] & 0xFF
    int len = ((data[i+2] & 0xFF) << 8) + (data[i+3] & 0xFF)
    
    if (len > 0 && len <= 4 && i + 4 + len <= data.size()) {
        List<Integer> rawVals = data.subList(i + 4, i + 4 + len)
        long rawVal = 0
        for (int j = 0; j < rawVals.size(); j++) {
            rawVal = (rawVal << 8) + (rawVals[j] & 0xFF)
        }
        procesSetValue(dp, rawVal)
        i += 3 + len
    } 
    else {
        int dpAlt = data[i+1] & 0xFF
        int typeAlt = data[i+2] & 0xFF
        int lenAlt = ((data[i+3] & 0xFF) << 8) + (data[i+4] & 0xFF)
        if (lenAlt > 0 && lenAlt <= 4 && i + 5 + lenAlt <= data.size()) {
            List<Integer> rawVals = data.subList(i + 5, i + 5 + lenAlt)
            long rawVal = 0
            for (int j = 0; j < rawVals.size(); j++) {
                rawVal = (rawVal << 8) + (rawVals[j] & 0xFF)
            }
            procesSetValue(dpAlt, rawVal)
            i += 4 + lenAlt
        }
    }
}

}

private void procesSetValue(int dp, long rawVal) {
if (logEnable) log.debug "Processing -> DP: ${dp}, Raw-value: ${rawVal}"

switch (dp) {
    case 2: // pH
        BigDecimal phVal = (BigDecimal) rawVal / 10.0
        if (phVal > 0 && phVal <= 14) {
            sendEvent(name: "ph", value: phVal, isStateChange: true)
            checkWarnings("pH", phVal)
        }
        break
        
    case 101: // Water temperature
        BigDecimal fahrenheit = (BigDecimal) rawVal / 10.0
        BigDecimal celsius = (fahrenheit - 32) * 5 / 9
        sendEvent(name: "temperature", value: celsius.setScale(1, BigDecimal.ROUND_HALF_UP), unit: "°C", isStateChange: true)
        break
        
    case 116: // TDS
        BigDecimal tdsVal = (BigDecimal) rawVal / 100.0
        def htds = tdsVal.setScale(0, BigDecimal.ROUND_HALF_UP)
        sendEvent(name: "tds", value: htds, unit: "ppm", isStateChange: true)
        checkWarnings("TDS", htds)
        break
        
    case 118: // EC
        BigDecimal ecVal = (BigDecimal) rawVal / 10.0
        def hec = ecVal.setScale(0, BigDecimal.ROUND_HALF_UP)
        sendEvent(name: "ec", value: hec, unit: "µS/cm", isStateChange: true)
        checkWarnings("EC", hec)
        break
        
    case 120: // SG
        BigDecimal sgVal = (BigDecimal) rawVal / 1000.0
        sendEvent(name: "sg", value: sgVal.setScale(3, BigDecimal.ROUND_HALF_UP), isStateChange: true)
        break
        
    case 125: // Saltynes Salinity
        sendEvent(name: "salinity", value: 0, unit: "ppt", isStateChange: true)
        break
}

}

// Funktion, that compare measurements to users set limits
private void checkWarnings(String tyyppi, def arvo) {
String tila = "OK"
String msg = "All values are normal"

if (tyyppi == "pH") {
    if (minPh != null && arvo < minPh) {
        tila = "WARNING"
        msg = "pH too low: ${arvo} (Limit: ${minPh})"
    } else if (maxPh != null && arvo > maxPh) {
        tila = "WARNING"
        msg = "pH too high: ${arvo} (Limit: ${maxPh})"
    }
}

if (tyyppi == "TDS" && maxTds != null && arvo > maxTds) {
    tila = "WARNING"
    msg = "TDS over the limit: ${arvo} ppm (Limit: ${maxTds})"
}

if (tyyppi == "EC" && maxEc != null && arvo > maxEc) {
    tila = "WARNING"
    msg = "EC over the limit: ${arvo} µS/cm (Limit: ${maxEc})"
}

if (tila == "WARNING") {
    log.warn "WATER QUALITY WARNING: ${msg}"
    sendEvent(name: "waterQualityAlert", value: msg, isStateChange: true)
} else {
    // If this test was OK, check that no other value is not in Warning state before resetting Warning to zero
    sendEvent(name: "waterQualityAlert", value: "Normal", isStateChange: false)
}

}

def configure() {
log.info "Configuring the device..."
return [
"he raw 0x${device.deviceNetworkId} 1 0x01 0x0000 {10 00 00 04 00}",
"he raw 0x${device.deviceNetworkId} 1 0x01 0xEF00 {10 00 01 03 00}"
]
}

def refresh() {
return ["he raw 0x${device.deviceNetworkId} 1 0x01 0xEF00 {10 00 02 00 00}"]
}

Can you post a screenshot of the device Info tab, Device Data section (at the bottom right) ?

Requesting factory firmware (.bin) for Excellux ZG-109TDS / ZSSF-NTDS-10 (Telink TLSR8251) to fix EC/TDS sleep bug

Hello community!
I have the Excellux ZG-109TDS 7-in-1 water quality monitor (PCB marked as ZSSF-NTDS-10, powered by Telink TLSR8251 chip).

I am facing a common issue: the sensor only updates air temperature, humidity, and probe temperature. All other values (EC, TDS, Salinity, SG) are permanently stuck at 0 or do not report at all.

Interestingly, I hooked up an oscilloscope to the EC probe lines. When I touch the circuit with the probe, the capacitive noise triggers a hardware interrupt on the ADC, forcing the TLSR8251 to wake up and temporarily generate a proper square wave (meander), sending valid data to Zigbee2MQTT for a short period. Once the oscilloscope probe is removed, the MCU falls into an aggressive deep sleep loop again, disabling the PWM/ADC generator for EC measurements.

The seller confirmed it is a factory firmware bug related to the power management configuration of the TLSR8251 MCU, but they haven't provided the updated binary yet.

Does anyone have the factory or corrected .bin firmware file for this device? I have a USB-UART flasher ready to flash it via the SWS interface. Thank you!