[RELEASE] Advanced Driver for Aqara Door/Window Sensor T1 (lumi.magnet.agl02)

I recently came across an Aqara Door & Window sensor T1
It seems to be an older device and none of the Xiaomi or Aqara drivers worked so I threw together one with help of chatgpt.

I’m sharing a fully featured custom Hubitat Zigbee driver for the Aqara Door/Window Sensor (lumi.magnet.agl02), built to handle all of the quirks this device has — including its tendency to drop off, ignore refresh commands, and require special enrollment.

This driver includes not just open/close detection, but enhanced battery monitoring, duration tracking, and timestamps for meaningful event history.

Feature Description
:arrows_clockwise: Open/Close Detection Real-time detection via IAS Zone (Cluster 0x0500)
:battery: Battery % Reporting Auto-calculated from voltage (Cluster 0x0001 Attr 0x0020)
:zap: Raw Battery Voltage Separate batteryVoltage attribute (e.g., 3.0V)
:clock4: lastOpened / lastClosed Human-readable timestamps
:date: lastContactChange Last time the contact changed (open or closed)
:stopwatch: contactDuration Seconds the device stayed in its previous state
:calendar: lastBatteryReport When the battery was last updated
:hammer_and_wrench: Manual IAS Enrollment Ensures open/close reporting works reliably
:open_book: Debug Logging Toggleable in Preferences

:hammer_and_wrench: How to Use

  1. Install the Driver
  • Go to Hubitat → Drivers Code
  • Click + New Driver
  • Paste the driver code from [GitHub Gist or this post]
  • Save
  1. Assign the Driver
  • Go to your Devices list
  • Open your lumi.magnet.agl02 device
  • In the "Type" dropdown, select Aqara Door/Window Sensor Advanced
  • Click Save
  1. Click Configure
  • This sends the custom IAS Zone enrollment required by Aqara devices
  1. Click Refresh
  • This attempts to manually read battery and contact status
  • :warning: Aqara often ignores manual refresh — see quirks below

:dna: Aqara Quirks You Should Know

Aqara sensors use a non-standard Zigbee implementation:

  • :x: They don’t reliably respond to readAttribute() commands for battery
  • :x: They may fall off the network if polled too frequently
  • :white_check_mark: They do report battery voltage when:
    • You press the physical button on the device
    • Sometimes automatically every ~6–12 hours

That’s why we track lastBatteryReport, so you can monitor how stale the battery data is.


:test_tube: Recommended Rules or Automations

1. Alert if battery hasn't reported in 48 hours
Use Rule Machine with:

  • Condition: lastBatteryReport is older than 48 hours
  • Action: Send push/email notification

2. Trigger automations with contactDuration
Examples:

  • "If contact has been open for more than 300 seconds, send alert"
  • "If door just closed and contactDuration > 60, log a ‘long open’ event"
Attribute Type Notes
contact open / closed Core
battery Percentage (%) Estimated
batteryVoltage Float (e.g., 2.9V) Raw
lastOpened Timestamp "2025-05-03 13:25:00"
lastClosed Timestamp ""
lastContactChange Timestamp When either open or close occurred
contactDuration Integer (seconds) Duration of previous state
lastBatteryReport Timestamp When battery was last updated

:paperclip: Download the Driver

You can find the driver source here:
:page_facing_up: [https://gist.githubusercontent.com/enishoca/44ad9c0bf75e50c6ae311db196c688de/raw/702be985ca7b919bbc0e85eeca4ffc0bbb141e67/AqaraDoorWindowSensorT1.groovy]

/**
 *  Aqara Door/Window Sensor T1
 *
 * https://gist.githubusercontent.com/enishoca/44ad9c0bf75e50c6ae311db196c688de/raw/702be985ca7b919bbc0e85eeca4ffc0bbb141e67/AqaraDoorWindowSensorT1.groovy
 *  
 *
 *  Copyright 2025 Enis Hoca
 *
 *  Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 *  in compliance with the License. You may obtain a copy of the License at:
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
 *  on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
 *  for the specific language governing permissions and limitations under the License.
 *
 *  Change History:
 *
 *    Date        Who            What
 *    ----        ---            ----
 *    2017-04-19  Enis Hoca  Original Creation
 *
 * 
 */

metadata {
    definition(name: "Aqara Door/Window Sensor T1", namespace: "custom", author: "enishoca") {
        capability "Contact Sensor"
        capability "Battery"
        capability "Configuration"
        capability "Refresh"
        capability "Sensor"

        attribute "batteryVoltage", "number"
        attribute "lastBatteryReport", "string"
        attribute "lastOpened", "string"
        attribute "lastClosed", "string"
        attribute "lastContactChange", "string"
        attribute "contactDuration", "number"

        fingerprint profileId:"0104", endpointId:"01", inClusters:"0000,0001,0003,0500", outClusters:"0019", model:"lumi.magnet.agl02", manufacturer:"LUMI"
    }

    preferences {
        input name: "logEnable", type: "bool", title: "Enable debug logging", defaultValue: true
    }
}

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

    if (description?.startsWith("zone status")) {
        return handleZoneStatus(description)
    }

    def descMap = zigbee.parseDescriptionAsMap(description)
    if (logEnable) log.debug "descMap: ${descMap}"

    if (descMap?.clusterInt == 0x0001 && descMap.attrInt == 0x0020 && descMap.value) {
        try {
            def rawVolts = Integer.parseInt(descMap.value, 16)
            def volts = rawVolts / 10.0
            def minVolts = 2.5
            def maxVolts = 3.0
            def pct = ((volts - minVolts) / (maxVolts - minVolts) * 100).toInteger()
            pct = Math.max(0, Math.min(pct, 100))

            def nowStr = new Date().format("yyyy-MM-dd HH:mm:ss", location.timeZone)

            if (logEnable) log.debug "Battery voltage: ${volts}V → ${pct}%"
            sendEvent(name: "battery", value: pct, unit: "%")
            sendEvent(name: "batteryVoltage", value: volts, unit: "V")
            sendEvent(name: "lastBatteryReport", value: nowStr)

        } catch (Exception e) {
            log.warn "Failed to parse battery voltage: ${descMap.value}"
        }
    }

    return []
}

def handleZoneStatus(description) {
    def zoneStatus = description.replace("zone status", "").trim().split(" ")[0]
    def statusInt = Integer.parseInt(zoneStatus.replace("0x", ""), 16)
    def contactState = (statusInt & 0x0001) ? "open" : "closed"

    def now = new Date()
    def nowStr = now.format("yyyy-MM-dd HH:mm:ss", location.timeZone)

    sendEvent(name: "contact", value: contactState)
    sendEvent(name: "lastContactChange", value: nowStr)

    if (contactState == "open") {
        sendEvent(name: "lastOpened", value: nowStr)
    } else {
        sendEvent(name: "lastClosed", value: nowStr)
    }

    // Duration tracking
    def lastChange = device.currentValue(contactState == "open" ? "lastClosed" : "lastOpened")
    if (lastChange) {
        try {
            def lastTime = Date.parse("yyyy-MM-dd HH:mm:ss", lastChange)
            def duration = ((now.time - lastTime.time) / 1000).toInteger()
            sendEvent(name: "contactDuration", value: duration)
        } catch (e) {
            log.warn "Failed to parse last state time: ${lastChange}"
        }
    }

    if (logEnable) log.debug "Parsed contact state: ${contactState} at ${nowStr}"
}

def configure() {
    if (logEnable) log.debug "Sending custom IAS enroll response..."
    def dni = device.deviceNetworkId
    def cmds = []

    cmds += [
        "he raw 0x${dni} 0x01 0x0500 0x00 {01 00}",
        "delay 200"
    ]

    cmds += zigbee.readAttribute(0x0001, 0x0020)
    return cmds
}

def refresh() {
    if (logEnable) log.debug "Requesting battery and contact status..."
    return zigbee.readAttribute(0x0001, 0x0020) + zigbee.readAttribute(0x0500, 0x0002)
}

def updated() {
    if (logEnable) log.debug "Driver updated"
    runIn(1800, logsOff)
    configure()
}

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