Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Device Health Check and Device Settings #48

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions src/apps/unofficial-ring-connect.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
36 changes: 35 additions & 1 deletion src/drivers/ring-virtual-camera-with-siren.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")
}
Expand Down Expand Up @@ -135,19 +160,28 @@ 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")
if (secondsRemaining > 0) {
runIn(secondsRemaining + 1, refresh)
}
}

if (msg.health) {
final Map health = msg.health

Expand Down Expand Up @@ -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
}
}
27 changes: 26 additions & 1 deletion src/drivers/ring-virtual-camera.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -54,6 +57,7 @@ void logTrace(msg) {
def updated() {
checkChanged("numberOfButtons", 1)
parent.snapshotOption(device.deviceNetworkId, snapshotPolling)
scheduleDevicePolling()
}

def parse(String description) {
Expand All @@ -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()
Expand Down Expand Up @@ -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, '%')
Expand Down Expand Up @@ -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
}
}
61 changes: 60 additions & 1 deletion src/drivers/ring-virtual-light-with-siren.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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() {
Expand All @@ -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()
Expand Down Expand Up @@ -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}"
}
Expand Down Expand Up @@ -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)
}
Expand All @@ -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.")
}
Expand Down Expand Up @@ -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
}
}
Loading