diff --git a/src/apps/unofficial-ring-connect.groovy b/src/apps/unofficial-ring-connect.groovy index ba1e466..81abdf9 100644 --- a/src/apps/unofficial-ring-connect.groovy +++ b/src/apps/unofficial-ring-connect.groovy @@ -1093,6 +1093,31 @@ void apiRequestDeviceSet(final String dni, final String kind, final String actio } } +/** + * Makes a ring api request to set a setting for a device + * @param dni DNI of device to refresh + * @param kind Kind of device ("doorbots", etc.) + * @param action Action to perform on device ("floodlight_light_off", etc) + */ +void apiRequestDeviceApiSet(final String dni, final String kind, final String action = null, final Map query = null) { + logTrace("apiRequestDeviceSet(${dni}, ${kind}, ${action}, ${query})") + + Map params = makeDeviceApiParams('/' + kind + '/' + getRingDeviceId(dni) + (action ? "/${action}" : ""), + [contentType: TEXT, requestContentType: JSON, query: query]) + + apiRequestAsyncCommon("apiRequestDeviceSet", "Patch", params, false) { resp -> + logTrace "apiRequestDeviceSet ${kind} ${action} for ${dni} succeeded" + + def body = resp.getData() ? resp.getJson() : null + ChildDeviceWrapper d = getChildDevice(dni) + if (d) { + d.handleDeviceSet(action, body, query) + } else { + log.error "apiRequestDeviceSet ${kind}.${action} cannot get child device with dni ${dni}" + } + } +} + /** * Makes a ring api request to set a value for a device * @param dni DNI of device to refresh @@ -1558,6 +1583,19 @@ Map makeClientsApiParams(final String urlSuffix, final Map args, final Map heade return params } +Map makeDeviceApiParams(final String urlSuffix, final Map args, final Map headerArgs = [:]) { + Map params = [ + uri: DEVICES_API_BASE_URL + urlSuffix, + contentType: args.getOrDefault('contentType', JSON), + ] + + params << args.subMap(['body', 'requestContentType', 'query']) + + addHeadersToHttpRequest(params, headerArgs) + + return params +} + // Called by initialize and by child ring-api-virtual-device when an old version of things was detected void schedulePeriodicMaintenance() { schedule("0 ${getRandomInteger(60)} ${getRandomInteger(5)} ? * MON", periodicMaintenance) diff --git a/src/drivers/ring-virtual-camera-with-siren.groovy b/src/drivers/ring-virtual-camera-with-siren.groovy index 2b3c4b9..6c599f4 100644 --- a/src/drivers/ring-virtual-camera-with-siren.groovy +++ b/src/drivers/ring-virtual-camera-with-siren.groovy @@ -24,15 +24,20 @@ metadata { capability "PushableButton" capability "Refresh" capability "Sensor" + capability "Health Check" attribute "firmware", "string" attribute "rssi", "number" attribute "wifi", "string" + attribute "healthStatus", "enum", [ "unknown", "offline", "online" ] command "getDings" + command "snoozeMotionAlerts", [ + [name:"minutes", type:"NUMBER", description:"Number of minutes to snooze motion alerts for", constraints:["NUMBER"]] ] } preferences { + input name: "deviceStatusPollingEnable", type: "bool", title: "Enable polling for device status", defaultValue: true input name: "snapshotPolling", type: "bool", title: "Enable polling for thumbnail snapshots on this device", defaultValue: false input name: "descriptionTextEnable", type: "bool", title: "Enable descriptionText logging", defaultValue: false input name: "logEnable", type: "bool", title: "Enable debug logging", defaultValue: false @@ -73,12 +78,32 @@ def getDings() { def updated() { parent.snapshotOption(device.deviceNetworkId, snapshotPolling) + scheduleDevicePolling() +} + +def installed() { + scheduleDevicePolling() +} + +def scheduleDevicePolling() { + unschedule(pollDeviceStatus) + + // Schedule at a random second starting in the next ~10 minutes + Random rnd = new Random() + def scheduledMinute = ((new Date().format( "m" ) as int) + rnd.nextInt(10)) % 60 + if (deviceStatusPollingEnable) { + schedule( "${rnd.nextInt(59)} ${scheduledMinute}/30 * ? * *", "refresh" ) + } } def off() { parent.apiRequestDeviceSet(device.deviceNetworkId, "doorbots", "siren_off") } +def snoozeMotionAlerts(minutes = 60) { + parent.apiRequestDeviceControl(device.deviceNetworkId, "doorbots", "motion_snooze?time=${minutes}", null) +} + def siren() { parent.apiRequestDeviceSet(device.deviceNetworkId, "doorbots", "siren_on") } @@ -135,12 +160,20 @@ void handleMotion(final Map msg) { } void handleRefresh(final Map msg) { + if (msg?.alerts?.connection != null) { + checkChanged("healthStatus", msg.alerts.connection) // devices seem to be considered offline after 20 minutes + } + else { + checkChanged("healthStatus", "unknown") + } + if (msg.battery_life != null) { checkChanged("battery", msg.battery_life, '%') } else if (msg.battery_life_2 != null) { checkChanged("battery", msg.battery_life_2, "%") } + if (msg.siren_status?.seconds_remaining != null) { final Integer secondsRemaining = msg.siren_status.seconds_remaining checkChanged("alarm", secondsRemaining > 0 ? "siren" : "off") @@ -148,6 +181,7 @@ void handleRefresh(final Map msg) { runIn(secondsRemaining + 1, refresh) } } + if (msg.health) { final Map health = msg.health @@ -177,4 +211,4 @@ boolean checkChanged(final String attribute, final newStatus, final String unit= } sendEvent(name: attribute, value: newStatus, unit: unit, type: type) return changed -} \ No newline at end of file +} diff --git a/src/drivers/ring-virtual-camera.groovy b/src/drivers/ring-virtual-camera.groovy index 121c774..c94d048 100644 --- a/src/drivers/ring-virtual-camera.groovy +++ b/src/drivers/ring-virtual-camera.groovy @@ -23,15 +23,18 @@ metadata { capability "PushableButton" capability "Refresh" capability "Sensor" + capability "Health Check" attribute "firmware", "string" attribute "rssi", "number" attribute "wifi", "string" + attribute "healthStatus", "enum", [ "unknown", "offline", "online" ] command "getDings" } preferences { + input name: "deviceStatusPollingEnable", type: "bool", title: "Enable polling for device status", defaultValue: true input name: "snapshotPolling", type: "bool", title: "Enable polling for thumbnail snapshots on this device", defaultValue: false input name: "descriptionTextEnable", type: "bool", title: "Enable descriptionText logging", defaultValue: false input name: "logEnable", type: "bool", title: "Enable debug logging", defaultValue: false @@ -54,6 +57,7 @@ void logTrace(msg) { def updated() { checkChanged("numberOfButtons", 1) parent.snapshotOption(device.deviceNetworkId, snapshotPolling) + scheduleDevicePolling() } def parse(String description) { @@ -74,6 +78,20 @@ def refresh() { parent.apiRequestDeviceHealth(device.deviceNetworkId, "doorbots") } +def installed() { + scheduleDevicePolling() +} + +def scheduleDevicePolling() { + unschedule(pollDeviceStatus) + // Schedule at a random second starting at the next minute + def nextMinute = ((new Date().format( "m" ) as int) + 1) % 60 + Random rnd = new Random() + if (deviceStatusPollingEnable) { + schedule( "${rnd.nextInt(59)} ${nextMinute}/30 * ? * *", "refresh" ) + } +} + def getDings() { logDebug "getDings()" parent.apiRequestDings() @@ -108,6 +126,13 @@ void handleMotion(final Map msg) { } void handleRefresh(final Map msg) { + if (msg?.alerts?.connection != null) { + checkChanged("healthStatus", msg.alerts.connection) // devices seem to be considered offline after 20 minutes + } + else { + checkChanged("healthStatus", "unknown") + } + if (!["jbox_v1", "lpd_v1", "lpd_v2"].contains(device.getDataValue("kind"))) { if (msg.battery_life != null) { checkChanged("battery", msg.battery_life, '%') @@ -146,4 +171,4 @@ boolean checkChanged(final String attribute, final newStatus, final String unit= } sendEvent(name: attribute, value: newStatus, unit: unit, type: type) return changed -} \ No newline at end of file +} diff --git a/src/drivers/ring-virtual-light-with-siren.groovy b/src/drivers/ring-virtual-light-with-siren.groovy index ef0d745..b19b0b7 100644 --- a/src/drivers/ring-virtual-light-with-siren.groovy +++ b/src/drivers/ring-virtual-light-with-siren.groovy @@ -23,16 +23,24 @@ metadata { capability "Refresh" capability "Sensor" capability "Switch" + capability "SwitchLevel" + capability "Health Check" attribute "firmware", "string" attribute "rssi", "number" attribute "wifi", "string" + attribute "healthStatus", "enum", [ "unknown", "offline", "online" ] command "alarmOff" command "getDings" + command "snoozeMotionAlerts", [ + [name:"minutes", type:"NUMBER", description:"Number of minutes to snooze motion alerts for", constraints:["NUMBER"]] ] + command "motionActivatedLights", [ + [name:"state", description:"Enable or disable lights on motion", type: "ENUM", constraints: ["enabled","disabled"]] ] } preferences { + input name: "deviceStatusPollingEnable", type: "bool", title: "Enable polling for device status", defaultValue: true input name: "lightPolling", type: "bool", title: "Enable polling for light status on this device", defaultValue: false input name: "lightInterval", type: "number", range: 10..600, title: "Number of seconds in between light polls", defaultValue: 15 input name: "snapshotPolling", type: "bool", title: "Enable polling for thumbnail snapshots on this device", defaultValue: false @@ -90,9 +98,35 @@ def pollLight() { } } +def snoozeMotionAlerts(minutes = 60) { + parent.apiRequestDeviceControl(device.deviceNetworkId, "doorbots", "motion_snooze?time=${minutes}", null) +} + + +def motionActivatedLights(state) { + // This is backwards as it's manipulating "always snooze" + stateOfLight = state == "enabled" ? false : true + parent.apiRequestDeviceApiSet(device.deviceNetworkId, "devices", "settings", ["enable":stateOfLight,"light_snooze_settings":["always_on":stateOfLight]]) +} + def updated() { setupPolling() parent.snapshotOption(device.deviceNetworkId, snapshotPolling) + scheduleDevicePolling() +} + +def installed() { + scheduleDevicePolling() +} + +def scheduleDevicePolling() { + unschedule(pollDeviceStatus) + // Schedule at a random second starting at the next minute + def nextMinute = ((new Date().format( "m" ) as int) + 1) % 60 + Random rnd = new Random() + if (deviceStatusPollingEnable) { + schedule( "${rnd.nextInt(59)} ${nextMinute}/30 * ? * *", "refresh" ) + } } def on() { @@ -105,6 +139,12 @@ def off() { switchOff() } +def setLevel(level) { + // Translating SwitchLevel 0-100% to Ring brightness (1-10) + Integer brightnessLevel = Math.max(1, (level / 10) as Integer) + parent.apiRequestDeviceSet(device.deviceNetworkId, "doorbots", "light_intensity?doorbot%5Bsettings%5D%5Blight_intensity%5D=${brightnessLevel}") +} + def switchOff() { if (state.strobing) { unschedule() @@ -173,6 +213,13 @@ void handleDeviceSet(final String action, final Map msg, final Map query) { else if (action == "siren_off") { checkChanged('alarm', "off") } + else if (action == "settings") { + log.trace("Updated setting: ${query}") + } + else if (action.startsWith("light_intensity?doorbot%5Bsettings%5D%5Blight_intensity%5D=")) { + Integer brightnessLevel = (action.split('=')[1] as Integer) * 10 + checkChanged("level", brightnessLevel) + } else { log.error "handleDeviceSet unsupported action ${action}, msg=${msg}, query=${query}" } @@ -202,6 +249,13 @@ void handleMotion(final Map msg) { } void handleRefresh(final Map msg) { + if (msg?.alerts?.connection != null) { + checkChanged("healthStatus", msg.alerts.connection) // devices seem to be considered offline after 20 minutes + } + else { + checkChanged("healthStatus", "unknown") + } + if (msg.led_status) { checkChanged("switch", msg.led_status) } @@ -214,6 +268,11 @@ void handleRefresh(final Map msg) { } } + if (msg.settings?.floodlight_settings?.brightness != null) { + final Integer brightnessLevel = msg.settings.floodlight_settings.brightness * 10 + checkChanged("level", brightnessLevel) + } + if (msg.is_sidewalk_gateway) { log.warn("Your device is being used as an Amazon sidewalk device.") } @@ -248,4 +307,4 @@ boolean checkChanged(final String attribute, final newStatus, final String unit= } sendEvent(name: attribute, value: newStatus, unit: unit, type: type) return changed -} \ No newline at end of file +} diff --git a/src/drivers/ring-virtual-light.groovy b/src/drivers/ring-virtual-light.groovy index 9bb314b..520b2ab 100644 --- a/src/drivers/ring-virtual-light.groovy +++ b/src/drivers/ring-virtual-light.groovy @@ -23,17 +23,24 @@ metadata { capability "Sensor" capability "Switch" capability "Refresh" + capability "Health Check" attribute "firmware", "string" attribute "battery2", "number" attribute "rssi", "number" attribute "wifi", "string" + attribute "healthStatus", "enum", [ "unknown", "offline", "online" ] command "flash" command "getDings" + command "snoozeMotionAlerts", [ + [name:"minutes", type:"NUMBER", description:"Number of minutes to snooze motion alerts for", constraints:["NUMBER"]] ] + command "motionActivatedLights", [ + [name:"state", description:"Enable or disable lights on motion", type: "ENUM", constraints: ["enabled","disabled"]] ] } preferences { + input name: "deviceStatusPollingEnable", type: "bool", title: "Enable polling for device status", defaultValue: true input name: "lightPolling", type: "bool", title: "Enable polling for light status on this device", defaultValue: false input name: "lightInterval", type: "number", range: 10..600, title: "Number of seconds in between light polls", defaultValue: 15 input name: "snapshotPolling", type: "bool", title: "Enable polling for thumbnail snapshots on this device", defaultValue: false @@ -94,9 +101,34 @@ def pollLight() { } } +def snoozeMotionAlerts(minutes = 60) { + parent.apiRequestDeviceControl(device.deviceNetworkId, "doorbots", "motion_snooze?time=${minutes}", null) +} + +def motionActivatedLights(state) { + // This is backwards as it's manipulating "always snooze" + stateOfLight = state == "enabled" ? false : true + parent.apiRequestDeviceApiSet(device.deviceNetworkId, "devices", "settings", ["enable":stateOfLight,"light_snooze_settings":["always_on":stateOfLight]]) +} + def updated() { setupPolling() parent.snapshotOption(device.deviceNetworkId, snapshotPolling) + scheduleDevicePolling() +} + +def installed() { + scheduleDevicePolling() +} + +def scheduleDevicePolling() { + unschedule(pollDeviceStatus) + // Schedule at a random second starting at the next minute + def nextMinute = ((new Date().format( "m" ) as int) + 1) % 60 + Random rnd = new Random() + if (deviceStatusPollingEnable) { + schedule( "${rnd.nextInt(59)} ${nextMinute}/30 * ? * *", "refresh" ) + } } def on() { @@ -168,7 +200,14 @@ void handleMotion(final Map msg) { } } -void handleRefresh(final Map msg) { +void handleRefresh(final Map msg) { + if (msg?.alerts?.connection != null) { + checkChanged("healthStatus", msg.alerts.connection) // devices seem to be considered offline after 20 minutes + } + else { + checkChanged("healthStatus", "unknown") + } + if (!discardBatteryLevel) { if (msg.battery_life != null) { checkChanged("battery", msg.battery_life, "%") @@ -220,4 +259,4 @@ boolean checkChanged(final String attribute, final newStatus, final String unit= } sendEvent(name: attribute, value: newStatus, unit: unit, type: type) return changed -} \ No newline at end of file +}