What do I need at Ikea?

Thank you - I knew I was missing something obvious .. : ) I’ve corrected my post above.
Unfortunately, IKEA doesn’t expose the battery percentage via the standard Matter protocol. Even though the IKEA Home app shows a simple “High battery” indicator , that information isn’t available to Matter controllers.

MYGGSPRAY fingerprints:

dev:78412025-12-23 17:56:25.326infofingerprint endpointId:"02", inClusters:"0003,001D,0406", outClusters:"", model:"MYGGSPRAY wrlss mtn sensor", manufacturer:"IKEA of Sweden", controllerType:"MAT"
dev:78412025-12-23 17:56:25.325infofingerprint endpointId:"01", inClusters:"0003,001D,0400", outClusters:"", model:"MYGGSPRAY wrlss mtn sensor", manufacturer:"IKEA of Sweden", controllerType:"MAT"`

The only extra function exposed is the Identify command (make the LED blink for about 15 seconds).

Update: The mystery regarding battery level management has now been resolved.

Here is a simple, 120 lines driver that reports both motion and illuminance and battery :


/*
 * IKEA MYGGSPRAY Matter Motion + Illuminance + Battery
 *
 * 
 *
 * Last edited: 2026/05/16 5:11 PM (uses newParse:true messages map format)
 */

import hubitat.device.HubAction
import hubitat.device.Protocol

metadata {
    definition(name: "IKEA MYGGSPRAY Matter Motion+Lux+Bat w/ healthStatus", namespace: "community", author: "kkossev + ChatGPT + Claude", singleThreaded: true, importUrl: "https://raw.githubusercontent.com/kkossev/Hubitat/development/Drivers/Ikea%20Matter/IKEA%20MYGGSPRAY%20Matter%20Motion%2BLux%2BBat.groovy") {
        capability "Sensor"
        capability "MotionSensor"
        capability "IlluminanceMeasurement"
        capability "Refresh"
        capability "Initialize"
        capability "Battery"
        capability "HealthCheck"

        attribute "healthStatus", "enum", ["online", "offline"]
        attribute "rtt", "number"
    }
    preferences {
        input name: "txtEnable",         type: "bool", title: "Enable descriptionText logging",                              defaultValue: true
        input name: "logEnable",         type: "bool", title: "Enable debug logging",                                       defaultValue: false
        input name: "enableHealthCheck", type: "bool", title: "Enable health check (ping every 5 min)",                     defaultValue: true
        input name: "enableAutoReInit",  type: "bool", title: "Auto re-initialize after 2 consecutive ping failures",       defaultValue: true
    }
}

void installed() {
    state.stats = [initializeCounter: 0, pingFailCounter: 0, autoReInitCounter: 0]
    initialize()
}

void updated() {
    logInfo "updated..."
    if (logEnable) runIn(7200, "logsOff")
    initialize()
}

void logsOff() {
    device.updateSetting("logEnable", [value: "false", type: "bool"])
    logWarn "Debug logging disabled"
}

void initialize() {
    if (state.stats == null) { state.stats = [initializeCounter: 0, pingFailCounter: 0, autoReInitCounter: 0] }
    state.stats.initializeCounter = (state.stats.initializeCounter ?: 0) + 1
    unschedule("deviceHealthCheck")
    unschedule("pingTimeout")
    unschedule("autoReInit")
    state.pingStart = null
    state.pingConsecutiveFails = 0
    if (getDataValue("newParse") != "true") { device.updateDataValue("newParse", "true") }
    logInfo "initialize... (initializeCounter=${state.stats.initializeCounter})"
    logInfo "model=${device.getDataValue('model') ?: device.model} newParse=${getDataValue("newParse")} uptime=${location.hub.uptime}"
    subscribeToAttributes()
    refresh()
    if (enableHealthCheck != false) { runEvery5Minutes("deviceHealthCheck") }
}

void refresh() {
    logDebug "refresh()"
    List<Map<String,String>> paths = []
    paths.add(matter.attributePath(0x01, 0x0400, 0x0000)) // Illuminance MeasuredValue
    paths.add(matter.attributePath(0x02, 0x0406, 0x0000)) // Occupancy
    paths.add(matter.attributePath(0x00, 0x002F, 0x000C)) // BatPercentRemaining
    paths.add(matter.attributePath(0x00, 0x0028, 0x000A)) // Software version string

    String cmd = matter.readAttributes(paths)
    sendHubCommand(new HubAction(cmd, Protocol.MATTER))
}

private void subscribeToAttributes() {
    List<Map<String,String>> paths = []
    paths.add(matter.attributePath(0x01, 0x0400, 0x0000)) // lux
    paths.add(matter.attributePath(0x02, 0x0406, 0x0000)) // motion
    paths.add(matter.attributePath(0x00, 0x002F, 0x000C)) // BatteryPercentRemaining

    String cmd = matter.cleanSubscribe(0, 600, paths)
    sendHubCommand(new HubAction(cmd, Protocol.MATTER))

    logInfo "subscribing to motion (EP2/0x0406) + illuminance (EP1/0x0400) + battery (EP0/0x002F/0x000C)"
}

void parse(String description) {
    logDebug "parse(String) called - ignored (newParse:true mode only)"
}

// parse(Map) - newParse:true format only
// Attribute report : [callbackType:Report, endpointInt:2, clusterInt:1030, attrInt:0, value:0]
// Battery report   : [callbackType:Report, endpointInt:0, clusterInt:47,   attrInt:12, value:200]
void parse(Map msg) {
    logDebug "parse(Map) received: ${msg}"
    handleLiveness(msg)

    // Ping response (explicit) or implicit ping success (any msg while ping in-flight)
    if (state.pingStart != null) {
        unschedule("pingTimeout")
        Long rtt = now() - (state.pingStart as Long)
        if (msg.clusterInt == 0x0028 && msg.attrInt == 0x0000) {
            sendEvent(name: "rtt", value: rtt, unit: "ms", type: "digital", descriptionText: "Ping round-trip time: ${rtt} ms")
            logInfo "Ping RTT: ${rtt} ms"
            state.pingStart = null
            return   // ping response fully handled
        }
        logDebug "Implicit ping success (msg arrived while ping in-flight), RTT: ${rtt} ms"
        state.pingStart = null
    }

    Integer ep     = msg.endpointInt
    Integer clus   = msg.clusterInt
    Integer attrId = msg.attrInt

    if (ep == null || clus == null || attrId == null) return

    // Illuminance Measurement: cluster 0x0400 attr 0x0000 (MeasuredValue)
    if (ep == 0x01 && clus == 0x0400 && attrId == 0x0000) {
        Integer value = safeInt(msg.value)
        if (value != null) {
            Integer lux = value / 100.0
            String descText = "Illuminance is ${lux} lx"
            sendEvent(name: "illuminance", value: lux, unit: "lx", descriptionText: txtEnable ? descText : null)
            logInfo descText
        }
        return
    }

    // Occupancy Sensing: cluster 0x0406 attr 0x0000 (Occupancy)
    if (ep == 0x02 && clus == 0x0406 && attrId == 0x0000) {
        Integer value = safeInt(msg.value)
        if (value != null) {
            String motion = ((value & 0x01) != 0) ? "active" : "inactive"
            String descText = "Motion is ${motion}"
            sendEvent(name: "motion", value: motion, descriptionText: txtEnable ? descText : null)
            logInfo descText
        }
        return
    }

    // Power Source (Battery): cluster 0x002F attr 0x000C (raw 0..200)
    if (ep == 0x00 && clus == 0x002F && attrId == 0x000C) {
        Integer raw = safeInt(msg.value)
        if (raw != null) {
            Integer pct = Math.round(raw / 2.0f)
            pct = Math.max(0, Math.min(100, pct))
            sendEvent(name: "battery", value: pct, unit: "%", descriptionText: "Battery is ${pct}%", isStateChange: true)
            logInfo "Battery is ${pct}%"
        }
        return
    }

    // Software version string (Basic Information cluster 0x0028, attr 0x000A)
    if (clus == 0x0028 && attrId == 0x000A) {
        String ver = msg.value?.toString() ?: ""
        device.updateDataValue("softwareVersion", ver)
        logInfo "softwareVersion=${ver}"
        return
    }

    logDebug "parse(Map): unhandled msg: ${msg}"
}

private Integer safeInt(def v) {
    if (v == null) return null
    if (v instanceof Boolean) return v ? 1 : 0
    try { return Integer.parseInt(v.toString(), 10) } catch (Exception ignored) { return null }
}

/* ---------- health check ---------- */

private void handleLiveness(Map msg) {
    // Cancel pending auto-reinit — any Matter message means the device is alive
    unschedule("autoReInit")

    // Reset consecutive fail counter on any activity
    state.pingConsecutiveFails = 0

    // If device is not yet online (null on first boot) or was offline, mark it online
    if (device.currentValue("healthStatus") != "online") {
        sendEvent(name: "healthStatus", value: "online", descriptionText: "${device.displayName} is online", type: "digital")
        logInfo "Device is back online"
    }
}

void ping() {
    deviceHealthCheck()
}

void deviceHealthCheck() {
    if (enableHealthCheck == false) { return }
    logDebug "deviceHealthCheck() - sending DataModelRevision read"
    state.pingStart = now()
    List<Map<String,String>> paths = [matter.attributePath(0x00, 0x0028, 0x0000)]
    sendHubCommand(new HubAction(matter.readAttributes(paths), Protocol.MATTER))
    runIn(30, "pingTimeout")
    // Battery staleness check: if no battery report in the last 12 hours, request a fresh read
    def lastBat = device.currentState("battery")
    if (lastBat == null || (now() - lastBat.date.time) > 12 * 3600 * 1000L) {
        logDebug "No battery report in >12h — requesting battery attribute read"
        sendHubCommand(new HubAction(matter.readAttributes([matter.attributePath(0x00, 0x002F, 0x000C)]), Protocol.MATTER))
    } else {
        logDebug "Battery report is recent (last: ${lastBat.date})"
    }
}

void pingTimeout() {
    state.pingStart = null
    state.pingConsecutiveFails = (state.pingConsecutiveFails ?: 0) + 1
    if (state.stats == null) { state.stats = [:] }
    state.stats.pingFailCounter = (state.stats.pingFailCounter ?: 0) + 1
    sendEvent(name: "rtt", value: -1, unit: "ms", type: "digital", descriptionText: "Ping timeout (consecutiveFails=${state.pingConsecutiveFails})")
    logWarn "Ping timeout! consecutiveFails=${state.pingConsecutiveFails} (total pingFails=${state.stats.pingFailCounter})"
    if (state.pingConsecutiveFails >= 2) {
        sendEvent(name: "healthStatus", value: "offline", descriptionText: "${device.displayName} is offline", type: "digital")
        logWarn "Device is OFFLINE after ${state.pingConsecutiveFails} consecutive ping failures"
        if (enableAutoReInit != false) {
            logWarn "Auto re-init scheduled in 30 seconds"
            runIn(30, "autoReInit")
        }
    }
}

void autoReInit() {
    if (state.stats == null) { state.stats = [:] }
    state.stats.autoReInitCounter = (state.stats.autoReInitCounter ?: 0) + 1
    logWarn "Auto re-initializing after failed health checks (autoReInitCounter=${state.stats.autoReInitCounter})"
    initialize()
}

// Logging helpers — prefix all messages with device display name
private void logDebug(String msg) {
    if (logEnable) { log.debug "${device.displayName} ${msg}" }
}

private void logInfo(String msg) {
    if (txtEnable) { log.info "${device.displayName} ${msg}" }
}

private void logWarn(String msg) {
    log.warn "${device.displayName} ${msg}"
}



Driver code is available on GitHub : https://raw.githubusercontent.com/kkossev/Hubitat/development/Drivers/Ikea%20Matter/IKEA%20MYGGSPRAY%20Matter%20Motion%2BLux%2BBat.groovy


Driver courtesy of ChatGPT :robot: — I just pressed the keys and drank the beer. :slight_smile:

Thanks for the feedback. I pair all my Matter devices via Google Home, and the battery level of the Myggspray motion sensor is also displayed there. The batteries are supposed to last 15 months; by then, Hubitat will surely have a driver that shows the battery status... :wink:

Hmm… it’s still a bit of a mystery how Google Home is showing the battery level in this case. Is it displayed as an exact percentage, or just a simple high/low (or OK/low) indicator?

It currently shows 100%.

Same 100% battery level reporting in SmartThings ... :thinking:
image

Update: I found a pair of nearly depleted batteries, and yes, ST shows the correct battery percentage.

image

:thinking: :thinking: :thinking:

Update2: I've found it! :slight_smile:

dev:78412025-12-23 20:36:43.902debugUnhandled Matter report: [endpoint:00, cluster:002F, attrId:000C, value:46, clusterInt:47, attrInt:12]
dev:78412025-12-23 20:36:43.901debugparse: read attr - endpoint: 00, cluster: 002F, attrId: 000C, value: 0446

The driver code is updated, now it reports the battery level too!
image

Congratulations on finding two batteries with 35% charge! It's almost like Christmas...:smile: Maybe someone from Hubitat will read this and update the seemingly incomplete Generic Matter Motion Sensor drivers. I really like your SmartThings interface; how did you pair your Hubitat with SmartThings so they're displayed like that?

The Generic Matter Motion Sensor driver subscribes to battery percent on cluster 0x002F, attribute 0x000C. From the above log entry, it looks like that's what this device reports. If you aren't seeing anything and a report should have come in (it should on pairing, or a "Refresh" should fetch it ... assuming Thread devices are normally awake to listen for it), if you could enable debug logging and share that output here, that would be great!

Yes, the Generic Matter Motion Sensor has Battery capability, but for some reason it is not shown on Refresh ...

Device Details Display

The device responds to commands with up to 7 seconds delay.

Interesting...the refresh() command has, in part, this:

attributePaths.add(matter.attributePath(device.endpointId, 0x002F, 0x0C))

It looks like the endpoint might be 00 for battery instead of 02 or whatever endpointId is set to for you. I've seen this in a lot of Matter drivers. Probably need to re-think how this is handled "generically"... :thinking:

I have seen other Matter devices where cluster 0x002F is explicitly declared on endpoints 1 or 2. That’s why I was curious about how the battery percentage is reported when the Power Source cluster is not present in the device fingerprints.

IKEA (and probably others?) include the Power Source cluster on endpointId 00.
This was my test code :

    // Try on endpoint 0x00 first (common for bridged/root), then also on 0x01/0x02 just in case.
    [0x00, 0x01, 0x02].each { ep ->
        paths.add(matter.attributePath(ep as int, 0x002F, 0x0000)) // Status
        paths.add(matter.attributePath(ep as int, 0x002F, 0x000C)) // BatPercentRemaining
        paths.add(matter.attributePath(ep as int, 0x002F, 0x000E)) // BatChargeLevel
        paths.add(matter.attributePath(ep as int, 0x002F, 0x000F)) // BatReplacementNeeded
    }

Next driver by ChatGPT : "IKEA ALPSTUGA Matter" :


/*
 * IKEA ALPSTUGA Matter Air Quality Monitor
 *
 * https://community.hubitat.com/t/what-do-i-need-at-ikea/158182/72?u=kkossev
 * 
 * Last edited: 2026/05/16 11:40 PM
 *
 */

import hubitat.device.HubAction
import hubitat.device.Protocol

metadata {
    definition(name: "IKEA ALPSTUGA Matter w/ healthStatus", namespace: "community", author: "kkossev + ChatGPT + Claude", singleThreaded: true, importUrl: "https://raw.githubusercontent.com/kkossev/Hubitat/development/Drivers/Ikea%20Matter/IKEA%20ALPSTUGA%20Matter%20AQ%2BTemp%2BHum%2BPM2.5%2BCO2.groovy") {
        capability "Sensor"
        capability "Switch"
        capability "TemperatureMeasurement"
        capability "RelativeHumidityMeasurement"
        capability "AirQuality"
        capability "CarbonDioxideMeasurement"
        capability "Refresh"
        capability "Initialize"
        capability "HealthCheck"

        attribute "airQuality", "string"
        attribute "pm25", "number"
        attribute "healthStatus", "enum", ["online", "offline"]
        attribute "rtt", "number"
    }
    preferences {
        input name: "txtEnable",         type: "bool", title: "Enable descriptionText logging",                              defaultValue: true
        input name: "logEnable",         type: "bool", title: "Enable debug logging",                                       defaultValue: false
        input name: "enableHealthCheck", type: "bool", title: "Enable health check (ping every 5 min)",                     defaultValue: true
        input name: "enableAutoReInit",  type: "bool", title: "Auto re-initialize after 2 consecutive ping failures",       defaultValue: true
    }
}

void installed() {
    state.stats = [initializeCounter: 0, pingFailCounter: 0, autoReInitCounter: 0]
    initialize()
}

void updated() {
    logInfo "updated..."
    if (logEnable) runIn(7200, "logsOff")
    initialize()
}

void logsOff() {
    device.updateSetting("logEnable", [value: "false", type: "bool"])
    logWarn "Debug logging disabled"
}

void initialize() {
    if (state.stats == null) { state.stats = [initializeCounter: 0, pingFailCounter: 0, autoReInitCounter: 0] }
    state.stats.initializeCounter = (state.stats.initializeCounter ?: 0) + 1
    unschedule("deviceHealthCheck")
    unschedule("pingTimeout")
    unschedule("autoReInit")
    state.pingStart = null
    state.pingConsecutiveFails = 0
    if (getDataValue("newParse") != "true") { device.updateDataValue("newParse", "true") }
    logInfo "initialize... (initializeCounter=${state.stats.initializeCounter})"
    logInfo "model=${device.getDataValue('model') ?: device.model} newParse=${getDataValue("newParse")} uptime=${location.hub.uptime}"
    subscribeToAttributes()
    refresh()
    if (enableHealthCheck != false) { runEvery5Minutes("deviceHealthCheck") }
}

void refresh() {
    logDebug "refresh()"
    List<Map<String,String>> paths = []
    paths.add(matter.attributePath(0x01, 0x0006, 0x0000)) // OnOff
    paths.add(matter.attributePath(0x01, 0x005B, 0x0000)) // AirQuality
    paths.add(matter.attributePath(0x01, 0x0402, 0x0000)) // Temperature
    paths.add(matter.attributePath(0x01, 0x0405, 0x0000)) // Humidity
    paths.add(matter.attributePath(0x01, 0x042A, 0x0000)) // PM2.5
    paths.add(matter.attributePath(0x01, 0x040D, 0x0000)) // CO2 MeasuredValue
    paths.add(matter.attributePath(0x00, 0x0028, 0x000A)) // Software version string

    String cmd = matter.readAttributes(paths)
    sendHubCommand(new HubAction(cmd, Protocol.MATTER))
}

private void subscribeToAttributes() {
    List<Map<String,String>> paths = []
    paths.add(matter.attributePath(0x01, 0x0006, 0x0000))
    paths.add(matter.attributePath(0x01, 0x005B, 0x0000))
    paths.add(matter.attributePath(0x01, 0x0402, 0x0000))
    paths.add(matter.attributePath(0x01, 0x0405, 0x0000))
    paths.add(matter.attributePath(0x01, 0x042A, 0x0000))
    paths.add(matter.attributePath(0x01, 0x040D, 0x0000))

    String cmd = matter.cleanSubscribe(0, 600, paths)
    sendHubCommand(new HubAction(cmd, Protocol.MATTER))

    logInfo "subscribing to on/off + air quality + temp + humidity + PM2.5 + CO2 (EP1)"
}

void parse(String description) {
    logDebug "parse(String) called - ignored (newParse:true mode only)"
}

// parse(Map) - newParse:true format only
// Attribute report : [callbackType:Report, endpointInt:1, clusterInt:1026, attrInt:0, value:2343]
// Float report     : [callbackType:Report, endpointInt:1, clusterInt:1066, attrInt:0, value:5.0]
void parse(Map msg) {
    logDebug "parse(Map) received: ${msg}"
    handleLiveness(msg)

    // Ping response (explicit) or implicit ping success (any msg while ping in-flight)
    if (state.pingStart != null) {
        unschedule("pingTimeout")
        Long rtt = now() - (state.pingStart as Long)
        if (msg.clusterInt == 0x0028 && msg.attrInt == 0x0000) {
            sendEvent(name: "rtt", value: rtt, unit: "ms", type: "digital", descriptionText: "Ping round-trip time: ${rtt} ms")
            logInfo "Ping RTT: ${rtt} ms"
            state.pingStart = null
            return   // ping response fully handled
        }
        logDebug "Implicit ping success (msg arrived while ping in-flight), RTT: ${rtt} ms"
        state.pingStart = null
    }

    Integer ep     = msg.endpointInt
    Integer clus   = msg.clusterInt
    Integer attrId = msg.attrInt

    if (ep == null || clus == null || attrId == null) return

    // Software version string (Basic Information cluster 0x0028, attr 0x000A) — EP0
    if (clus == 0x0028 && attrId == 0x000A) {
        String ver = msg.value?.toString() ?: ""
        device.updateDataValue("softwareVersion", ver)
        logInfo "softwareVersion=${ver}"
        return
    }

    // All sensor attributes are on EP1
    if (ep != 0x01) {
        logDebug "Ignoring endpointInt=${ep} cluster=${clus}"
        return
    }

    // On/Off: cluster 0x0006 attr 0x0000
    if (clus == 0x0006 && attrId == 0x0000) {
        Integer v = safeInt(msg.value)
        if (v != null) {
            String sw = (v != 0) ? "on" : "off"
            sendEvent(name: "switch", value: sw, descriptionText: txtEnable ? "Switch is ${sw}" : null)
            logInfo "Switch is ${sw}"
        }
        return
    }

    // Air Quality: cluster 0x005B attr 0x0000
    if (clus == 0x005B && attrId == 0x0000) {
        Integer aq = safeInt(msg.value)
        if (aq != null) {
            String aqText = airQualityToText(aq)
            sendEvent(name: "airQuality", value: aqText, descriptionText: txtEnable ? "Air quality is ${aqText}" : null)
            sendEvent(name: "airQualityIndex", value: aq)
            logInfo "Air quality is ${aqText} (index: ${aq})"
        }
        return
    }

    // Temperature: cluster 0x0402 attr 0x0000 (0.01 °C, signed)
    if (clus == 0x0402 && attrId == 0x0000) {
        Integer raw = safeInt(msg.value)
        if (raw != null) {
            BigDecimal c = ((short) raw) / 100.0
            BigDecimal cRounded = c.setScale(1, BigDecimal.ROUND_HALF_UP)
            def t = convertTemperatureIfNeeded(cRounded, "C", 1)
            String unit = (location.temperatureScale == "F") ? "°F" : "°C"
            sendEvent(name: "temperature", value: t, unit: unit, descriptionText: txtEnable ? "Temperature is ${t} ${unit}" : null)
            logInfo "Temperature is ${t} ${unit}"
        }
        return
    }

    // Humidity: cluster 0x0405 attr 0x0000 (0.01 %)
    if (clus == 0x0405 && attrId == 0x0000) {
        Integer raw = safeInt(msg.value)
        if (raw != null) {
            BigDecimal rh = (raw / 100.0).setScale(1, BigDecimal.ROUND_HALF_UP)
            sendEvent(name: "humidity", value: rh, unit: "%", descriptionText: txtEnable ? "Humidity is ${rh}%" : null)
            logInfo "Humidity is ${rh}%"
        }
        return
    }

    // PM2.5: cluster 0x042A attr 0x0000 (IEEE 754 float32)
    if (clus == 0x042A && attrId == 0x0000) {
        Float pm = safeFloat(msg.value)
        if (pm != null) {
            Integer pmInt = Math.round(pm)
            sendEvent(name: "pm25", value: pmInt, unit: "µg/m³", descriptionText: txtEnable ? "PM2.5 is ${pmInt} µg/m³" : null)
            logInfo "PM2.5 is ${pmInt} µg/m³"
        }
        return
    }

    // CO2: cluster 0x040D attr 0x0000 (IEEE 754 float32, ppm)
    if (clus == 0x040D && attrId == 0x0000) {
        Float co2f = safeFloat(msg.value)
        if (co2f != null) {
            Integer co2 = Math.round(co2f)
            sendEvent(name: "carbonDioxide", value: co2, unit: "ppm", descriptionText: txtEnable ? "CO₂ is ${co2} ppm" : null)
            logInfo "CO₂ is ${co2} ppm"
        }
        return
    }

    logDebug "parse(Map): unhandled msg: ${msg}"
}

private Integer safeInt(def v) {
    if (v == null) return null
    if (v instanceof Boolean) return v ? 1 : 0
    try { return Integer.parseInt(v.toString(), 10) } catch (Exception ignored) { return null }
}

// For IEEE 754 float32 clusters (PM2.5, CO2):
// In newParse:true mode Hubitat delivers float attributes as Java Float; fall back to intBitsToFloat for raw integer delivery.
private Float safeFloat(def v) {
    if (v == null) return null
    if (v instanceof Float) return v
    if (v instanceof Double) return v.floatValue()
    if (v instanceof Number) return Float.intBitsToFloat(v.intValue())
    try { return Float.intBitsToFloat(Integer.parseInt(v.toString(), 10)) } catch (Exception ignored) { return null }
}

void on() {
    String cmd = matter.on()
    sendHubCommand(new HubAction(cmd, Protocol.MATTER))
}

void off() {
    String cmd = matter.off()
    sendHubCommand(new HubAction(cmd, Protocol.MATTER))
}

private static String airQualityToText(Integer v) {
    switch (v) {
        case 0: return "unknown"
        case 1: return "good"
        case 2: return "fair"
        case 3: return "moderate"
        case 4: return "poor"
        case 5: return "very poor"
        case 6: return "extremely poor"
        default: return "unknown"
    }
}

/* ---------- health check ---------- */

private void handleLiveness(Map msg) {
    // Cancel pending auto-reinit — any Matter message means the device is alive
    unschedule("autoReInit")

    // Reset consecutive fail counter on any activity
    state.pingConsecutiveFails = 0

    // If device was offline, mark it back online
    if (device.currentValue("healthStatus") != "online") {
        sendEvent(name: "healthStatus", value: "online", descriptionText: "${device.displayName} is online", type: "digital")
        logInfo "Device is back online"
    }
}

void ping() {
    deviceHealthCheck()
}

void deviceHealthCheck() {
    if (enableHealthCheck == false) { return }
    logDebug "deviceHealthCheck() - sending DataModelRevision read"
    state.pingStart = now()
    List<Map<String,String>> paths = [matter.attributePath(0x00, 0x0028, 0x0000)]
    sendHubCommand(new HubAction(matter.readAttributes(paths), Protocol.MATTER))
    runIn(30, "pingTimeout")
}

void pingTimeout() {
    state.pingStart = null
    state.pingConsecutiveFails = (state.pingConsecutiveFails ?: 0) + 1
    if (state.stats == null) { state.stats = [:] }
    state.stats.pingFailCounter = (state.stats.pingFailCounter ?: 0) + 1
    sendEvent(name: "rtt", value: -1, unit: "ms", type: "digital", descriptionText: "Ping timeout (consecutiveFails=${state.pingConsecutiveFails})")
    logWarn "Ping timeout! consecutiveFails=${state.pingConsecutiveFails} (total pingFails=${state.stats.pingFailCounter})"
    if (state.pingConsecutiveFails >= 2) {
        sendEvent(name: "healthStatus", value: "offline", descriptionText: "${device.displayName} is offline", type: "digital")
        logWarn "Device is OFFLINE after ${state.pingConsecutiveFails} consecutive ping failures"
        if (enableAutoReInit != false) {
            logWarn "Auto re-init scheduled in 30 seconds"
            runIn(30, "autoReInit")
        }
    }
}

void autoReInit() {
    if (state.stats == null) { state.stats = [:] }
    state.stats.autoReInitCounter = (state.stats.autoReInitCounter ?: 0) + 1
    logWarn "Auto re-initializing after failed health checks (autoReInitCounter=${state.stats.autoReInitCounter})"
    initialize()
}

// Logging helpers — prefix all messages with device display name
private void logDebug(String msg) {
    if (logEnable) { log.debug "${device.displayName} ${msg}" }
}

private void logInfo(String msg) {
    if (txtEnable) { log.info "${device.displayName} ${msg}" }
}

private void logWarn(String msg) {
    log.warn "${device.displayName} ${msg}"
}




The code is also available on GitHub : https://raw.githubusercontent.com/kkossev/Hubitat/development/Drivers/Ikea%20Matter/IKEA%20ALPSTUGA%20Matter%20AQ%2BTemp%2BHum%2BPM2.5%2BCO2.groovy


image

Another tiny code driver for IKEA TIMMERFLOTTE Matter Temp + Humidity + Battery :


/*
 * IKEA TIMMERFLOTTE Matter Temp + Humidity + Battery
 *
 * https://community.hubitat.com/t/what-do-i-need-at-ikea/158182/73?u=kkossev
 *
 * Last edited: 2026/05/16 10:59 PM
 */

import hubitat.device.HubAction
import hubitat.device.Protocol

metadata {
    definition(name: "IKEA TIMMERFLOTTE Matter Temp+Hum+Bat w/ healthStatus", namespace: "community", author: "kkossev + ChatGPT + Claude", singleThreaded: true, importUrl: "https://raw.githubusercontent.com/kkossev/Hubitat/development/Drivers/Ikea%20Matter/IKEA%20TIMMERFLOTTE%20Matter%20Temp%2BHum%2BBat.groovy") {
        capability "Sensor"
        capability "TemperatureMeasurement"
        capability "RelativeHumidityMeasurement"
        capability "Battery"
        capability "Refresh"
        capability "Initialize"
        capability "HealthCheck"

        attribute "healthStatus", "enum", ["online", "offline"]
        attribute "rtt", "number"
    }
    preferences {
        input name: "txtEnable",         type: "bool", title: "Enable descriptionText logging",                              defaultValue: true
        input name: "logEnable",         type: "bool", title: "Enable debug logging",                                       defaultValue: false
        input name: "enableHealthCheck", type: "bool", title: "Enable health check (ping every 5 min)",                     defaultValue: true
        input name: "enableAutoReInit",  type: "bool", title: "Auto re-initialize after 2 consecutive ping failures",       defaultValue: true
    }
}

void installed() {
    state.stats = [initializeCounter: 0, pingFailCounter: 0, autoReInitCounter: 0]
    initialize()
}

void updated() {
    logInfo "updated..."
    if (logEnable) runIn(7200, "logsOff")
    initialize()
}

void logsOff() {
    device.updateSetting("logEnable", [value: "false", type: "bool"])
    logWarn "Debug logging disabled"
}

void initialize() {
    if (state.stats == null) { state.stats = [initializeCounter: 0, pingFailCounter: 0, autoReInitCounter: 0] }
    state.stats.initializeCounter = (state.stats.initializeCounter ?: 0) + 1
    unschedule("deviceHealthCheck")
    unschedule("pingTimeout")
    unschedule("autoReInit")
    state.pingStart = null
    state.pingConsecutiveFails = 0
    if (getDataValue("newParse") != "true") { device.updateDataValue("newParse", "true") }
    logInfo "initialize... (initializeCounter=${state.stats.initializeCounter})"
    logInfo "model=${device.getDataValue('model') ?: device.model} newParse=${getDataValue("newParse")} uptime=${location.hub.uptime}"
    subscribeToAttributes()
    refresh()
    if (enableHealthCheck != false) { runEvery5Minutes("deviceHealthCheck") }
}

void refresh() {
    logDebug "refresh()"
    List<Map<String,String>> paths = []
    paths.add(matter.attributePath(0x01, 0x0402, 0x0000)) // Temperature MeasuredValue
    paths.add(matter.attributePath(0x02, 0x0405, 0x0000)) // Humidity MeasuredValue
    paths.add(matter.attributePath(0x00, 0x002F, 0x000C)) // BatteryPercentRemaining
    paths.add(matter.attributePath(0x00, 0x0028, 0x000A)) // Software version string
    String cmd = matter.readAttributes(paths)
    sendHubCommand(new HubAction(cmd, Protocol.MATTER))
}

private void subscribeToAttributes() {
    List<Map<String,String>> paths = []
    paths.add(matter.attributePath(0x01, 0x0402, 0x0000)) // temp
    paths.add(matter.attributePath(0x02, 0x0405, 0x0000)) // humidity
    paths.add(matter.attributePath(0x00, 0x002F, 0x000C)) // BatteryPercentRemaining

    String cmd = matter.cleanSubscribe(0, 600, paths)
    sendHubCommand(new HubAction(cmd, Protocol.MATTER))

    logInfo "subscribing to temperature (EP1/0x0402) + humidity (EP2/0x0405) + battery (EP0/0x002F/0x000C)"
}

void parse(String description) {
    logDebug "parse(String) called - ignored (newParse:true mode only)"
}

// parse(Map) - newParse:true format only
// Attribute report : [callbackType:Report, endpointInt:1, clusterInt:1026, attrInt:0, value:2150]
// Battery report   : [callbackType:Report, endpointInt:0, clusterInt:47,   attrInt:12, value:200]
void parse(Map msg) {
    logDebug "parse(Map) received: ${msg}"
    handleLiveness(msg)

    // Ping response (explicit) or implicit ping success (any msg while ping in-flight)
    if (state.pingStart != null) {
        unschedule("pingTimeout")
        Long rtt = now() - (state.pingStart as Long)
        if (msg.clusterInt == 0x0028 && msg.attrInt == 0x0000) {
            sendEvent(name: "rtt", value: rtt, unit: "ms", type: "digital", descriptionText: "Ping round-trip time: ${rtt} ms")
            logInfo "Ping RTT: ${rtt} ms"
            state.pingStart = null
            return   // ping response fully handled
        }
        logDebug "Implicit ping success (msg arrived while ping in-flight), RTT: ${rtt} ms"
        state.pingStart = null
    }

    Integer ep     = msg.endpointInt
    Integer clus   = msg.clusterInt
    Integer attrId = msg.attrInt

    if (ep == null || clus == null || attrId == null) return

    // Temperature: EP01 cluster 0x0402 attr 0x0000 (0.01 °C)
    if (ep == 0x01 && clus == 0x0402 && attrId == 0x0000) {
        Integer raw = safeInt(msg.value)
        if (raw != null) {
            BigDecimal c = raw / 100.0
            BigDecimal cRounded = c.setScale(1, BigDecimal.ROUND_HALF_UP)
            def t = convertTemperatureIfNeeded(cRounded, "C", 1)
            String unit = (location.temperatureScale == "F") ? "°F" : "°C"
            String descText = "Temperature is ${t} ${unit}"
            sendEvent(name: "temperature", value: t, unit: unit, descriptionText: txtEnable ? descText : null)
            logInfo descText
        }
        return
    }

    // Humidity: EP02 cluster 0x0405 attr 0x0000 (0.01 %)
    if (ep == 0x02 && clus == 0x0405 && attrId == 0x0000) {
        Integer raw = safeInt(msg.value)
        if (raw != null) {
            BigDecimal rh = (raw / 100.0).setScale(1, BigDecimal.ROUND_HALF_UP)
            String descText = "Humidity is ${rh}%"
            sendEvent(name: "humidity", value: rh, unit: "%", descriptionText: txtEnable ? descText : null)
            logInfo descText
        }
        return
    }

    // Power Source (Battery): EP0 cluster 0x002F attr 0x000C (raw 0..200)
    if (ep == 0x00 && clus == 0x002F && attrId == 0x000C) {
        Integer raw = safeInt(msg.value)
        if (raw != null) {
            Integer pct = Math.round(raw / 2.0f)
            pct = Math.max(0, Math.min(100, pct))
            sendEvent(name: "battery", value: pct, unit: "%", descriptionText: "Battery is ${pct}%", isStateChange: true)
            logInfo "Battery is ${pct}%"
        }
        return
    }

    // Software version string (Basic Information cluster 0x0028, attr 0x000A)
    if (clus == 0x0028 && attrId == 0x000A) {
        String ver = msg.value?.toString() ?: ""
        device.updateDataValue("softwareVersion", ver)
        logInfo "softwareVersion=${ver}"
        return
    }

    logDebug "parse(Map): unhandled msg: ${msg}"
}

private Integer safeInt(def v) {
    if (v == null) return null
    if (v instanceof Boolean) return v ? 1 : 0
    try { return Integer.parseInt(v.toString(), 10) } catch (Exception ignored) { return null }
}

/* ---------- health check ---------- */

private void handleLiveness(Map msg) {
    // Cancel pending auto-reinit — any Matter message means the device is alive
    unschedule("autoReInit")

    // Reset consecutive fail counter on any activity
    state.pingConsecutiveFails = 0

    // If device is not yet online (null on first boot) or was offline, mark it online
    if (device.currentValue("healthStatus") != "online") {
        sendEvent(name: "healthStatus", value: "online", descriptionText: "${device.displayName} is online", type: "digital")
        logInfo "Device is back online"
    }
}

void ping() {
    deviceHealthCheck()
}

void deviceHealthCheck() {
    if (enableHealthCheck == false) { return }
    logDebug "deviceHealthCheck() - sending DataModelRevision read"
    state.pingStart = now()
    List<Map<String,String>> paths = [matter.attributePath(0x00, 0x0028, 0x0000)]
    sendHubCommand(new HubAction(matter.readAttributes(paths), Protocol.MATTER))
    runIn(30, "pingTimeout")
    // Battery staleness check: if no battery report in the last 12 hours, request a fresh read
    def lastBat = device.currentState("battery")
    if (lastBat == null || (now() - lastBat.date.time) > 12 * 3600 * 1000L) {
        logWarn "No battery report in >12h — requesting battery attribute read"
        sendHubCommand(new HubAction(matter.readAttributes([matter.attributePath(0x00, 0x002F, 0x000C)]), Protocol.MATTER))
    } else {
        logDebug "Battery report is recent (last: ${lastBat.date})"
    }
}

void pingTimeout() {
    state.pingStart = null
    state.pingConsecutiveFails = (state.pingConsecutiveFails ?: 0) + 1
    if (state.stats == null) { state.stats = [:] }
    state.stats.pingFailCounter = (state.stats.pingFailCounter ?: 0) + 1
    sendEvent(name: "rtt", value: -1, unit: "ms", type: "digital", descriptionText: "Ping timeout (consecutiveFails=${state.pingConsecutiveFails})")
    logWarn "Ping timeout! consecutiveFails=${state.pingConsecutiveFails} (total pingFails=${state.stats.pingFailCounter})"
    if (state.pingConsecutiveFails >= 2) {
        sendEvent(name: "healthStatus", value: "offline", descriptionText: "${device.displayName} is offline", type: "digital")
        logWarn "Device is OFFLINE after ${state.pingConsecutiveFails} consecutive ping failures"
        if (enableAutoReInit != false) {
            logWarn "Auto re-init scheduled in 30 seconds"
            runIn(30, "autoReInit")
        }
    }
}

void autoReInit() {
    if (state.stats == null) { state.stats = [:] }
    state.stats.autoReInitCounter = (state.stats.autoReInitCounter ?: 0) + 1
    logWarn "Auto re-initializing after failed health checks (autoReInitCounter=${state.stats.autoReInitCounter})"
    initialize()
}

// Logging helpers — prefix all messages with device display name
private void logDebug(String msg) {
    if (logEnable) { log.debug "${device.displayName} ${msg}" }
}

private void logInfo(String msg) {
    if (txtEnable) { log.info "${device.displayName} ${msg}" }
}

private void logWarn(String msg) {
    log.warn "${device.displayName} ${msg}"
}




The code is available on GitHub : https://raw.githubusercontent.com/kkossev/Hubitat/development/Drivers/Ikea%20Matter/IKEA%20TIMMERFLOTTE%20Matter%20Temp%2BHum%2BBat.groovy


image

Stop, stop...you're making me regret missing my IKEA adventure last week even more!! :wink:

One more : IKEA KLIPPBOK Matter Water Leak Sensor :


/*
 * IKEA KLIPPBOK Matter Water Leak Sensor
 * 
 * https://community.hubitat.com/t/what-do-i-need-at-ikea/158182/75?u=kkossev
 *
 * Last edited: 2026/05/14 5:35 PM
 */

import hubitat.device.HubAction
import hubitat.device.Protocol

metadata {
    definition(name: "IKEA KLIPPBOK Matter Water Leak Sensor w/ healthStatus", namespace: "community", author: "kkossev + ChatGPT + Claude", singleThreaded: true, importUrl: "https://raw.githubusercontent.com/kkossev/Hubitat/development/Drivers/Ikea%20Matter/IKEA%20KLIPPBOK%20Matter%20Water%20Leak%20Sensor.groovy") {

        capability "Sensor"
        capability "WaterSensor"
        capability "Battery"
        capability "Refresh"
        capability "Initialize"
        capability "HealthCheck"

        attribute "healthStatus", "enum", ["online", "offline"]
        attribute "rtt", "number"

        fingerprint endpointId: "01",
                inClusters: "0003,001D,0045,0080",
                outClusters: "",
                model: "KLIPPBOK water leak sensor",
                manufacturer: "IKEA of Sweden",
                controllerType: "MAT"
    }

    preferences {
        input name: "txtEnable",         type: "bool", title: "Enable descriptionText logging",                              defaultValue: true
        input name: "logEnable",         type: "bool", title: "Enable debug logging",                                       defaultValue: false
        input name: "enableHealthCheck", type: "bool", title: "Enable health check (ping every 5 min)",                     defaultValue: true
        input name: "enableAutoReInit",  type: "bool", title: "Auto re-initialize after 2 consecutive ping failures",       defaultValue: true
    }
}

void installed() {
    state.stats = [initializeCounter: 0, pingFailCounter: 0, autoReInitCounter: 0]
    initialize()
}

void updated() {
    logInfo "updated..."
    if (logEnable) runIn(7200, "logsOff")
    initialize()
}

void logsOff() {
    device.updateSetting("logEnable", [value: "false", type: "bool"])
    logWarn "Debug logging disabled"
}

void initialize() {
    if (state.stats == null) { state.stats = [initializeCounter: 0, pingFailCounter: 0, autoReInitCounter: 0] }
    state.stats.initializeCounter = (state.stats.initializeCounter ?: 0) + 1
    unschedule("deviceHealthCheck")
    unschedule("pingTimeout")
    unschedule("autoReInit")
    state.pingStart = null
    state.pingConsecutiveFails = 0
    if (getDataValue("newParse") != "true") { device.updateDataValue("newParse", "true") }
    logInfo "initialize... (initializeCounter=${state.stats.initializeCounter})"
    logInfo "model=${device.getDataValue('model') ?: device.model} newParse=${getDataValue("newParse")} uptime=${location.hub.uptime}"
    subscribeToAttributes()
    refresh()
    if (enableHealthCheck != false) { runEvery5Minutes("deviceHealthCheck") }
}

void refresh() {
    logDebug "refresh()"

    List<Map<String,String>> paths = []
    // Water/leak state (cluster 0x0045 attr 0x0000 => boolean)
    paths.add(matter.attributePath(0x01, 0x0045, 0x0000))
    // Battery (endpoint 0, Power Source cluster 0x002F, attr 0x000C)
    paths.add(matter.attributePath(0x00, 0x002F, 0x000C))
    // Software version string (Basic Information cluster 0x0028, attr 0x000A)
    paths.add(matter.attributePath(0x00, 0x0028, 0x000A))

    String cmd = matter.readAttributes(paths)
    sendHubCommand(new HubAction(cmd, Protocol.MATTER))
}

private void subscribeToAttributes() {
    List<Map<String,String>> paths = []
    paths.add(matter.attributePath(0x01, 0x0045, 0x0000))
    paths.add(matter.attributePath(0x00, 0x002F, 0x000C))

    String cmd = matter.cleanSubscribe(0, 600, paths)
    sendHubCommand(new HubAction(cmd, Protocol.MATTER))

    logInfo "subscribing to water state (EP1/0x0045) + battery (EP0/0x002F/0x000C)"
}

void parse(String description) {
    logDebug "parse(String) called - ignored (newParse:true mode only)"
}

// parse(Map) - newParse:true format only
// Attribute report : [callbackType:Report, endpointInt:1, clusterInt:69, attrInt:0, data:[0:BOOL:false], value:false]
// Battery report   : [callbackType:Report, endpointInt:0, clusterInt:47, attrInt:12, value:200]
void parse(Map msg) {
    logDebug "parse(Map) received: ${msg}"
    handleLiveness(msg)

    // Ping response (explicit) or implicit ping success (any msg while ping in-flight)
    if (state.pingStart != null) {
        unschedule("pingTimeout")
        Long rtt = now() - (state.pingStart as Long)
        if (msg.clusterInt == 0x0028 && msg.attrInt == 0x0000) {
            sendEvent(name: "rtt", value: rtt, unit: "ms", type: "digital", descriptionText: "Ping round-trip time: ${rtt} ms")
            logInfo "Ping RTT: ${rtt} ms"
            state.pingStart = null
            return   // ping response fully handled
        }
        logDebug "Implicit ping success (msg arrived while ping in-flight), RTT: ${rtt} ms"
        state.pingStart = null
    }

    Integer ep     = msg.endpointInt
    Integer clus   = msg.clusterInt
    Integer attrId = msg.attrInt

    if (ep == null || clus == null || attrId == null) return

    // Water state: EP1 cluster 0x0045 attr 0x0000 (BooleanState; false=dry, true=wet)
    if (ep == 0x01 && clus == 0x0045 && attrId == 0x0000) {
        Integer v = safeInt(msg.value)
        if (v != null) {
            String wet = (v != 0) ? "wet" : "dry"
            sendEvent(name: "water", value: wet, descriptionText: txtEnable ? "Water sensor is ${wet}" : null)
            logInfo "Water sensor is ${wet}"
        }
        return
    }

    // Battery: EP0 cluster 0x002F attr 0x000C (raw 0..200)
    if (ep == 0x00 && clus == 0x002F && attrId == 0x000C) {
        Integer raw = safeInt(msg.value)
        if (raw != null) {
            Integer pct = Math.round(raw / 2.0f)
            pct = Math.max(0, Math.min(100, pct))
            sendEvent(name: "battery", value: pct, unit: "%", descriptionText: "Battery is ${pct}%", isStateChange: true)
            logInfo "Battery is ${pct}%"
        }
        return
    }

    // Software version string (Basic Information cluster 0x0028, attr 0x000A)
    if (clus == 0x0028 && attrId == 0x000A) {
        String ver = msg.value?.toString() ?: ""
        device.updateDataValue("softwareVersion", ver)
        logInfo "softwareVersion=${ver}"
        return
    }

    logDebug "parse(Map): unhandled msg: ${msg}"
}

private Integer safeInt(def v) {
    if (v == null) return null
    if (v instanceof Boolean) return v ? 1 : 0
    try { return Integer.parseInt(v.toString(), 10) } catch (Exception ignored) { return null }
}

/* ---------- health check ---------- */

private void handleLiveness(Map msg) {
    // Cancel pending auto-reinit — any Matter message means the device is alive
    unschedule("autoReInit")

    // Reset consecutive fail counter on any activity
    state.pingConsecutiveFails = 0

    // If device was offline, mark it back online
    if (device.currentValue("healthStatus") != "online") {
        sendEvent(name: "healthStatus", value: "online", descriptionText: "${device.displayName} is online", type: "digital")
        logInfo "Device is back online"
    }
}

void ping() {
    deviceHealthCheck()
}

void deviceHealthCheck() {
    if (enableHealthCheck == false) { return }
    logDebug "deviceHealthCheck() - sending DataModelRevision read"
    state.pingStart = now()
    List<Map<String,String>> paths = [matter.attributePath(0x00, 0x0028, 0x0000)]
    sendHubCommand(new HubAction(matter.readAttributes(paths), Protocol.MATTER))
    runIn(30, "pingTimeout")
    // Battery staleness check: if no battery report in the last 12 hours, request a fresh read
    def lastBat = device.currentState("battery")
    if (lastBat == null || (now() - lastBat.date.time) > 12 * 3600 * 1000L) {
        logWarn "No battery report in >12h — requesting battery attribute read"
        sendHubCommand(new HubAction(matter.readAttributes([matter.attributePath(0x00, 0x002F, 0x000C)]), Protocol.MATTER))
    } else {
        logDebug "Battery report is recent (last: ${lastBat.date})"
    }
}

void pingTimeout() {
    state.pingStart = null
    state.pingConsecutiveFails = (state.pingConsecutiveFails ?: 0) + 1
    if (state.stats == null) { state.stats = [:] }
    state.stats.pingFailCounter = (state.stats.pingFailCounter ?: 0) + 1
    sendEvent(name: "rtt", value: -1, unit: "ms", type: "digital", descriptionText: "Ping timeout (consecutiveFails=${state.pingConsecutiveFails})")
    logWarn "Ping timeout! consecutiveFails=${state.pingConsecutiveFails} (total pingFails=${state.stats.pingFailCounter})"
    if (state.pingConsecutiveFails >= 2) {
        sendEvent(name: "healthStatus", value: "offline", descriptionText: "${device.displayName} is offline", type: "digital")
        logWarn "Device is OFFLINE after ${state.pingConsecutiveFails} consecutive ping failures"
        if (enableAutoReInit != false) {
            logWarn "Auto re-init scheduled in 30 seconds"
            runIn(30, "autoReInit")
        }
    }
}

void autoReInit() {
    if (state.stats == null) { state.stats = [:] }
    state.stats.autoReInitCounter = (state.stats.autoReInitCounter ?: 0) + 1
    logWarn "Auto re-initializing after failed health checks (autoReInitCounter=${state.stats.autoReInitCounter})"
    initialize()
}

// Logging helpers — prefix all messages with device display name
private void logDebug(String msg) {
    if (logEnable) { log.debug "${device.displayName} ${msg}" }
}

private void logInfo(String msg) {
    if (txtEnable) { log.info "${device.displayName} ${msg}" }
}

private void logWarn(String msg) {
    log.warn "${device.displayName} ${msg}"
}


image


Code is avaialbe on GitHub : https://raw.githubusercontent.com/kkossev/Hubitat/development/Drivers/Ikea%20Matter/Matter%20Thread%20Diagnostic.groovy


The device exposes an interesting Matter cluster (0x0080 – Boolean State Configuration), but exploring or making use of it is outside the scope of this minimalistic implementation :slight_smile:

This ends the set of 'easy' drivers : IKEA MYGGBETT Matter Door/Window Sensor :


/*
 * IKEA MYGGBETT Matter Door/Window Sensor
 *
 * https://community.hubitat.com/t/what-do-i-need-at-ikea/158182/76?u=kkossev
 *
 * Last edited: 2026/05/14 5:03 PM
 */

import hubitat.device.HubAction
import hubitat.device.Protocol

metadata {
    definition(name: "IKEA MYGGBETT Matter Door/Window Sensor w/ healthStatus", namespace: "community", author: "kkossev + ChatGPT + Claude", singleThreaded: true, importUrl: "https://raw.githubusercontent.com/kkossev/Hubitat/development/Drivers/Ikea%20Matter/IKEA%20MYGGBETT%20Matter%20Door%20Window%20Sensor.groovy") {

        capability "Sensor"
        capability "ContactSensor"
        capability "Battery"
        capability "Refresh"
        capability "Initialize"
        capability "HealthCheck"

        attribute "healthStatus", "enum", ["online", "offline"]
        attribute "rtt", "number"

        fingerprint endpointId: "01",
                inClusters: "0003,001D,0045",
                outClusters: "",
                model: "MYGGBETT door/window sensor",
                manufacturer: "IKEA of Sweden",
                controllerType: "MAT"
    }

    preferences {
        input name: "txtEnable",         type: "bool", title: "Enable descriptionText logging",                              defaultValue: true
        input name: "logEnable",         type: "bool", title: "Enable debug logging",                                       defaultValue: false
        input name: "enableHealthCheck", type: "bool", title: "Enable health check (ping every 5 min)",                     defaultValue: true
        input name: "enableAutoReInit",  type: "bool", title: "Auto re-initialize after 2 consecutive ping failures",       defaultValue: true
    }
}

void installed() {
    state.stats = [initializeCounter: 0, pingFailCounter: 0, autoReInitCounter: 0]
    initialize()
}

void updated() {
    logInfo "updated..."
    if (logEnable) runIn(7200, "logsOff")
    initialize()
}

void logsOff() {
    device.updateSetting("logEnable", [value: "false", type: "bool"])
    logWarn("Debug logging disabled")
}

void initialize() {
    if (state.stats == null) { state.stats = [initializeCounter: 0, pingFailCounter: 0, autoReInitCounter: 0] }
    state.stats.initializeCounter = (state.stats.initializeCounter ?: 0) + 1
    unschedule("deviceHealthCheck")
    unschedule("pingTimeout")
    unschedule("autoReInit")
    state.pingStart = null
    state.pingConsecutiveFails = 0
    state.lastInitializeTime = now()
    if (getDataValue("newParse") != "true") { device.updateDataValue("newParse", "true") }
    logInfo "initialize... (initializeCounter=${state.stats.initializeCounter})"
    logInfo "model=${device.getDataValue('model') ?: device.model} newParse=${getDataValue("newParse")} uptime=${location.hub.uptime}"
    subscribeToAttributes()
    refresh()
    if (enableHealthCheck != false) { runEvery5Minutes("deviceHealthCheck") }
}

void refresh() {
    logDebug "refresh()"
    state.lastRefreshTime = now()

    List<Map<String,String>> paths = []

    // Contact state (cluster 0x0045 attr 0x0000 => boolean)
    paths.add(matter.attributePath(0x01, 0x0045, 0x0000))

    // Battery (endpoint 0, Power Source cluster 0x002F, attr 0x000C)
    paths.add(matter.attributePath(0x00, 0x002F, 0x000C))

    // Software version string (Basic Information cluster 0x0028, attr 0x000A)
    paths.add(matter.attributePath(0x00, 0x0028, 0x000A))

    String cmd = matter.readAttributes(paths)
    sendHubCommand(new HubAction(cmd, Protocol.MATTER))
}

private void subscribeToAttributes() {
    List<Map<String,String>> paths = []
    paths.add(matter.attributePath(0x01, 0x0045, 0x0000))
    paths.add(matter.attributePath(0x00, 0x002F, 0x000C))

    String cmd = matter.cleanSubscribe(0, 600, paths)
    sendHubCommand(new HubAction(cmd, Protocol.MATTER))

    logInfo "subscribing to contact (EP1/0x0045) + battery (EP0/0x002F/0x000C)"
}

void parse(String description) {
    logDebug "parse(String) called - ignored (newParse:true mode only)"
}

// parse(Map) - newParse:true format only
// Attribute report : [callbackType:Report, endpointInt:1, clusterInt:69,  attrInt:0,  value:0]
// Battery report   : [callbackType:Report, endpointInt:0, clusterInt:47,  attrInt:12, value:200]
void parse(Map msg) {
    logDebug "parse(Map) received: ${msg}"
    handleLiveness(msg)

    // Ping response (explicit) or implicit ping success (any msg while ping in-flight)
    if (state.pingStart != null) {
        unschedule("pingTimeout")
        Long rtt = now() - (state.pingStart as Long)
        if (msg.clusterInt == 0x0028 && msg.attrInt == 0x0000) {
            sendEvent(name: "rtt", value: rtt, unit: "ms", type: "digital", descriptionText: "Ping round-trip time: ${rtt} ms")
            logInfo "Ping RTT: ${rtt} ms"
            state.pingStart = null
            return   // ping response fully handled
        }
        logDebug "Implicit ping success (msg arrived while ping in-flight), RTT: ${rtt} ms"
        state.pingStart = null
    }

    // Contact state: EP1 cluster 0x0045 attr 0x0000 (BooleanState; value 0=open, 1=closed, or boolean false/true)
    if (msg.endpointInt == 0x01 && msg.clusterInt == 0x0045 && msg.attrInt == 0x0000) {
        Integer v = safeInt(msg.value)
        if (v != null) {
            String contact = (v == 0) ? "open" : "closed"
            boolean isInit = state.lastInitializeTime ? (now() - state.lastInitializeTime <= 15000) : false
            boolean isRef = state.lastRefreshTime ? (now() - state.lastRefreshTime <= 15000) : false
            def sfx = (isInit ? " [initialize]" : "") + (isRef ? " [refresh]" : "")
            String prev = device.currentValue('contact')
            String txt = (prev != null && prev.toString() == contact) ? "Contact is ${contact}${sfx}" : "Contact was ${contact}${sfx}"
            // for contact, do NOT force isStateChange=true to avoid apps retriggering on same state
            sendEvent(name: "contact", value: contact, descriptionText: txtEnable ? txt : null, type: "physical")
            logInfo txt
        }
        return
    }

    // Battery: EP0 cluster 0x002F attr 0x000C (raw 0..200)
    if (msg.endpointInt == 0x00 && msg.clusterInt == 0x002F && msg.attrInt == 0x000C) {
        Integer raw = safeInt(msg.value)
        if (raw != null) {
            Integer pct = Math.round(raw / 2.0f)
            pct = Math.max(0, Math.min(100, pct))
            boolean isInit = state.lastInitializeTime ? (now() - state.lastInitializeTime <= 15000) : false
            boolean isRef = state.lastRefreshTime ? (now() - state.lastRefreshTime <= 15000) : false
            def sfx = (isInit ? " [initialize]" : "") + (isRef ? " [refresh]" : "")
            def contactVal = device.currentValue('contact')
            logDebug "Battery report received; current contact state is ${contactVal}"
            if (sfx == "") sfx = " (contact is ${contactVal})"
            // for battery, we can safely mark isStateChange true during init/refresh without causing issues in apps
            sendEvent(name: "battery", value: pct, unit: "%", descriptionText: "Battery is ${pct}%${sfx}", isStateChange: true, type: "physical")
            logInfo "Battery is ${pct}%${sfx}"
        }
        return
    }

    // Software version string (Basic Information cluster 0x0028, attr 0x000A)
    if (msg.clusterInt == 0x0028 && msg.attrInt == 0x000A) {
        String ver = msg.value?.toString() ?: ""
        device.updateDataValue("softwareVersion", ver)
        logInfo "softwareVersion=${ver}"
        return
    }

    logDebug "parse(Map): unhandled msg: ${msg}"
}

private Integer safeInt(def v) {
    if (v == null) return null
    if (v instanceof Boolean) return v ? 1 : 0
    try { return Integer.parseInt(v.toString(), 10) } catch (Exception ignored) { return null }
}

/* ---------- health check ---------- */

private void handleLiveness(Map msg) {
    // Cancel pending auto-reinit — any Matter message means the device is alive
    unschedule("autoReInit")

    // Reset consecutive fail counter on any activity
    state.pingConsecutiveFails = 0

    // If device is not yet online (null on first boot) or was offline, mark it online
    if (device.currentValue("healthStatus") != "online") {
        sendEvent(name: "healthStatus", value: "online", descriptionText: "${device.displayName} is online", type: "digital")
        logInfo "Device is back online"
    }
}

void ping() {
    deviceHealthCheck()
}

void deviceHealthCheck() {
    if (enableHealthCheck == false) { return }
    logDebug "deviceHealthCheck() - sending DataModelRevision read"
    state.pingStart = now()
    List<Map<String,String>> paths = [matter.attributePath(0x00, 0x0028, 0x0000)]
    sendHubCommand(new HubAction(matter.readAttributes(paths), Protocol.MATTER))
    runIn(30, "pingTimeout")
    // Battery staleness check: if no battery report in the last 12 hours, request a fresh read
    def lastBat = device.currentState("battery")
    if (lastBat == null || (now() - lastBat.date.time) > 12 * 3600 * 1000L) {
        logWarn "No battery report in >12h — requesting battery attribute read"
        sendHubCommand(new HubAction(matter.readAttributes([matter.attributePath(0x00, 0x002F, 0x000C)]), Protocol.MATTER))
    } else {
        logDebug "Battery report is recent (last: ${lastBat.date})"
    }
}

void pingTimeout() {
    state.pingStart = null
    state.pingConsecutiveFails = (state.pingConsecutiveFails ?: 0) + 1
    if (state.stats == null) { state.stats = [:] }
    state.stats.pingFailCounter = (state.stats.pingFailCounter ?: 0) + 1
    sendEvent(name: "rtt", value: -1, unit: "ms", type: "digital", descriptionText: "Ping timeout (consecutiveFails=${state.pingConsecutiveFails})")
    logWarn "Ping timeout! consecutiveFails=${state.pingConsecutiveFails} (total pingFails=${state.stats.pingFailCounter})"
    if (state.pingConsecutiveFails >= 2) {
        sendEvent(name: "healthStatus", value: "offline", descriptionText: "${device.displayName} is offline", type: "digital")
        logWarn "Device is OFFLINE after ${state.pingConsecutiveFails} consecutive ping failures"
        if (enableAutoReInit != false) {
            logWarn "Auto re-init scheduled in 30 seconds"
            runIn(30, "autoReInit")
        }
    }
}

void autoReInit() {
    if (state.stats == null) { state.stats = [:] }
    state.stats.autoReInitCounter = (state.stats.autoReInitCounter ?: 0) + 1
    logWarn "Auto re-initializing after failed health checks (autoReInitCounter=${state.stats.autoReInitCounter})"
    initialize()
}

// Logging helpers — prefix all messages with device display name
private void logDebug(String msg) { 
    if (logEnable) { log.debug "${device.displayName} ${msg}" }
}

private void logInfo(String msg) {
    if (txtEnable) { log.info "${device.displayName} ${msg}" }
}

private void logWarn(String msg) {
    log.warn "${device.displayName} ${msg}"
}



The code is avaialble on GitHub : https://raw.githubusercontent.com/kkossev/Hubitat/development/Drivers/Ikea%20Matter/IKEA%20MYGGBETT%20Matter%20Door%20Window%20Sensor.groovy


image

Could we please have drivers for IKEA BILRESA button?

I’m working on it — single press is already functioning correctly, but double-press and long-press aren’t reporting yet. Still investigating how to expose them through Matter/Hubitat.

It’s a little bit outside of the thread topic, so this post can be broken out if necessary, and you probably already know this, but have you looked into wildcard subscriptions specifically for endpoints and attributes, but leave the specific clusters alone? Something like:


(endpoint = *, cluster = 0x0406, attribute = *)
(endpoint = *, cluster = 0x0400, attribute = *)
(endpoint = *, cluster = 0x002F, attribute = *)

This would allow the generic built-in drivers be narrowly tailored and device specific or community drivers cover the full possibilities of the device.

Using a wildcard subscription may not be the best approach, it generates excessive traffic.... : (

Wildcard events :(

dev:78352025-12-24 22:32:09.996

debug

BILRESA dual button IKEA Matter parse(Map): [callbackType:Report, endpointInt:0, clusterInt:47, attrInt:12, data:[12:UINT:200], value:200]

dev:78352025-12-24 22:31:18.913

debug

BILRESA dual button IKEA Matter parse(Map): [callbackType:Report, endpointInt:1, clusterInt:59, attrInt:1, data:[1:UINT:0], value:0]

dev:78352025-12-24 22:31:18.750

debug

BILRESA dual button IKEA Matter parse(Map): [callbackType:Report, endpointInt:1, clusterInt:59, attrInt:1, data:[1:UINT:0], value:0]

dev:78352025-12-24 22:31:18.730

debug

BILRESA dual button IKEA Matter parse(Map): [callbackType:Report, endpointInt:1, clusterInt:59, attrInt:1, data:[1:UINT:0], value:0]

dev:78352025-12-24 22:31:17.843

debug

BILRESA dual button IKEA Matter parse(Map): [callbackType:Report, endpointInt:1, clusterInt:59, attrInt:1, data:[1:UINT:1], value:1]

dev:78352025-12-24 22:31:17.705

debug

BILRESA dual button IKEA Matter parse(Map): [callbackType:Report, endpointInt:1, clusterInt:59, attrInt:1, data:[1:UINT:1], value:1]

dev:78352025-12-24 22:31:17.695

debug

BILRESA dual button IKEA Matter parse(Map): [callbackType:Report, endpointInt:1, clusterInt:59, attrInt:1, data:[1:UINT:1], value:1]

dev:78352025-12-24 22:31:15.138

debug

BILRESA dual button IKEA Matter parse(Map): [callbackType:Report, endpointInt:2, clusterInt:59, attrInt:65533, data:[65533:UINT:1], value:1]

dev:78352025-12-24 22:31:15.134

debug

BILRESA dual button IKEA Matter parse(Map): [callbackType:Report, endpointInt:2, clusterInt:59, attrInt:65532, data:[65532:UINT:30], value:30]

dev:78352025-12-24 22:31:15.129

debug

BILRESA dual button IKEA Matter parse(Map): [callbackType:Report, endpointInt:2, clusterInt:59, attrInt:65531, data:[65531:ARRAY-UINT:[0, 1, 2, 65528, 65529, 65531, 65532, 65533]], value:[0, 1, 2, 65528, 65529, 65531, 65532, 65533]]

dev:78352025-12-24 22:31:15.124

debug

BILRESA dual button IKEA Matter parse(Map): [callbackType:Report, endpointInt:2, clusterInt:59, attrInt:65529, data:[65529:ARRAY-?:], value:]

dev:78352025-12-24 22:31:15.120

debug

BILRESA dual button IKEA Matter parse(Map): [callbackType:Report, endpointInt:2, clusterInt:59, attrInt:65528, data:[65528:ARRAY-?:], value:]

dev:78352025-12-24 22:31:15.115

debug

BILRESA dual button IKEA Matter parse(Map): [callbackType:Report, endpointInt:2, clusterInt:59, attrInt:2, data:[2:UINT:2], value:2]

dev:78352025-12-24 22:31:15.111

debug

BILRESA dual button IKEA Matter parse(Map): [callbackType:Report, endpointInt:2, clusterInt:59, attrInt:1, data:[1:UINT:0], value:0]

dev:78352025-12-24 22:31:15.107

debug

BILRESA dual button IKEA Matter parse(Map): [callbackType:Report, endpointInt:2, clusterInt:59, attrInt:0, data:[0:UINT:2], value:2]

dev:78352025-12-24 22:31:15.102

debug

BILRESA dual button IKEA Matter parse(Map): [callbackType:Report, endpointInt:1, clusterInt:59, attrInt:65533, data:[65533:UINT:1], value:1]

dev:78352025-12-24 22:31:15.097

debug

BILRESA dual button IKEA Matter parse(Map): [callbackType:Report, endpointInt:1, clusterInt:59, attrInt:65532, data:[65532:UINT:30], value:30]

dev:78352025-12-24 22:31:15.093

debug

BILRESA dual button IKEA Matter parse(Map): [callbackType:Report, endpointInt:1, clusterInt:59, attrInt:65531, data:[65531:ARRAY-UINT:[0, 1, 2, 65528, 65529, 65531, 65532, 65533]], value:[0, 1, 2, 65528, 65529, 65531, 65532, 65533]]

dev:78352025-12-24 22:31:15.088

debug

BILRESA dual button IKEA Matter parse(Map): [callbackType:Report, endpointInt:1, clusterInt:59, attrInt:65529, data:[65529:ARRAY-?:], value:]

dev:78352025-12-24 22:31:15.083

debug

BILRESA dual button IKEA Matter parse(Map): [callbackType:Report, endpointInt:1, clusterInt:59, attrInt:65528, data:[65528:ARRAY-?:], value:]

dev:78352025-12-24 22:31:15.078

debug

BILRESA dual button IKEA Matter parse(Map): [callbackType:Report, endpointInt:1, clusterInt:59, attrInt:2, data:[2:UINT:2], value:2]

dev:78352025-12-24 22:31:15.074

debug

BILRESA dual button IKEA Matter parse(Map): [callbackType:Report, endpointInt:1, clusterInt:59, attrInt:1, data:[1:UINT:0], value:0]

dev:78352025-12-24 22:31:15.069

debug

BILRESA dual button IKEA Matter parse(Map): [callbackType:Report, endpointInt:1, clusterInt:59, attrInt:0, data:[0:UINT:2], value:2]

dev:78352025-12-24 22:31:15.064

debug

BILRESA dual button IKEA Matter parse(Map): [callbackType:Report, endpointInt:0, clusterInt:49, attrInt:65533, data:[65533:UINT:2], value:2]

dev:78352025-12-24 22:31:15.026

debug

BILRESA dual button IKEA Matter parse(Map): [callbackType:Report, endpointInt:0, clusterInt:49, attrInt:65532, data:[65532:UINT:2], value:2]

dev:78352025-12-24 22:31:15.021

debug

BILRESA dual button IKEA Matter parse(Map): [callbackType:Report, endpointInt:0, clusterInt:49, attrInt:65531, data:[65531:ARRAY-UINT:[0, 1, 2, 3, 4, 5, 6, 7, 9, 10, 65528, 65529, 65531, 65532, 65533]], value:[0, 1, 2, 3, 4, 5, 6, 7, 9, 10, 65528, 65529, 65531, 65532, 65533]]

dev:78352025-12-24 22:31:15.017

debug

BILRESA dual button IKEA Matter parse(Map): [callbackType:Report, endpointInt:0, clusterInt:49, attrInt:65529, data:[65529:ARRAY-UINT:[0, 3, 4, 6, 8]], value:[0, 3, 4, 6, 8]]

dev:78352025-12-24 22:31:15.012

debug

BILRESA dual button IKEA Matter parse(Map): [callbackType:Report, endpointInt:0, clusterInt:49, attrInt:65528, data:[65528:ARRAY-UINT:[1, 5, 7]], value:[1, 5, 7]]

dev:78352025-12-24 22:31:15.006

debug

BILRESA dual button IKEA Matter parse(Map): [callbackType:Report, endpointInt:0, clusterInt:49, attrInt:10, data:[10:UINT:4], value:4]

dev:78352025-12-24 22:31:15.002

debug

BILRESA dual button IKEA Matter parse(Map): [callbackType:Report, endpointInt:0, clusterInt:49, attrInt:9, data:[9:UINT:4], value:4]

dev:78352025-12-24 22:31:14.997

debug

BILRESA dual button IKEA Matter parse(Map): [callbackType:Report, endpointInt:0, clusterInt:49, attrInt:7, data:[7:NULL:null]]

dev:78352025-12-24 22:31:14.992

debug

BILRESA dual button IKEA Matter parse(Map): [callbackType:Report, endpointInt:0, clusterInt:49, attrInt:6, data:[6:BYTES:mCsgOyUoSRc=]]

dev:78352025-12-24 22:31:14.987

debug

BILRESA dual button IKEA Matter parse(Map): [callbackType:Report, endpointInt:0, clusterInt:49, attrInt:5, data:[5:UINT:0], value:0]

dev:78352025-12-24 22:31:14.982

debug

BILRESA dual button IKEA Matter parse(Map): [callbackType:Report, endpointInt:0, clusterInt:49, attrInt:4, data:[4:BOOL:true], value:true]

dev:78352025-12-24 22:31:14.978

debug

BILRESA dual button IKEA Matter parse(Map): [callbackType:Report, endpointInt:0, clusterInt:49, attrInt:3, data:[3:UINT:20], value:20]

dev:78352025-12-24 22:31:14.973

debug

BILRESA dual button IKEA Matter parse(Map): [callbackType:Report, endpointInt:0, clusterInt:49, attrInt:2, data:[2:UINT:10], value:10]

dev:78352025-12-24 22:31:14.969

debug

BILRESA dual button IKEA Matter parse(Map): [callbackType:Report, endpointInt:0, clusterInt:49, attrInt:1, data:[1:ARRAY-STRUCT:[[0:BYTES:mCsgOyUoSRc=, 1:BOOL:true]]], value:[[[tag:0, value:mCsgOyUoSRc=], [tag:1, value:true]]]]

dev:78352025-12-24 22:31:14.963

debug

BILRESA dual button IKEA Matter parse(Map): [callbackType:Report, endpointInt:0, clusterInt:49, attrInt:0, data:[0:UINT:1], value:1]

dev:78352025-12-24 22:31:14.939

debug

BILRESA dual button IKEA Matter parse(Map): [callbackType:Report, endpointInt:2, clusterInt:59, attrInt:65533, data:[65533:UINT:1], value:1]

dev:78352025-12-24 22:31:14.935

debug

BILRESA dual button IKEA Matter parse(Map): [callbackType:Report, endpointInt:2, clusterInt:59, attrInt:65532, data:[65532:UINT:30], value:30]

dev:78352025-12-24 22:31:14.930

debug

BILRESA dual button IKEA Matter parse(Map): [callbackType:Report, endpointInt:2, clusterInt:59, attrInt:65531, data:[65531:ARRAY-UINT:[0, 1, 2, 65528, 65529, 65531, 65532, 65533]], value:[0, 1, 2, 65528, 65529, 65531, 65532, 65533]]

dev:78352025-12-24 22:31:14.926

debug

BILRESA dual button IKEA Matter parse(Map): [callbackType:Report, endpointInt:2, clusterInt:59, attrInt:65529, data:[65529:ARRAY-?:], value:]

dev:78352025-12-24 22:31:14.921

debug

BILRESA dual button IKEA Matter parse(Map): [callbackType:Report, endpointInt:2, clusterInt:59, attrInt:65528, data:[65528:ARRAY-?:], value:]

dev:78352025-12-24 22:31:14.916

debug

BILRESA dual button IKEA Matter parse(Map): [callbackType:Report, endpointInt:2, clusterInt:59, attrInt:2, data:[2:UINT:2], value:2]

dev:78352025-12-24 22:31:14.912

debug

BILRESA dual button IKEA Matter parse(Map): [callbackType:Report, endpointInt:2, clusterInt:59, attrInt:1, data:[1:UINT:0], value:0]

dev:78352025-12-24 22:31:14.907

debug

BILRESA dual button IKEA Matter parse(Map): [callbackType:Report, endpointInt:2, clusterInt:59, attrInt:0, data:[0:UINT:2], value:2]

dev:78352025-12-24 22:31:14.902

debug

BILRESA dual button IKEA Matter parse(Map): [callbackType:Report, endpointInt:1, clusterInt:59, attrInt:65533, data:[65533:UINT:1], value:1]

dev:78352025-12-24 22:31:14.898

debug

BILRESA dual button IKEA Matter parse(Map): [callbackType:Report, endpointInt:1, clusterInt:59, attrInt:65532, data:[65532:UINT:30], value:30]

dev:78352025-12-24 22:31:14.894

debug

BILRESA dual button IKEA Matter parse(Map): [callbackType:Report, endpointInt:1, clusterInt:59, attrInt:65531, data:[65531:ARRAY-UINT:[0, 1, 2, 65528, 65529, 65531, 65532, 65533]], value:[0, 1, 2, 65528, 65529, 65531, 65532, 65533]]

dev:78352025-12-24 22:31:14.889

debug

BILRESA dual button IKEA Matter parse(Map): [callbackType:Report, endpointInt:1, clusterInt:59, attrInt:65529, data:[65529:ARRAY-?:], value:]

dev:78352025-12-24 22:31:14.885

debug

BILRESA dual button IKEA Matter parse(Map): [callbackType:Report, endpointInt:1, clusterInt:59, attrInt:65528, data:[65528:ARRAY-?:], value:]

dev:78352025-12-24 22:31:14.880

debug

BILRESA dual button IKEA Matter parse(Map): [callbackType:Report, endpointInt:1, clusterInt:59, attrInt:2, data:[2:UINT:2], value:2]

dev:78352025-12-24 22:31:14.875

debug

BILRESA dual button IKEA Matter parse(Map): [callbackType:Report, endpointInt:1, clusterInt:59, attrInt:1, data:[1:UINT:0], value:0]

dev:78352025-12-24 22:31:14.871

debug

BILRESA dual button IKEA Matter parse(Map): [callbackType:Report, endpointInt:1, clusterInt:59, attrInt:0, data:[0:UINT:2], value:2]

dev:78352025-12-24 22:31:14.866

debug

BILRESA dual button IKEA Matter parse(Map): [callbackType:Report, endpointInt:0, clusterInt:49, attrInt:65533, data:[65533:UINT:2], value:2]

dev:78352025-12-24 22:31:14.862

debug

BILRESA dual button IKEA Matter parse(Map): [callbackType:Report, endpointInt:0, clusterInt:49, attrInt:65532, data:[65532:UINT:2], value:2]

dev:78352025-12-24 22:31:14.856

debug

BILRESA dual button IKEA Matter parse(Map): [callbackType:Report, endpointInt:0, clusterInt:49, attrInt:65531, data:[65531:ARRAY-UINT:[0, 1, 2, 3, 4, 5, 6, 7, 9, 10, 65528, 65529, 65531, 65532, 65533]], value:[0, 1, 2, 3, 4, 5, 6, 7, 9, 10, 65528, 65529, 65531, 65532, 65533]]

dev:78352025-12-24 22:31:14.847

debug

BILRESA dual button IKEA Matter parse(Map): [callbackType:Report, endpointInt:0, clusterInt:49, attrInt:65529, data:[65529:ARRAY-UINT:[0, 3, 4, 6, 8]], value:[0, 3, 4, 6, 8]]

dev:78352025-12-24 22:31:14.843

debug

BILRESA dual button IKEA Matter parse(Map): [callbackType:Report, endpointInt:0, clusterInt:49, attrInt:65528, data:[65528:ARRAY-UINT:[1, 5, 7]], value:[1, 5, 7]]

dev:78352025-12-24 22:31:14.766

debug

BILRESA dual button IKEA Matter parse(Map): [callbackType:Report, endpointInt:0, clusterInt:49, attrInt:10, data:[10:UINT:4], value:4]

dev:78352025-12-24 22:31:14.761

debug

BILRESA dual button IKEA Matter parse(Map): [callbackType:Report, endpointInt:0, clusterInt:49, attrInt:9, data:[9:UINT:4], value:4]

dev:78352025-12-24 22:31:14.756

debug

BILRESA dual button IKEA Matter parse(Map): [callbackType:Report, endpointInt:0, clusterInt:49, attrInt:7, data:[7:NULL:null]]

dev:78352025-12-24 22:31:14.751

debug

BILRESA dual button IKEA Matter parse(Map): [callbackType:Report, endpointInt:0, clusterInt:49, attrInt:6, data:[6:BYTES:mCsgOyUoSRc=]]

dev:78352025-12-24 22:31:14.746

debug

BILRESA dual button IKEA Matter parse(Map): [callbackType:Report, endpointInt:0, clusterInt:49, attrInt:5, data:[5:UINT:0], value:0]

dev:78352025-12-24 22:31:14.742

debug

BILRESA dual button IKEA Matter parse(Map): [callbackType:Report, endpointInt:0, clusterInt:49, attrInt:4, data:[4:BOOL:true], value:true]

dev:78352025-12-24 22:31:14.737

debug

BILRESA dual button IKEA Matter parse(Map): [callbackType:Report, endpointInt:0, clusterInt:49, attrInt:3, data:[3:UINT:20], value:20]

dev:78352025-12-24 22:31:14.732

debug

BILRESA dual button IKEA Matter parse(Map): [callbackType:Report, endpointInt:0, clusterInt:49, attrInt:2, data:[2:UINT:10], value:10]

dev:78352025-12-24 22:31:14.728

debug

BILRESA dual button IKEA Matter parse(Map): [callbackType:Report, endpointInt:0, clusterInt:49, attrInt:1, data:[1:ARRAY-STRUCT:[[0:BYTES:mCsgOyUoSRc=, 1:BOOL:true]]], value:[[[tag:0, value:mCsgOyUoSRc=], [tag:1, value:true]]]]

dev:78352025-12-24 22:31:14.721

debug

BILRESA dual button IKEA Matter parse(Map): [callbackType:Report, endpointInt:0, clusterInt:49, attrInt:0, data:[0:UINT:1], value:1]

dev:78352025-12-24 22:31:14.612

debug

BILRESA dual button IKEA Matter parse(Map): [callbackType:Report, endpointInt:2, clusterInt:59, attrInt:1, data:[1:UINT:0], value:0]

dev:78352025-12-24 22:31:14.607

debug

BILRESA dual button IKEA Matter parse(Map): [callbackType:Report, endpointInt:1, clusterInt:59, attrInt:1, data:[1:UINT:0], value:0]

dev:78352025-12-24 22:31:14.599

info

BILRESA dual button IKEA Matter Battery is 100%

dev:78352025-12-24 22:31:14.595

debug

BILRESA dual button IKEA Matter parse(Map): [callbackType:Report, endpointInt:0, clusterInt:47, attrInt:12, data:[12:UINT:200], value:200]