Skip to content

Instantly share code, notes, and snippets.

@bkrn
Created July 10, 2022 23:00
Show Gist options
  • Save bkrn/1c613361b7d4239b63bc24be5d3444e0 to your computer and use it in GitHub Desktop.
Save bkrn/1c613361b7d4239b63bc24be5d3444e0 to your computer and use it in GitHub Desktop.
import groovy.transform.Field
definition(
name: 'Eiger',
namespace: 'eiger',
author: 'Aaron Di Silvestro',
description: 'Event initiated general engine for rules',
iconUrl: '',
iconX2Url: '',
iconX3Url: ''
)
@Field static Map<String, Object> rules = null
public static String version() { return 'v???' }
preferences
{
page(name: 'mainPage')
}
// Save current rule etag globally
Void cacheRule(Map<String, String> headers) {
if (headers.containsKey('ETag')) {
etag = headers['ETag']
} else {
etag = null
}
}
Void saveRules(response, _) {
if (response.getStatus() == 200) {
rules = parseJson(response.getData())
rules['etag'] = response.getHeaders()['ETag']
log.debug 'Got new rules'
createDevices()
} else if (response.getStatus() == 304) {
log.debug 'Rules have not changed'
} else {
log.debug 'Error getting rules'
}
runIn(300, 'loadRules')
}
Boolean notDeviceExists(name) {
return allDevices().find { it.name == name || it.label == name } == null
}
// Create an id for a new child device from the lowest available integer
String newDeviceId() {
children = getChildDevices()
List<Integer> deviceIds = []
for (child in children) {
deviceIds.add(child.getDeviceNetworkId().split('-')[-1] as int)
}
deviceIds = deviceIds.sort()
c = 0
for (ix in deviceIds) {
if (ix != c) {
return String.format('eiger-child-%d', c)
}
c++
}
return String.format('eiger-child-%d', c)
}
// Remove all child devices that are no longer in the rules object
Void cleanDevices() {
children = getChildDevices()
for (child in children) {
if (rules.groups.find { it.name == child.name } == null && rules.rules.find { it.name == child.name } == null) {
deleteChildDevice(child.getDeviceNetworkId())
} else if (child.getTypeName() != 'Virtual Dimmer') {
// **DRAGONS**
// This means that the vitual device lacks the
// dimmer capability and we should delete and start over
// removes it from other apps like Alexa :(
log.info "Reset device ${child.name}/${child.label} to make it a dimmer"
deleteChildDevice(child.getDeviceNetworkId())
}
}
}
// Given a list of device specifications, create any that do not exist as children
Void _createDevices(List<Object> devices) {
for (device in devices) {
if (notDeviceExists(device.name)) {
addChildDevice('hubitat', 'Virtual Dimmer', newDeviceId(), [
isComponent: true,
name: device.name,
label: device.name
])
}}
}
// Create any devices specified by rules that do not currently exist
Void createDevices() {
cleanDevices()
_createDevices(rules.groups)
_createDevices(rules.rules)
subscribeToDevices()
}
// Fetch rules from github and call saveRules with response if etag is updated
def loadRules() {
url = 'https://gist.githubusercontent.com/bkrn/e787932996ffe0a856dcd8226e38f321/raw/rules.json'
if (rules != null) {
asynchttpGet('saveRules', [uri: url, headers: ['If-None-Match': rules['etag']]])
} else {
asynchttpGet('saveRules', [uri: url])
}
}
def mainPage() {
dynamicPage(name: '', title: '', install: true, uninstall: true, refreshInterval:0)
{
section("<h2>${ app.label }</h2>") { }
if (state?.serverInstalled == null || state.serverInstalled == false) {
section("<b style=\"color:green\">${app.label} Installed!</b>")
{
paragraph "Click <i>Done</i> to save then return to ${app.label} to continue."
}
return
}
section(getFormat('title', " ${ app.label }")) { }
section('')
{
input 'externalDevices', 'capability.*', title: 'Select Devices', multiple: true, required: true
}
section('')
{
input 'logEnable', 'bool', title: 'Enable Debug Logging?', required: false, defaultValue: true
}
display()
}
}
/*
installed
Purpose: Standard install function.
Notes: Doesn't do much.
*/
def installed() {
log.info 'installed'
state.serverInstalled = true
initialize()
}
// Given a group from user rules
// Returns all devices inside it and any nested groups
// breaks cycles
List<Object> getGroupDevices(group) {
List<Object> groupDevices = []
List<String> todo = group['devices'].clone()
Map<String, Boolean> cycleCache = [:]
while (!todo.isEmpty()) {
deviceName = todo.pop()
if (cycleCache[deviceName] == true) {
continue
}
device = allDevices().find { d -> d.getLabel() == deviceName }
if (!device) {
log.debug String.format('Device Label "%s" was not found', deviceName)
continue
}
for (subGroup in rules['groups'].findAll { d -> d['name'] == deviceName }) {
todo.addAll(subGroup['devices'])
}
cycleCache[deviceName] = true
// **DRAGONS**
// Right now we do not add App manufactured children
// (which represent groups)
// since, if we turn them on/off, we end up triggering
// their groups again
if (device.getParentAppId() != app.getId()) {
groupDevices.add(device)
}
}
return groupDevices
}
Void runGroup(action, root, eventName) {
log.debug "Eiger Group: ${root.name}, ${eventName} -> ${action}"
for (device in getGroupDevices(root)) {
log.debug "Eiger Group Member: ${device.label}, ${eventName} -> ${action}"
log.debug "Capabilities ${device.getCapabilities()}"
if (eventName == 'switch' && device.hasCapability('Switch')) {
if (action == 'on') { device.on() }
if (action == 'off') { device.off() }
}
// Hue dimmer buttons for on/off
if (eventName == 'pushed' && device.hasCapability('Switch')) {
if (action == '1') { device.on() }
if (action == '4') { device.off() }
}
if (eventName == 'level' && device.hasCapability('SwitchLevel')) {
device.setLevel(action as int, 2)
}
}
}
Boolean overidden(cache, deviceName, command) {
return ((cache[deviceName] == 'off' && command == 'on') ||
(cache[deviceName] == 'on' && command == 'off') ||
(cache[deviceName] == command))
}
List<Object> populateRule(rule) {
List<Object> actions = []
Map<String, Object> cached = [:]
for (action in rule['commands']) {
for (device in externalDevices.findAll { it.getLabel() == action.device }) {
if (overidden(cached, action.device, action.command)) {
break
}
actions.add([
device: device,
command: action.command,
arg: action.arg,
])
cached[action.device] = action.command
break
}
for (group in rules['groups']) {
if (action.device == group['name']) {
for (device in getGroupDevices(group)) {
if (!overidden(cached, device.getLabel(), action.command)) {
actions.add([
device: device,
command: action.command,
arg: action.arg,
])
cached[device.getLabel()] = action.command
}
}
}
}
}
log.debug cached
return actions
}
Void runRule(rule) {
log.debug String.format('Eiger Rule: %s', rule.name)
for (action in populateRule(rule)) {
log.debug action
if (action['command'] == 'on') { action.device.on() }
if (action['command'] == 'off') { action.device.off() }
if (action['command'] == 'color') { action.device.setColor(action.arg) }
if (action['command'] == 'level') { action.device.setLevel(action.arg) }
}
}
Boolean ruleMatchesEvent(event, rule) {
if (rule['name'] != event.getDevice().label) {
return false
}
if (rule.hasProperty('unit') && event.unit != rule['unit']) {
return false
}
if (rule.hasProperty('comparator')) {
if (rule['comparator'] == '>') { return (event.value as float) > action }
if (rule['comparator'] == '<') { return (event.value as float) < action }
}
return rule['action'] == event.value
}
def handleEvent(event) {
String action = event.value
String target = event.getDevice().label
String eventName = event.name
log.debug String.format('Eiger Event: %s %s %s', target, action, eventName)
for (rule in rules['rules']) {
Boolean ranRule = false
if (ruleMatchesEvent(event, rule)) {
runRule(rule)
ranRule = true
}
if (ranRule) {
return
}
}
for (group in rules['groups']) {
if (target == group['name']) {
runGroup(action, group, eventName)
}
}
}
def subscribeToDevices() {
log.info 'Subscribed to devices'
unsubscribe('handleEvent')
for (device in allDevices()) {
subscribe(device, 'handleEvent', [filterEvents: false])
}
}
/*
updated
Purpose: Standard update function.
Notes: Still doesn't do much.
*/
def updated() {
log.info 'updated run'
if (debugOutput) runIn(1800, logsOff) //disable debug logs after 30 min
initialize()
}
List<Object> allDevices() {
List<Object> devices = getChildDevices() + externalDevices
return devices
}
/*
initialize
Purpose: Initialize the server instance.
Notes: Does it all
*/
def initialize() {
log.info 'initialize'
if (logEnable) log.debug 'Debugs are: ' + logEnable
unschedule()
loadRules()
subscribeToDevices()
}
/*
uninstalled
Purpose: uninstall the app.
Notes: Does it all
*/
def uninstalled() {
log.info 'uninstalled'
}
/*
display
Purpose: Displays the title/copyright/version info
Notes: Not very exciting.
*/
def display() {
section {
paragraph getFormat('line')
paragraph "<div style='color:#1A77C9;text-align:center;font-weight:small;font-size:9px'>Developed by: Aaron Di Silvestro<br/>Version Status: $state.Status<br>Current Version: ${version()} - ${thisCopyright}</div>"
}
}
/*
getFormat
Purpose: Formats of the title/copyright/version info for consistency
Notes: Not very exciting.
*/
def getFormat(type, myText='') {
if (type == 'header-green') return "<div style='color:#ffffff;font-weight: bold;background-color:#81BC00;border: 1px solid;box-shadow: 2px 3px #A9A9A9'>${myText}</div>"
if (type == 'line') return "\n<hr style='background-color:#1A77C9; height: 1px; border: 0;'></hr>"
if (type == 'title') return "<h2 style='color:#1A77C9;font-weight: bold'>${myText}</h2>"
}
/*
logsOff
Purpose: Disable debug logs
Notes: Not very exciting. Called by a 'runin' timer, typically 30 min.
*/
def logsOff() {
log.warn 'debug logging disabled...'
app?.updateSetting('logEnable', [value:'false', type:'bool'])
}
def getThisCopyright() { '&copy; 2020 Aaron Di Silvestro' }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment