From 891309f10c5f5556703cfa788f5c6e4796102914 Mon Sep 17 00:00:00 2001 From: jake9190 Date: Sat, 21 Oct 2023 22:34:07 -0400 Subject: [PATCH 1/7] Update unofficial-ring-connect.groovy --- src/apps/unofficial-ring-connect.groovy | 40 ++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/src/apps/unofficial-ring-connect.groovy b/src/apps/unofficial-ring-connect.groovy index 0d91d85..2924223 100644 --- a/src/apps/unofficial-ring-connect.groovy +++ b/src/apps/unofficial-ring-connect.groovy @@ -1043,7 +1043,7 @@ void apiRequestDeviceRefresh(final String dni) { * @param query Action to perform on device ([kind: "motion"], etc) */ void apiRequestDeviceControl(final String dni, final String kind, final String action, final Map query) { - logTrace("apiRequestDeviceControl(${dni}, ${kind}, ${action}, ${query})") + logInfo("apiRequestDeviceControl(${dni}, ${kind}, ${action}, ${query})") Map params = makeClientsApiParams('/' + kind + '/' + getRingDeviceId(dni) + '/' + action, [contentType: TEXT, requestContentType: JSON, query: query]) @@ -1086,6 +1086,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 @@ -1551,6 +1576,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) From 33e1bdbd8cbf14f755f788f0bacdb8ff52c2f8b4 Mon Sep 17 00:00:00 2001 From: jake9190 Date: Sat, 21 Oct 2023 22:34:36 -0400 Subject: [PATCH 2/7] Update ring-virtual-camera.groovy --- src/drivers/ring-virtual-camera.groovy | 27 +++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) 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 +} From 8dc9f694b59bbf54a5ef743fc6079a4235a8e5da Mon Sep 17 00:00:00 2001 From: jake9190 Date: Sat, 21 Oct 2023 22:34:52 -0400 Subject: [PATCH 3/7] Update ring-virtual-camera-with-siren.groovy --- .../ring-virtual-camera-with-siren.groovy | 36 ++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) 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 +} From 3986c820e6bd6dd8fc740fb665e6f6399e5aafd8 Mon Sep 17 00:00:00 2001 From: jake9190 Date: Sat, 21 Oct 2023 22:35:11 -0400 Subject: [PATCH 4/7] Update ring-virtual-light.groovy --- src/drivers/ring-virtual-light.groovy | 43 +++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) 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 +} From 28484bb5ecb83e015aa686bb4eba43db3aba20a8 Mon Sep 17 00:00:00 2001 From: jake9190 Date: Sat, 21 Oct 2023 22:35:25 -0400 Subject: [PATCH 5/7] Update ring-virtual-light-with-siren.groovy --- .../ring-virtual-light-with-siren.groovy | 45 ++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/src/drivers/ring-virtual-light-with-siren.groovy b/src/drivers/ring-virtual-light-with-siren.groovy index ef0d745..b255a4d 100644 --- a/src/drivers/ring-virtual-light-with-siren.groovy +++ b/src/drivers/ring-virtual-light-with-siren.groovy @@ -23,16 +23,23 @@ metadata { capability "Refresh" capability "Sensor" capability "Switch" + 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 +97,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() { @@ -173,6 +206,9 @@ 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 { log.error "handleDeviceSet unsupported action ${action}, msg=${msg}, query=${query}" } @@ -202,6 +238,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) } @@ -248,4 +291,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 +} From 52805958dd32b3040cc4685cdb1c2d246d285fa7 Mon Sep 17 00:00:00 2001 From: jake9190 Date: Sat, 21 Oct 2023 22:37:14 -0400 Subject: [PATCH 6/7] Update unofficial-ring-connect.groovy --- src/apps/unofficial-ring-connect.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/apps/unofficial-ring-connect.groovy b/src/apps/unofficial-ring-connect.groovy index 2924223..42e8ff4 100644 --- a/src/apps/unofficial-ring-connect.groovy +++ b/src/apps/unofficial-ring-connect.groovy @@ -1043,7 +1043,7 @@ void apiRequestDeviceRefresh(final String dni) { * @param query Action to perform on device ([kind: "motion"], etc) */ void apiRequestDeviceControl(final String dni, final String kind, final String action, final Map query) { - logInfo("apiRequestDeviceControl(${dni}, ${kind}, ${action}, ${query})") + logTrace("apiRequestDeviceControl(${dni}, ${kind}, ${action}, ${query})") Map params = makeClientsApiParams('/' + kind + '/' + getRingDeviceId(dni) + '/' + action, [contentType: TEXT, requestContentType: JSON, query: query]) From d632948997c080dde845c926370b1aade347f60d Mon Sep 17 00:00:00 2001 From: jake9190 Date: Tue, 24 Oct 2023 12:46:05 -0400 Subject: [PATCH 7/7] Add SetLevel for floodlight brightness --- src/drivers/ring-virtual-light-with-siren.groovy | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/drivers/ring-virtual-light-with-siren.groovy b/src/drivers/ring-virtual-light-with-siren.groovy index b255a4d..b19b0b7 100644 --- a/src/drivers/ring-virtual-light-with-siren.groovy +++ b/src/drivers/ring-virtual-light-with-siren.groovy @@ -23,6 +23,7 @@ metadata { capability "Refresh" capability "Sensor" capability "Switch" + capability "SwitchLevel" capability "Health Check" attribute "firmware", "string" @@ -138,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() @@ -209,6 +216,10 @@ void handleDeviceSet(final String action, final Map msg, final Map query) { 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}" } @@ -257,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.") }