/** * Reolink Doorbell (Component Driver) * Version: 1.4.2 * * Same delegation pattern as Reolink Camera, plus a "visitor" (button press) * event so Rule Machine can trigger straight off "pushed 1" for a doorbell * ring, separate from AI person/motion detection. * * v1.4.2 -- HOTFIX: bare paragraph("text") calls in preferences are App-DSL * only and don't exist on a driver's compiled script -- caused a fatal * "No signature of method: Script1.paragraph()" on save/update, blocking the * 1.4.1 update entirely (same bug as CameraDriver.groovy). Fixed via * input(type: "paragraph"). * v1.4.1 -- Added chargingStatus attribute (charging/not_charging/unknown) * from GetBatteryInfo's Battery.chargeStatus, same confirmed field as the * camera driver. batteryMode self-heal was app-side only, no change needed * here. * v1.3.9 -- Added Battery capability so a battery-powered doorbell can show * a percentage and get pulled into the app's auto battery-check scheduler * (keyed off hasCapability("Battery"), no app-side change needed for that * part). Added receiveBatteryInfo() with the confirmed nested * Battery.batteryPercent field, and receiveBatteryMode() (called once at * device creation). * v1.3.8 -- No functional change (app-side: event-driven updates, PIR, * login/action fixes, all camera-only). Added lastUpdateSource attribute * (event/poll), same as the camera driver. * v1.3.6 -- No functional change (app-side only). */ metadata { definition(name: "Reolink Doorbell", namespace: "jdthomas24", author: "Jason", component: true) { capability "Motion Sensor" capability "PushableButton" capability "Refresh" capability "Sensor" // v1.3.9: added so a battery-powered doorbell can show a % and get // pulled into the app's auto battery-check scheduler, which keys // off hasCapability("Battery") rather than device type. capability "Battery" attribute "person", "enum", ["active", "inactive"] attribute "vehicle", "enum", ["active", "inactive"] attribute "pet", "enum", ["active", "inactive"] attribute "package", "enum", ["active", "inactive"] attribute "snapshotUrl", "string" attribute "batteryMode", "enum", ["wired", "battery", "unknown"] // Both "charging" and "not_charging" confirmed against real // hardware -- see CameraDriver.groovy's matching attribute comment. attribute "chargingStatus", "enum", ["unknown", "not_charging", "charging"] attribute "sleepStatus", "enum", ["awake", "asleep", "unknown"] // Tracks whether the most recent state update came from the // real-time event push path or the plain polling fallback. attribute "lastUpdateSource", "enum", ["event", "poll"] attribute "supportedFeatures", "string" command "takeSnapshot" command "checkAbilities", [[name: "Refreshes the supportedFeatures attribute from the doorbell's current GetAbility data"]] command "checkBattery", [[name: "Battery-mode devices only"]] command "setPollInterval", [[name: "seconds", type: "NUMBER"]] command "setSnapshotInterval", [[name: "seconds", type: "NUMBER"]] } preferences { // v1.4.2 follow-up: reordered so each paragraph header is the FIRST // of its own 3-item row in this 3-column grid -- see // CameraDriver.groovy's matching preferences comment for why // (input(type: "paragraph") doesn't span the full row on a driver // the way App-DSL paragraph() does). input name: "battChkHdr", type: "paragraph", title: "Scheduled battery check" input name: "batteryCheckEnabled", type: "bool", title: "Enable auto battery check", defaultValue: false, description: "Battery devices only, OFF by default. When ON, auto-checks and updates battery level " + "on the interval below. Checking briefly wakes the device (negligible power at default " + "interval). Ignored for wired devices. Check Battery still works manually any time regardless " + "of this setting." input name: "batteryCheckIntervalHours", type: "number", title: "Auto battery check interval (hours)", defaultValue: 12, description: "Only used if the setting above is ON." input name: "eventWakeHdr", type: "paragraph", title: "Event-triggered battery check" input name: "checkBatteryOnEventWake", type: "bool", title: "Also check battery/charging on real motion/AI events", defaultValue: false, description: "Battery devices only, OFF by default. When ON, a real motion/AI event (device " + "already awake) also triggers a battery/charging check -- free, unlike the interval above, " + "since it doesn't force an extra wakeup." input name: "eventWakeBatteryThrottleSec", type: "number", title: "Minimum seconds between event-triggered checks", defaultValue: 60, description: "Only used if the setting above is ON. Keeps a burst of events (motion, person, " + "vehicle in seconds) from triggering more than one check." input name: "pollIntervalSec", type: "number", title: "Poll interval (sec)", defaultValue: 5, description: "Controls how often motion/AI/visitor state is polled. Does NOT control snapshot image " + "freshness -- see Snapshot interval below." input name: "snapshotIntervalSec", type: "number", title: "Snapshot interval (sec)", defaultValue: 30, description: "Controls how often the cached dashboard snapshot image refreshes. A dashboard tile's " + "own refresh rate does NOT make the image any fresher than this -- it just re-displays whatever " + "was last cached at this interval. Kept separate from poll interval so motion/visitor detection " + "can stay fast without forcing a full image download that often." } } def installed() { sendEvent(name: "numberOfButtons", value: 1) } def refresh() { parent?.componentRefresh(this, device.deviceNetworkId) } def takeSnapshot() { parent?.componentTakeSnapshot(this, device.deviceNetworkId) } def setPollInterval(seconds) { parent?.componentSetPollInterval(this, seconds as Integer, device.deviceNetworkId) } def setSnapshotInterval(seconds) { parent?.componentSetSnapshotInterval(this, seconds as Integer, device.deviceNetworkId) } def checkAbilities() { parent?.componentCheckAbilities(this, device.deviceNetworkId) } def checkBattery() { parent?.componentCheckBattery(this, device.deviceNetworkId) } /** * v1.3.9: battery% reads the confirmed nested reolink_aio field * Battery.batteryPercent first, same fix already validated on the camera * driver, with flat fallbacks kept for firmware variants that return it * unnested. chargingStatus (v1.4.1) reads Battery.chargeStatus -- see * CameraDriver.groovy's matching comment for the confirmed hardware detail. */ def receiveBatteryInfo(battInfo) { def pct = battInfo?.Battery?.batteryPercent ?: battInfo?.batteryPercent ?: battInfo?.batteryPercentage if (pct != null) sendEvent(name: "battery", value: pct) def chargeStatus = battInfo?.Battery?.chargeStatus def chargingLabel = (chargeStatus == 1) ? "charging" : (chargeStatus == 0) ? "not_charging" : "unknown" if (chargeStatus != null) sendEvent(name: "chargingStatus", value: chargingLabel) } /** * Required by the PushableButton capability -- declaring the capability adds * the Push command/attributes to the device page, but does NOT auto-implement * this method; without it, clicking Push (or any app/rule calling push()) * throws MissingMethodException. Untyped buttonNumber parameter deliberately * -- Hubitat's own Commands-tab test UI can pass this as a String rather than * a Number, and a typed/coerced parameter would reject that. */ def push(buttonNumber) { sendEvent(name: "pushed", value: buttonNumber, isStateChange: true) } /** * Called by the app after GetAbility, both at discovery/creation time and on * a manual checkAbilities command. Informational only -- see the app's Tips * page ("Supported Features") for what this does and doesn't mean. Does NOT * hide or disable any command on this device; Hubitat has no way to do that * for an individual device instance. */ def receiveSupportedFeatures(List features) { sendEvent(name: "supportedFeatures", value: features ? features.join(", ") : "None detected") } /** Called by the app after either a poll or a real-time event push -- see CameraDriver.groovy's matching note. */ def parseReolinkState(aiState, mdState, String source = "poll") { sendIfChanged("sleepStatus", "awake") sendIfChanged("lastUpdateSource", source) // TODO confirm the visitor/doorbell-press field name in your firmware's GetAiState/GetMdState payload def visitorPressed = aiState?.visitor?.alarm_state == 1 if (visitorPressed) { push(1) } def motionActive = mdState?.state == 1 sendIfChanged("motion", motionActive ? "active" : "inactive") ["people", "vehicle", "dog_cat"].each { key -> def attr = key == "people" ? "person" : (key == "dog_cat" ? "pet" : key) def active = aiState?.getAt(key)?.alarm_state == 1 sendIfChanged(attr, active ? "active" : "inactive") } def pkgActive = aiState?.package?.alarm_state == 1 sendIfChanged("package", pkgActive ? "active" : "inactive") } /** See camera driver for why this exists -- cuts redundant sendEvent() calls to reduce load on lower-spec hubs. */ private void sendIfChanged(String name, value) { if (device.currentValue(name)?.toString() != value?.toString()) { sendEvent(name: name, value: value) } } /** Called by the app when a poll gets no response -- see camera driver for the reasoning. */ def markAsleep() { sendIfChanged("sleepStatus", "asleep") } /** * v1.3.9: called once by the app at device creation time with the discovery- * time battery probe result -- see CameraDriver.groovy's matching note. * v1.4.1: the app's scheduler can also call this later to backfill a device * that ended up without batteryMode set -- no change needed here either way. */ def receiveBatteryMode(String mode) { sendEvent(name: "batteryMode", value: mode) } def receiveSnapshotUrl(url) { sendEvent(name: "snapshotUrl", value: url) }