What do I need at Ikea?

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

/*
 * IKEA MYGGSPRAY Matter Motion + Illuminance + Battery (minimal)
 *
 * Last edited: 2025/12/24 10:21 AM
 */

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

metadata {
    definition(name: "IKEA MYGGSPRAY Matter Motion+Lux+Bat", namespace: "community", author: "kkossev + ChatGPT :)") {
        capability "Sensor"
        capability "MotionSensor"
        capability "IlluminanceMeasurement"
        capability "Refresh"
        capability "Initialize"
        capability "Battery"
    }
    preferences {
        input name: "txtEnable", type: "bool", title: "Enable descriptionText logging", defaultValue: true
        input name: "logEnable", type: "bool", title: "Enable debug logging", defaultValue: false
    }
}

void installed() {
    if (logEnable) log.debug "installed()"
    initialize()
}

void updated() {
    if (logEnable) log.debug "updated()"
    if (logEnable) runIn(1800, "logsOff")
    initialize()
}

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

void initialize() {
    if (logEnable) log.debug "initialize()"
    subscribeToAttributes()
    refresh()
}

void refresh() {
    // Read current values (optional but handy after pairing/restart)
    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
    
    String cmd = matter.readAttributes(paths)
    sendHubCommand(new HubAction(cmd, Protocol.MATTER))
}

private void subscribeToAttributes() {
    // Keep it as ONE subscription request with multiple attribute paths.
    // Many devices support only a few subscriptions per fabric. :contentReference[oaicite:5]{index=5}
    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

    // minReportTime=1s, maxReportTime=0xFFFF (device chooses / periodic refresh)
    String cmd = matter.cleanSubscribe(1, 0xFFFF, paths)
    sendHubCommand(new HubAction(cmd, Protocol.MATTER))

    if (txtEnable) log.info "Subscribed to MYGGSPRAY motion + illuminance + battery"
}

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

    Map msg = matter.parseDescriptionAsMap(description)  // :contentReference[oaicite:6]{index=6}
    if (!msg) return

    // Hubitat’s parsed map keys can vary by message type; we handle common patterns.
    // Typical fields you may see: endpoint, cluster, attrId, value
    Integer ep     = safeHexToInt(msg.endpoint)
    Integer clus   = safeHexToInt(msg.cluster)
    Integer attrId = safeHexToInt(msg.attrId)
    String  value  = msg.value?.toString()

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

    // Illuminance Measurement: cluster 0x0400 attr 0x0000 (MeasuredValue)
    if (ep == 0x01 && clus == 0x0400 && attrId == 0x0000) {
        Integer raw = safeHexToInt(value)
        if (raw != null) {
            // Matter Illuminance MeasuredValue is in lux * 100
            Integer lux = raw / 100.0
            String descText = "Illuminance is ${lux} lx"
            sendEvent(name: "illuminance", value: lux, unit: "lx", descriptionText: txtEnable ? descText : null)
            if (txtEnable) { log.info descText }
        }
        return
    }

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

    // Power Source (Battery): cluster 0x002F attr 0x000C
    if (ep == 0x00 && clus == 0x002F && attrId == 0x000C) {
        Integer pct = safeHexToInt(value) / 2
        if (pct != null) {
            // Device is already reporting percentage as an integer (e.g. 0x46 -> 70)
            pct = Math.max(0, Math.min(100, pct))
            sendEvent(name: "battery", value: pct, unit: "%")
            if (txtEnable) log.info "Battery is ${pct}%"
        }
        return
    }
    
    if (logEnable) log.debug "Unhandled Matter report: ${msg}"
}

private Integer safeHexToInt(Object hex) {
    if (hex == null) return null
    String s = hex.toString().trim()
    if (s.startsWith("0x") || s.startsWith("0X")) s = s.substring(2)
    if (s == "") return null
    try { return Integer.parseUnsignedInt(s, 16) } catch (Exception ignored) { return null }
}



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

4 Likes