diff --git a/dashboard/js/app.js b/dashboard/js/app.js index 02002351..bf81aa89 100644 --- a/dashboard/js/app.js +++ b/dashboard/js/app.js @@ -878,7 +878,7 @@ config.factory('dataService', ['$http', '$location', '$rootScope', '$window', '$ if (!si || !si.token) { if ((app.initialInstanceUri && app.initialInstanceUri.length) || (uri && uri.length)) { uri = app.initialInstanceUri ? app.initialInstanceUri : uri; - if (!uri.startsWith('https://')) { + if (!uri.startsWith('https://') && !uri.startsWith('http://')) { if (uri && (uri.indexOf('tat.comapi') > 0)) { var parts = uri.split('api'); if (parts[1].length >= 33) { @@ -1375,21 +1375,51 @@ config.factory('dataService', ['$http', '$location', '$rootScope', '$window', '$ dataService.listFuelStreams = function() { var instance = dataService.getInstance(); if (instance) { - var iid = instance.id; - var si = store[instance.id]; - if (!si) si = {}; - var region = (si && si.uri && si.uri.startsWith('https://graph-eu')) ? 'eu' : 'us'; - var req = { - method: 'POST', - url: 'https://api-' + region + '-' + iid[32] + '.webcore.co:9287/fuelStreams/list', - headers: { - 'Auth-Token': '|'+ iid - }, - data: { i: iid } + var urls = instance.fuelStreamUrls; + var jsonp = false; + var req; + + if(urls){ + var params = urls.list; + + if(params.l){ + jsonp = true; + req = params.u; + } + else { + req = { + method: params.m, + url: params.u, + headers: params.h, + data: params.d + } + } + } + else { + var iid = instance.id; + var si = store[instance.id]; + if (!si) si = {}; + var region = (si && si.uri && si.uri.startsWith('https://graph-eu')) ? 'eu' : 'us'; + req = { + method: 'POST', + url: 'https://api-' + region + '-' + iid[32] + '.webcore.co:9287/fuelStreams/list', + headers: { + 'Auth-Token': '|'+ iid + }, + data: { i: iid } + } + } + + if(jsonp){ + return $http.jsonp(req,{jsonpCallbackParam: 'callback'}).then(function(response) { + return response.data; + }); + } + else { + return $http(req).then(function(response) { + return response.data; + }); } - return $http(req).then(function(response) { - return response.data; - }); } } @@ -1438,21 +1468,54 @@ config.factory('dataService', ['$http', '$location', '$rootScope', '$window', '$ dataService.listFuelStreamData = function(fuelStreamId) { var instance = dataService.getInstance(); if (instance) { - var iid = instance.id; - var si = store[instance.id]; - if (!si) si = {}; - var region = (si && si.uri && si.uri.startsWith('https://graph-eu')) ? 'eu' : 'us'; - var req = { - method: 'POST', - url: 'https://api-' + region + '-' + iid[32] + '.webcore.co:9287/fuelStreams/get', - headers: { - 'Auth-Token': '|'+iid - }, - data: { i: iid, f: fuelStreamId } + var urls = instance.fuelStreamUrls; + var jsonp = false; + var req; + + if(urls){ + var params = urls.get; + + if(params.l){ + jsonp = true; + req = params.u.replace("{" + params.p + "}", fuelStreamId); + } + else { + var data = params.d + data[params.p] = fuelStreamId; + + req = { + method: params.m, + url: params.u, + headers: params.h, + data: data + } + } + } + else { + var iid = instance.id; + var si = store[instance.id]; + if (!si) si = {}; + var region = (si && si.uri && si.uri.startsWith('https://graph-eu')) ? 'eu' : 'us'; + req = { + method: 'POST', + url: 'https://api-' + region + '-' + iid[32] + '.webcore.co:9287/fuelStreams/get', + headers: { + 'Auth-Token': '|'+iid + }, + data: { i: iid, f: fuelStreamId } + } + } + + if(jsonp){ + return $http.jsonp(req,{jsonpCallbackParam: 'callback'}).then(function(response) { + return response.data; + }); + } + else { + return $http(req).then(function(response) { + return response.data; + }); } - return $http(req).then(function(response) { - return response.data; - }); } } diff --git a/dashboard/js/modules/dashboard.module.js b/dashboard/js/modules/dashboard.module.js index 253d15ec..3f886fea 100644 --- a/dashboard/js/modules/dashboard.module.js +++ b/dashboard/js/modules/dashboard.module.js @@ -37,7 +37,7 @@ config.controller('dashboard', ['$scope', '$rootScope', 'dataService', '$timeout if ($scope.$$destroyed) return; if (currentRequestId != $scope.requestId) { return }; if (data) { - $scope.endpoint=data.endpoint + 'execute/:pistonId:'; + $scope.endpoint=data.endpoint + 'execute/:pistonId:' + (data.accessToken ? '?access_token=' + data.accessToken : ''); $scope.rawEndpoint=data.endpoint; $scope.rawAccessToken=data.accessToken; if (data.error) { diff --git a/dashboard/js/modules/piston.module.js b/dashboard/js/modules/piston.module.js index dcc2fceb..669b9921 100644 --- a/dashboard/js/modules/piston.module.js +++ b/dashboard/js/modules/piston.module.js @@ -209,7 +209,7 @@ config.controller('piston', ['$scope', '$rootScope', 'dataService', '$timeout', if ($scope.piston) $scope.loading = true; dataService.getPiston($scope.pistonId).then(function (response) { if ($scope.$$destroyed) return; - $scope.endpoint = data.endpoint + 'execute/' + $scope.pistonId; + $scope.endpoint = data.endpoint + 'execute/' + $scope.pistonId + (si.accessToken ? '?access_token=' + si.accessToken : ''); try { var showOptions = $scope.piston ? !!$scope.showOptions : false; if (!response || !response.data || !response.data.piston) { @@ -853,7 +853,7 @@ config.controller('piston', ['$scope', '$rootScope', 'dataService', '$timeout', $scope.getIFTTTUri = function(eventName) { var uri = dataService.getApiUri(); if (!uri) return "An error has occurred retrieving the IFTTT Maker URL"; - return uri + 'ifttt/' + eventName; + return uri + 'ifttt/' + eventName + (si.accessToken ? '?access_token=' + si.accessToken : ''); } $scope.toggleAdvancedOptions = function() { @@ -5377,4 +5377,4 @@ function test(value, parseAsString, dataType) { scope.evaluateExpression(scope.parseExpression(value, parseAsString, dataType)); } -var MAX_STACK_SIZE = 10; \ No newline at end of file +var MAX_STACK_SIZE = 10; diff --git a/smartapps/ady624/webcore-dashboard.src/webcore-dashboard.groovy b/smartapps/ady624/webcore-dashboard.src/webcore-dashboard.groovy index 4c78e690..abcdbdf9 100644 --- a/smartapps/ady624/webcore-dashboard.src/webcore-dashboard.groovy +++ b/smartapps/ady624/webcore-dashboard.src/webcore-dashboard.groovy @@ -21,7 +21,7 @@ public static String version() { return "v0.3.108.20180906" } /*** webCoRE DEFINITION ***/ /******************************************************************************/ private static String handle() { return "webCoRE" } -include 'asynchttp_v1' +if(!isHubitat())include 'asynchttp_v1' definition( name: "${handle()} Dashboard", namespace: "ady624", @@ -147,17 +147,21 @@ private void broadcastEvent(deviceId, eventName, eventValue, eventTime) { def iid = state.instanceId def region = state.region ?: 'us' if (!iid || !iid.startsWith(':') || !iid.endsWith(':')) return - asynchttp_v1.put(null, [ - uri: "https://api-${region}-${iid[32]}.webcore.co:9237", - path: '/event/sink', - headers: ['ST' : state.instanceId], - body: [ - d: deviceId, - n: eventName, - v: eventValue, - t: eventTime - ] - ]) + + def params = [ + uri: "https://api-${region}-${iid[32]}.webcore.co:9237", + path: '/event/sink', + requestContentType: "application/json", + headers: ['ST' : state.instanceId], + body: [d: deviceId, n: eventName, v: eventValue, t: eventTime] + ] + + if(asynchttp_v1){ + asynchttp_v1.put(null, params) + } + //else { + // asynchttpPut((String)null, params) + //} } /******************************************************************************/ @@ -191,8 +195,12 @@ def String hashId(id) { return result } +private isHubitat(){ + return hubUID != null +} + /******************************************************************************/ /*** ***/ /*** END OF CODE ***/ /*** ***/ -/******************************************************************************/ +/******************************************************************************/ \ No newline at end of file diff --git a/smartapps/ady624/webcore-fuel-stream.src/webcore-fuel-stream.groovy b/smartapps/ady624/webcore-fuel-stream.src/webcore-fuel-stream.groovy new file mode 100644 index 00000000..9de098f3 --- /dev/null +++ b/smartapps/ady624/webcore-fuel-stream.src/webcore-fuel-stream.groovy @@ -0,0 +1,111 @@ +private static String handle() { return "webCoRE" } +definition( + namespace:"ady624", + name:"${handle()} Fuel Stream", + description: "Local container for fuel streams", + author:"jp0550", + category:"My Apps", + iconUrl: "https://cdn.rawgit.com/ady624/${handle()}/master/resources/icons/app-CoRE.png", + iconX2Url: "https://cdn.rawgit.com/ady624/${handle()}/master/resources/icons/app-CoRE@2x.png", + iconX3Url: "https://cdn.rawgit.com/ady624/${handle()}/master/resources/icons/app-CoRE@3x.png", + parent: "ady624:webCoRE" +) + +preferences { + page(name: "settingsPage") +} + +def settingsPage(){ + dynamicPage(name: "settingsPage", title: "Settings", uninstall: true, install: true){ + section(){ + input "maxSize", "number", title: "Max size of all fuelStream data in KB", defaultValue: 95 + + def storageSize = (int)(state.toString().size() / 1024.0) + paragraph("Current memory usage is ${storageSize}KB") + } + } +} + +def installed(){ + log.debug "Installed with settings $settings" + initialize() +} + +def updated(){ + log.debug "Updated with settings $settings" + initialize() +} + +def createStream(settings){ + state.fuelStream = [i: settings.id, c: (settings.canister ?: ""), n: settings.name, w: 1, t: getFormattedDate(new Date())] +} + +def initialize(){ + unsubscribe() + unschedule() + + if(app.id){ + getFuelStreamData() + cleanFuelStreams() + } +} + +def getFuelStreamData(){ + if(!state.fuelStreamData){ + state.fuelStreamData = [] + } + return state.fuelStreamData +} + +def cleanFuelStreams(){ + //ensure max size is obeyed + def storageSize = (int)(state.toString().size() / 1024.0) + def max = (settings.maxSize ?: 95).toInteger() + + if(storageSize > max){ + log.debug "Trim down fuel stream" + def points = getFuelStreamData().size() + def averageSize = points > 0 ? storageSize/(double)points : 0 + + def pointsToRemove = averageSize > 0 ? (int)((storageSize - max) / (double)averageSize) : 0 + pointsToRemove = pointsToRemove > 0 ? pointsToRemove : 0 + + log.debug "Size ${storageSize}KB Points ${points} Avg $averageSize Remove $pointsToRemove" + def toBeRemoved = getFuelStreamData().sort { it.i }.take(pointsToRemove) + getFuelStreamData().removeAll(toBeRemoved) + } + + getFuelStreamData().each { + it.keySet().remove('t') + } +} + +def updateFuelStream(req){ + def canister = req.c ?: "" + def name = req.n + def data = req.d + def instance = req.i + def source = req.s + + getFuelStreamData().add([d: data, i: (new Date()).getTime()]) + + cleanFuelStreams() +} + +def getFormattedDate(date = new Date()){ + def format = new java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"); + format.setTimeZone(TimeZone.getTimeZone("UTC")); + format.format(date) +} + +def getFuelStream(){ + state.fuelStream +} + +def listFuelStreamData(){ + getFuelStreamData().collect{ it + [t: getFormattedDate(new Date(it.i))]} +} + +def uninstalled(){ + parent.resetFuelStreamList() +} \ No newline at end of file diff --git a/smartapps/ady624/webcore-piston.src/webcore-piston.groovy b/smartapps/ady624/webcore-piston.src/webcore-piston.groovy index 6bf67fc5..aca240f1 100644 --- a/smartapps/ady624/webcore-piston.src/webcore-piston.groovy +++ b/smartapps/ady624/webcore-piston.src/webcore-piston.groovy @@ -289,7 +289,9 @@ public static String version() { return "v0.3.108.20180906" } /*** webCoRE DEFINITION ***/ /******************************************************************************/ private static String handle() { return "webCoRE" } -include 'asynchttp_v1' + +if(!isHubitat())include 'asynchttp_v1' + definition( name: "${handle()} Piston", namespace: "ady624", @@ -319,7 +321,7 @@ preferences { /******************************************************************************/ def pageMain() { //webCoRE Piston main page - return dynamicPage(name: "pageMain", title: "", uninstall: !!state.build) { + return dynamicPage(name: "pageMain", title: "", install: isHubitat() ? true : false, uninstall: !!state.build) { if (!parent || !parent.isInstalled()) { section() { paragraph "Sorry, you cannot install a piston directly from the Marketplace, please use the webCoRE SmartApp instead." @@ -358,6 +360,14 @@ def pageMain() { href "pageClear", title: "Clear all data except variables", description: "You will lose all logs, trace points, statistics, but no variables" href "pageClearAll", title: "Clear all data", description: "You will lose all data stored in any variables" } + + if(isHubitat()){ + section(){ + input "dev", "capability.*", title: "Devices", description: "Piston devices", multiple: true + input "maxStats", "number", title: "Max number of stats", description: "Max number of stats", defaultValue: getPistonLimits().maxStats + input "maxLogs", "number", title: "Max number of logs", description: "Max number of logs", defaultValue: getPistonLimits().maxLogs + } + } } } } @@ -404,7 +414,12 @@ def pageClearAll() { /*** ***/ /******************************************************************************/ +def isInstalled(){ + return !!state.created +} + def installed() { + if(isHubitat() && !app.id) return state.created = now() state.modified = now() state.build = 0 @@ -419,6 +434,12 @@ def installed() { def updated() { unsubscribe() initialize() + + if(isHubitat()){ + if((settings.maxStats?.toInteger() ?: 0) < 1) app.updateSetting("maxStats", [type: "number", value: 1]) + if((settings.maxLogs?.toInteger() ?: 0) < 1) app.updateSetting("maxLogs", [type: "number", value: 1]) + } + return true } @@ -608,7 +629,7 @@ def setBin(bin) { return [:] } -Map pause() { +Map pausePiston() { state.active = false def rtData = getRunTimeData() def msg = timer "Piston successfully stopped", null, -1 @@ -618,8 +639,8 @@ Map pause() { rtData.stats.nextSchedule = 0 unsubscribe() unschedule() - app.updateSetting('dev', null) - app.updateSetting('contacts', null) + app.updateSetting('dev', (Map) null) + app.updateSetting('contacts', (Map) null) state.hash = null state.trace = [:] state.subscriptions = [:] @@ -689,6 +710,21 @@ private getTemporaryRunTimeData() { ] } +//atomic state performance is much worse in hubitat than in smartthings. Grab a cached version where possible +private getCachedAtomicState(){ + def atomStart = now() + + try{ + atomicState.loadState() + def atomState = atomicState.@backingMap + return atomState + } + catch(e){ + return atomicState + } + //debug "Atomic state generated in ${now() - atomStart}ms", rtData +} + private getRunTimeData(rtData = null, semaphore = null, fetchWrappers = false) { def n = now() try { @@ -708,7 +744,9 @@ private getRunTimeData(rtData = null, semaphore = null, fetchWrappers = false) { rtData.category = state.category; rtData.stats = [nextScheduled: 0] //we're reading the cache from atomicState because we might have waited at a semaphore - rtData.cache = atomicState.cache ?: [:] + def atomState = (rtData.waitedAtSemaphore ?: true) ? (isHubitat() ? getCachedAtomicState() : atomicState) : state + + rtData.cache = atomState.cache ?: [:] rtData.newCache = [:] rtData.schedules = [] rtData.cancelations = [statements:[], conditions:[], all: false] @@ -716,25 +754,28 @@ private getRunTimeData(rtData = null, semaphore = null, fetchWrappers = false) { def logging = "$state.logging".toString() logging = logging.isInteger() ? logging.toInteger() : 0 rtData.logging = (int) logging - rtData.locationId = hashId(location.id) + rtData.locationId = hashId(location.id + (isHubitat() ? '-L' : '')) rtData.locationModeId = hashId(location.getCurrentMode().id) //flow control //we're reading the old state from atomicState because we might have waited at a semaphore - def st = atomicState.state + + def st = atomState.state rtData.state = (st instanceof Map) ? st : [old: '', new: ''] rtData.state.old = rtData.state.new; - rtData.store = atomicState.store ?: [:] + rtData.store = atomState.store ?: [:] rtData.statementLevel = 0; rtData.fastForwardTo = null rtData.break = false rtData.updateDevices = false - state.schedules = atomicState.schedules + rtData.pistonLimits = getPistonLimits() + + state.schedules = atomState.schedules if (!fetchWrappers) { rtData.devices = (settings.dev && (settings.dev instanceof List) ? settings.dev.collectEntries{[(hashId(it.id)): it]} : [:]) rtData.contacts = (settings.contacts && (settings.contacts instanceof List) ? settings.contacts.collectEntries{[(hashId(it.id)): it]} : [:]) - } + } rtData.systemVars = getSystemVariables() - rtData.localVars = getLocalVariables(rtData, piston.v) + rtData.localVars = getLocalVariables(rtData, piston.v, atomState) } catch(all) { error "Error while getting runtime data:", rtData, null, all } @@ -780,20 +821,38 @@ def timeoutRecoveryHandler_webCoRE(event) { timeHandler([t:now()], true) } -/* def timeRecoveryHandler(event) { timeHandler(event, true) } -*/ def executeHandler(event) { handleEvents([date: event.date, device: location, name: 'execute', value: event.value, jsonData: event.jsonData]) } +def getPistonLimits(){ + return isHubitat() ? [ + schedule: 20000, + scheduleVariance: 3000, + executionTime: 30000, + taskRemaining: 3000, + taskDelayMax: 5000, + maxStats: settings.maxStats ?: 50, + maxLogs: settings.maxLogs ?: 50, + recovery: 45 + ] : [ + schedule: 5000, + scheduleVariance: 2000, + executionTime: 20000, + taskRemaining: 10000, + taskDelayMax: 5000, + maxStats: 500, + maxLogs: 500 + ] +} //entry point for all events def handleEvents(event) { //cancel all pending jobs, we'll handle them later - //unschedule(timeHandler) + if(isHubitat()) unschedule(timeHandler) if (!state.active) return def startTime = now() state.lastExecuted = startTime @@ -812,8 +871,13 @@ def handleEvents(event) { return; } checkVersion(rtData) - setTimeoutRecoveryHandler('timeoutRecoveryHandler_webCoRE') - //runIn(30, timeRecoveryHandler) + if(isHubitat()) { + runIn(rtData.pistonLimits.recovery.toInteger(), timeRecoveryHandler) + } + else { + setTimeoutRecoveryHandler('timeoutRecoveryHandler_webCoRE') + } + if (rtData.semaphoreDelay) { warn "Piston waited at a semaphore for ${rtData.semaphoreDelay}ms", rtData } @@ -835,8 +899,8 @@ def handleEvents(event) { } //process all time schedules in order def t = now() - while (success && (20000 + rtData.timestamp - now() > 15000)) { - //we only keep doing stuff if we haven't passed the 10s execution time mark + + while (success && (rtData.pistonLimits.executionTime + rtData.timestamp - now() > rtData.pistonLimits.schedule)) { def schedules = rtData.piston.o?.pep ? atomicState.schedules : state.schedules //anything less than 2 seconds in the future is considered due, we'll do some pause to sync with it //we're doing this because many times, the scheduler will run a job early, usually 0-1.5 seconds early... @@ -844,7 +908,7 @@ def handleEvents(event) { if (event.name == 'wc_async_reply') { event.schedule = schedules.sort{ it.t }.find{ it.d == event.value } } else { - event = [date: event.date, device: location, name: 'time', value: now(), schedule: schedules.sort{ it.t }.find{ it.t < now() + 2000 }] + event = [date: event.date, device: location, name: 'time', value: now(), schedule: schedules.sort{ it.t }.find{ it.t < now() + rtData.pistonLimits.scheduleVariance }] } if (!event.schedule) break long threshold = now() > event.schedule.t ? now() : event.schedule.t @@ -895,7 +959,7 @@ def handleEvents(event) { if (rtData.logging > 1) trace msg2, rtData if (!success) msg.m = "Event processing failed" finalizeEvent(rtData, msg, success) - if (rtData.currentEvent) { + if (rtData.currentEvent && rtData.logPistonExecutions) { try { def desc = 'webCore piston \'' + app.label + '\' was executed' sendLocationEvent(name: 'webCoRE', value: 'pistonExecuted', isStateChange: true, displayed: false, linkText: desc, descriptionText: desc, data: [ @@ -936,9 +1000,10 @@ private Boolean executeEvent(rtData, event) { rtData.currentEvent = [ date: event.date.getTime(), delay: rtData.stats?.timing?.d ?: 0, - device: srcEvent ? srcEvent.device : hashId((event.device?:location).id), + device: srcEvent ? srcEvent.device : hashId((event.device?:location).id + (isHubitat() ? !isDeviceLocation(device) ? '' : '-L' : '')), name: srcEvent ? srcEvent.name : event.name, value: srcEvent ? srcEvent.value : event.value, + descriptionText: srcEvent ? srcEvent.descriptionText : event.descriptionText, unit: srcEvent ? srcEvent.unit : event.unit, physical: srcEvent ? srcEvent.physical : !!event.physical, index: index @@ -959,6 +1024,7 @@ private Boolean executeEvent(rtData, event) { setSystemVariableValue(rtData, '$previousEventDevice', [rtData.previousEvent?.device]) setSystemVariableValue(rtData, '$previousEventDeviceIndex', rtData.previousEvent?.index ?: 0) setSystemVariableValue(rtData, '$previousEventAttribute', rtData.previousEvent?.name ?: '') + setSystemVariableValue(rtData, '$previousEventDescription', rtData.currentEvent.descriptionText ?: '') setSystemVariableValue(rtData, '$previousEventValue', rtData.previousEvent?.value ?: '') setSystemVariableValue(rtData, '$previousEventUnit', rtData.previousEvent?.unit ?: '') setSystemVariableValue(rtData, '$previousEventDevicePhysical', !!rtData.previousEvent?.physical) @@ -968,6 +1034,7 @@ private Boolean executeEvent(rtData, event) { setSystemVariableValue(rtData, '$currentEventDevice', [rtData.currentEvent?.device]) setSystemVariableValue(rtData, '$currentEventDeviceIndex', (rtData.currentEvent.index != '') && (rtData.currentEvent.index != null) ? rtData.currentEvent.index : 0) setSystemVariableValue(rtData, '$currentEventAttribute', rtData.currentEvent.name ?: '') + setSystemVariableValue(rtData, '$currentEventDescription', rtData.currentEvent.descriptionText ?: '') setSystemVariableValue(rtData, '$currentEventValue', rtData.currentEvent.value ?: '') setSystemVariableValue(rtData, '$currentEventUnit', rtData.currentEvent.unit ?: '') setSystemVariableValue(rtData, '$currentEventDevicePhysical', !!rtData.currentEvent.physical) @@ -1028,7 +1095,7 @@ private finalizeEvent(rtData, initialMsg, success = true) { processSchedules(rtData, true) if (rtData.updateDevices) { - updateDeviceList(rtData.devices*.value.id) + updateDeviceList(rtData, rtData.devices*.value.id) } if (initialMsg) { if (success) { @@ -1044,7 +1111,7 @@ private finalizeEvent(rtData, initialMsg, success = true) { def stats = (rtData.piston.o?.pep ? atomicState.stats : state.stats) ?: [:] stats.timing = stats.timing ?: [] stats.timing.push(rtData.stats.timing) - if (stats.timing.size() > 500) stats.timing = stats.timing[stats.timing.size() - 500..stats.timing.size() - 1] + if (stats.timing.size() > rtData.pistonLimits.maxStats) stats.timing = stats.timing[stats.timing.size() - rtData.pistonLimits.maxStats..stats.timing.size() - 1] rtData.trace.d = now() - rtData.trace.t //temporary fix for migration from single to multiple tiles if (rtData.state.i || rtData.state.t) { @@ -1138,12 +1205,16 @@ private processSchedules(rtData, scheduleJob = false) { t = (t < 1 ? 1 : t) rtData.stats.nextSchedule = next.t if (rtData.logging) info "Setting up scheduled job for ${formatLocalTime(next.t)} (in ${t}s)" + (schedules.size() > 1 ? ', with ' + (schedules.size() - 1).toString() + ' more job' + (schedules.size() > 2 ? 's' : '') + ' pending' : ''), rtData - runIn(t, timeHandler, [data: next]) - //runIn(t + 30, timeRecoveryHandler, [data: next]) + runIn(t.toInteger(), timeHandler, [data: next]) + if(isHubitat()){ + runIn((t + rtData.pistonLimits.recovery).toInteger(), timeRecoveryHandler, [data: next]) + } } else { rtData.stats.nextSchedule = 0 //remove the recovery - //unschedule(timeRecoveryHandler) + if(isHubitat()){ + unschedule(timeRecoveryHandler) + } } } if (rtData.piston.o?.pep) atomicState.schedules = schedules @@ -1157,7 +1228,7 @@ private updateLogs(rtData) { //we only save the logs if we got some if (!rtData || !rtData.logs || (rtData.logs.size() < 2)) return def logs = (rtData.logs?:[]) + (atomicState.logs?:[]) - def maxLogSize = 500 + def maxLogSize = rtData.pistonLimits.maxLogs //we attempt to store 500 logs, but if that's too much, we go down in 50 increments while (maxLogSize >= 0) { if (logs.size() > maxLogSize) { @@ -1197,7 +1268,7 @@ private Boolean executeStatements(rtData, statements, async = false) { return true } -private Boolean executeStatement(rtData, statement, async = false) { +private Boolean executeStatement(rtData, statement, async = false) { //if rtData.fastForwardTo is a positive, non-zero number, we need to fast forward through all //branches until we find the task with an id equal to that number, then we play nicely after that if (!statement) return false @@ -1621,21 +1692,25 @@ private Boolean executeTask(rtData, devices, statement, task, async) { //ensure value type is successfuly passed through params.push p } + + //handle duplicate command "push" which was replaced with fake command "pushMomentary" + def override = rtData.commands.overrides.find { it.value.r == task.c } + def command = override ? override.value.c : task.c - def vcmd = rtData.commands.virtual[task.c] + def vcmd = rtData.commands.virtual[command] long delay = 0 for (device in (virtualDevice ? [virtualDevice] : devices)) { - if (!virtualDevice && device.hasCommand(task.c)) { - def msg = timer "Executed [$device].${task.c}" + if (!virtualDevice && device.hasCommand(command) && !(vcmd && vcmd.o /*virutal command overrides physical command*/)) { + def msg = timer "Executed [$device].${command}" try { - delay = "cmd_${task.c}"(rtData, device, params) + delay = "cmd_${command}"(rtData, device, params) } catch(all) { - executePhysicalCommand(rtData, device, task.c, params) + executePhysicalCommand(rtData, device, command, params) } if (rtData.logging > 1) trace msg, rtData } else { if (vcmd) { - delay = executeVirtualCommand(rtData, vcmd.a ? devices : device, task, params) + delay = executeVirtualCommand(rtData, vcmd.a ? devices : device, command, params) //aggregate commands only run once, for all devices at the same time if (vcmd.a) break } @@ -1644,12 +1719,13 @@ private Boolean executeTask(rtData, devices, statement, task, async) { //if we don't have to wait, we're home free if (delay) { //get remaining piston time - def timeLeft = 20000 + rtData.timestamp - now() + def timeLeft = rtData.pistonLimits.executionTime + rtData.timestamp - now() //negative delays force us to reschedule, no sleeping on this one boolean reschedule = (delay < 0) delay = reschedule ? -delay : delay - //we're aiming at waking up with at least 10s left - if (reschedule || (timeLeft - delay < 10000) || (delay >= 5000) || async) { + //we're aiming at waking up with at least 3s left + //keep executing until we hit 3 seconds before the total execution time limit + if (reschedule || (timeLeft - delay < rtData.pistonLimits.taskRemaining) || (delay >= rtData.pistonLimits.taskMaxDelay) || async) { //schedule a wake up if (rtData.logging > 1) trace "Requesting a wake up for ${formatLocalTime(now() + delay)} (in ${cast(rtData, delay / 1000, 'decimal')}s)", rtData tracePoint(rtData, "t:${task.$}", now() - t, -delay) @@ -1664,15 +1740,15 @@ private Boolean executeTask(rtData, devices, statement, task, async) { return true } -private long executeVirtualCommand(rtData, devices, task, params) +private long executeVirtualCommand(rtData, devices, command, params) { - def msg = timer "Executed virtual command ${devices ? (devices instanceof List ? "$devices." : "[$devices].") : ""}${task.c}" + def msg = timer "Executed virtual command ${devices ? (devices instanceof List ? "$devices." : "[$devices].") : ""}${command}" long delay = 0 try { - delay = "vcmd_${task.c}"(rtData, devices, params) + delay = "vcmd_${command}"(rtData, devices, params) if (rtData.logging > 1) trace msg, rtData } catch(all) { - msg.m = "Error executing virtual command ${devices instanceof List ? "$devices" : "[$devices]"}.${task.c}:" + msg.m = "Error executing virtual command ${devices instanceof List ? "$devices" : "[$devices]"}.${command}:" msg.e = all error msg, rtData } @@ -1680,6 +1756,10 @@ private long executeVirtualCommand(rtData, devices, task, params) } private executePhysicalCommand(rtData, device, command, params = [], delay = null, scheduleDevice = null, disableCommandOptimization = false) { + if(isHubitat() && (!!delay && !scheduleDevice)){ + //delay without schedules is not supported in hubitat + scheduleDevice = hashId(device.id) + } if (!!delay && !!scheduleDevice) { //we're using schedules instead def statement = rtData.currentAction @@ -1723,23 +1803,23 @@ private executePhysicalCommand(rtData, device, command, params = [], delay = nul } //if we're skipping, we already have a message if (skip) { - msg.m = "Skipped execution of physical command [${device.label}].$command($params) because it would make no change to the device." + msg.m = "Skipped execution of physical command [${device.label ?: device.name}].$command($params) because it would make no change to the device." } else { if (params.size()) { - if (delay) { + if (delay) { //not supported in hubitat device."$command"((params as Object[]) + [delay: delay]) - msg.m = "Executed physical command [${device.label}].$command($params, [delay: $delay])" + msg.m = "Executed physical command [${device.label ?: device.name}].$command($params, [delay: $delay])" } else { device."$command"(params as Object[]) - msg.m = "Executed physical command [${device.label}].$command($params)" + msg.m = "Executed physical command [${device.label ?: device.name}].$command($params)" } } else { - if (delay) { + if (delay) { //not supported in hubitat device."$command"([delay: delay]) - msg.m = "Executed physical command [${device.label}].$command([delay: $delay])" + msg.m = "Executed physical command [${device.label ?: device.name}].$command([delay: $delay])" } else { device."$command"() - msg.m = "Executed physical command [${device.label}].$command()" + msg.m = "Executed physical command [${device.label ?: device.name}].$command()" } } } @@ -1809,9 +1889,10 @@ private scheduleTimer(rtData, timer, long lastRun = 0) { //switch to local date/times - time = utcToLocalTime(time) - long rightNow = utcToLocalTime(now()) - lastRun = lastRun ? utcToLocalTime(lastRun) : rightNow + //hubitat timezone is already local + time = isHubitat() ? time : utcToLocalTime(time) + long rightNow = isHubitat() ? now() : utcToLocalTime(now()) + lastRun = lastRun ? (isHubitat() ? lastRun : utcToLocalTime(lastRun)) : rightNow long nextSchedule = lastRun if (lastRun > rightNow) { @@ -1927,7 +2008,7 @@ private scheduleTimer(rtData, timer, long lastRun = 0) { if (nextSchedule > lastRun) { //convert back to UTC - nextSchedule = localToUtcTime(nextSchedule) + nextSchedule = isHubitat() ? nextSchedule : localToUtcTime(nextSchedule) rtData.schedules.removeAll{ it.s == timer.$ } requestWakeUp(rtData, timer, [$: -1], nextSchedule) } @@ -2185,8 +2266,8 @@ private long cmd_setColorTemperature(rtData, device, params) { return 0 } -private getColor(colorValue) { - def color = (colorValue == 'Random') ? colorUtil?.RANDOM : colorUtil?.findByName(colorValue) +private getColor(rtData, colorValue) { + def color = (colorValue == 'Random') ? (colorUtil?.RANDOM ?: getRandomColor(rtData)) : (colorUtil?.findByName(colorValue) ?: getColorByName(rtData, colorValue)) if (color) { color = [ hex: color.rgb, @@ -2209,7 +2290,7 @@ private getColor(colorValue) { } private long cmd_setColor(rtData, device, params) { - def color = getColor(params[0]) + def color = getColor(rtData, params[0]) if (!color) { error "ERROR: Invalid color $params", rtData return 0 @@ -2224,7 +2305,7 @@ private long cmd_setColor(rtData, device, params) { } private long cmd_setAdjustedColor(rtData, device, params) { - def color = getColor(params[0]) + def color = getColor(rtData, params[0]) if (!color) { error "ERROR: Invalid color $params", rtData return 0 @@ -2297,8 +2378,8 @@ private long vcmd_setState(rtData, device, params) { private long vcmd_setTileColor(rtData, device, params) { int index = cast(rtData, params[0], 'integer') if ((index < 1) || (index > 16)) return 0 - rtData.state["c$index"] = getColor(params[1])?.hex - rtData.state["b$index"] = getColor(params[2])?.hex + rtData.state["c$index"] = getColor(rtData, params[1])?.hex + rtData.state["b$index"] = getColor(rtData, params[2])?.hex rtData.state["f$index"] = !!params[3] return 0 } @@ -2330,8 +2411,8 @@ private long vcmd_setTile(rtData, device, params) { rtData.state["i$index"] = params[1] rtData.state["t$index"] = params[2] rtData.state["o$index"] = params[3] - rtData.state["c$index"] = getColor(params[4])?.hex - rtData.state["b$index"] = getColor(params[5])?.hex + rtData.state["c$index"] = getColor(rtData, params[4])?.hex + rtData.state["b$index"] = getColor(rtData, params[5])?.hex rtData.state["f$index"] = !!params[6] return 0 } @@ -2362,9 +2443,12 @@ private long vcmd_setLocationMode(rtData, device, params) { private long vcmd_setAlarmSystemStatus(rtData, device, params) { def statusIdOrName = params[0] - def status = rtData.virtualDevices['alarmSystemStatus']?.o?.find{ (it.key == statusIdOrName) || (it.value == statusIdOrName)}.collect{ [id: it.key, name: it.value] } + def dev = rtData.virtualDevices['alarmSystemStatus'] + def options = isHubitat() ? dev?.ac : dev?.o + def status = options?.find{ (it.key == statusIdOrName) || (it.value == statusIdOrName)}.collect{ [id: it.key, name: it.value] } + if (status && status.size()) { - sendLocationEvent(name: 'alarmSystemStatus', value: status[0].id) + sendLocationEvent(name: (isHubitat() ? 'hsmSetArm' : 'alarmSystemStatus'), value: status[0].id) } else { error "Error setting SmartThings Home Monitor status. Status '$statusIdOrName' does not exist.", rtData } @@ -2654,6 +2738,10 @@ private long vcmd_internal_fade(Map rtData, device, String command, int startLev return duration + 100 } +private long vcmd_emulatedFlash(rtData, device, params) { + vcmd_flash(rtData, device, params) +} + private long vcmd_flash(rtData, device, params) { long onDuration = cast(rtData, params[0], 'long') long offDuration = cast(rtData, params[1], 'long') @@ -2721,9 +2809,9 @@ private long vcmd_flashLevel(rtData, device, params) { } private long vcmd_flashColor(rtData, device, params) { - def color1 = getColor(params[0]) + def color1 = getColor(rtData, params[0]) long duration1 = cast(rtData, params[1], 'long') - def color2 = getColor(params[2]) + def color2 = getColor(rtData, params[2]) long duration2 = cast(rtData, params[3], 'long') int cycles = cast(rtData, params[4], 'integer') def state = params.size() > 5 ? params[5] : "" @@ -2886,12 +2974,13 @@ private long vcmd_wolRequest(rtData, device, params) { def mac = params[0] def secureCode = params[1] mac = mac.replace(":", "").replace("-", "").replace(".", "").replace(" ", "").toLowerCase() - sendHubCommand(new physicalgraph.device.HubAction( - "wake on lan $mac", - physicalgraph.device.Protocol.LAN, - null, - secureCode ? [secureCode: secureCode] : [:] - )) + + sendHubCommand(HubActionClass().newInstance( + "wake on lan $mac", + HubProtocolClass().LAN, + null, + secureCode ? [secureCode: secureCode] : [:] + )) return 0 } @@ -3003,7 +3092,7 @@ private long vcmd_lifxState(rtData, device, params) { return 0 } def power = params[1] - def color = getColor(params[2]) + def color = getColor(rtData, params[2]) def level = params[3] def infraredLevel = params[4] double duration = cast(rtData, params[5], 'long') / 1000 @@ -3078,8 +3167,8 @@ private long vcmd_lifxBreathe(rtData, device, params) { error "Sorry, could not find the specified LIFX selector.", rtData return 0 } - def color = getColor(params[1]) - def fromColor = (params[2] == null) ? null : getColor(params[2]) + def color = getColor(rtData, params[1]) + def fromColor = (params[2] == null) ? null : getColor(rtData, params[2]) def period = (params[3] == null) ? null : cast(rtData, params[3], 'long') / 1000 def cycles = params[4] def peak = params[5] @@ -3120,8 +3209,8 @@ private long vcmd_lifxPulse(rtData, device, params) { error "Sorry, could not find the specified LIFX selector.", rtData return 0 } - def color = getColor(params[1]) - def fromColor = (params[2] == null) ? null : getColor(params[2]) + def color = getColor(rtData, params[1]) + def fromColor = (params[2] == null) ? null : getColor(rtData, params[2]) def period = (params[3] == null) ? null : cast(rtData, params[3], 'long') / 1000 def cycles = params[4] def powerOn =(params[5] == null)? null : cast(rtData, params[5], 'boolean') @@ -3151,7 +3240,7 @@ private long vcmd_lifxPulse(rtData, device, params) { } -public localHttpRequestHandler(physicalgraph.device.HubResponse hubResponse) { +public localHttpRequestHandler(hubResponse) { def responseCode = '' for (header in hubResponse.headers) { if (header.key.startsWith('http')) { @@ -3246,7 +3335,7 @@ private long vcmd_httpRequest(rtData, device, params) { data[variable] = getVariable(rtData, variable).v } } - if (internal) { + if (internal && !isHubitat()) { try { if (rtData.logging > 2) debug "Sending internal web request to: $userPart$uri", rtData def ip = ((uri.indexOf("/") > 0) ? uri.substring(0, uri.indexOf("/")) : uri) @@ -3260,7 +3349,7 @@ private long vcmd_httpRequest(rtData, device, params) { query: useQueryString ? data : null, //thank you @destructure00 body: !useQueryString ? data : null //thank you @destructure00 ] - sendHubCommand(new physicalgraph.device.HubAction(requestParams, null, [callback: localHttpRequestHandler])) + sendHubCommand(HubActionClass().newInstance(requestParams, null, [callback: localHttpRequestHandler])) return 20000 } catch (all) { error "Error executing internal web request: ", rtData, null, all @@ -3275,6 +3364,7 @@ private long vcmd_httpRequest(rtData, device, params) { requestContentType: (method == "GET" || requestBodyType == "FORM") ? "application/x-www-form-urlencoded" : (requestBodyType == "JSON") ? "application/json" : contentType, body: !useQueryString ? data : null ] + def func = "" switch(method) { case "GET": @@ -3338,22 +3428,40 @@ private long vcmd_writeToFuelStream(rtData, device, params) { def name = params[1] def data = params[2] def source = params[3] - def requestParams = [ - uri: "https://api-${rtData.region}-${rtData.instanceId[32]}.webcore.co:9247", - path: "/fuelStream/write", - headers: [ - 'ST' : rtData.instanceId - ], - body: [ - c: canister, - n: name, - s: source, - d: data, - i: rtData.instanceId - ], - requestContentType: "application/json" + + def req = [ + c: canister, + n: name, + s: source, + d: data, + i: rtData.instanceId ] - if (asynchttp_v1) asynchttp_v1.put(null, requestParams) + + if(rtData.useLocalFuelStreams){ + parent.writeToFuelStream(req) + } + else if(!isHubitat()){ + def requestParams = [ + uri: "https://api-${rtData.region}-${rtData.instanceId[32]}.webcore.co:9247", + path: "/fuelStream/write", + headers: [ + 'ST' : rtData.instanceId + ], + body: [ + c: canister, + n: name, + s: source, + d: data, + i: rtData.instanceId + ], + requestContentType: "application/json" + ] + if (asynchttp_v1) asynchttp_v1.put(null, requestParams) + } + else { + log.error "Fuel stream app is not installed. Install it to write to local fuel streams" + } + return 0 } @@ -3641,7 +3749,7 @@ private evaluateOperand(rtData, node, operand, index = null, trigger = false, ne case 'd': //devices def deviceIds = [] for (d in expandDeviceList(rtData, operand.d)) { - if (getDevice(rtData, d)) deviceIds.push(d) + if (getDevice(rtData, d)) deviceIds.push(d) } /* for (d in rtData, operand.d) { @@ -3666,6 +3774,15 @@ private evaluateOperand(rtData, node, operand, index = null, trigger = false, ne case 'alarmSystemStatus': values = [[i: "${node?.$}:v", v:getDeviceAttribute(rtData, rtData.locationId, operand.v)]]; break; + case 'alarmSystemAlert': + values = [[i: "${node?.$}:v", v:[t: 'string', v: (rtData.event.name == 'hsmAlert' ? rtData.event.value : null)]]] + break; + case 'alarmSystemEvent': + values = [[i: "${node?.$}:v", v:[t: 'string', v: (rtData.event.name == 'hsmSetArm' ? rtData.event.value : null)]]] + break; + case 'alarmSystemRule': + values = [[i: "${node?.$}:v", v:[t: 'string', v: (rtData.event.name == 'hsmRules' ? rtData.event.value : null)]]] + break; case 'powerSource': values = [[i: "${node?.$}:v", v:[t: 'enum', v:rtData.powerSource]]]; break; @@ -3963,7 +4080,7 @@ private Boolean evaluateComparison(rtData, comparison, lo, ro = null, ro2 = null case 'time': case 'date': case 'datetime': - boolean pass = checkTimeRestrictions(rtData, lo.operand, utcToLocalTime(), 5, 1) == 0 + boolean pass = checkTimeRestrictions(rtData, lo.operand, isHubitat() ? now() : utcToLocalTime(), 5, 1) == 0 if (rtData.logging > 2) debug "Time restriction check ${pass ? 'passed' : 'failed'}", rtData if (!pass) res = false; } @@ -4304,6 +4421,7 @@ private traverseExpressions(node, closure, param, parentNode = null) { } private getRoutineById(routineId) { + if(isHubitat()) return [ id : routineId ] def routines = location.helloHome?.getPhrases() for(routine in routines) { if (routine && routine?.label && (hashId(routine.id) == routineId)) { @@ -4313,8 +4431,9 @@ private getRoutineById(routineId) { return null } -private void updateDeviceList(deviceIdList) { - app.updateSetting('dev', [type: 'capability.device', value: deviceIdList.unique()]) +private void updateDeviceList(rtData, deviceIdList) { + if(isHubitat() && deviceIdList && !settings.dev) debug "Unable to update setting 'dev' from child app. Open child app '$app.label' on the Hubitat apps page and click 'Done' for faster operation", rtData + app.updateSetting('dev', [type: isHubitat() ? 'capability' : 'capability.device', value: deviceIdList.unique()]) } private void updateContactList(contactIdList) { @@ -4361,7 +4480,7 @@ private void subscribeAll(rtData) { if ((expression.t == 'variable') && expression.x && expression.x.startsWith('@')) { subscriptionId = "${expression.x}" deviceId = rtData.locationId - attribute = "${expression.x.startsWith('@@') ? '@@' + handle() : rtData.instanceId}.${expression.x}" + attribute = "${expression.x.startsWith('@@') ? '@@' + handle() : rtData.instanceId}${isHubitat() ? "" : ".${expression.x}"}" } if (subscriptionId && deviceId) { def ct = subscriptions[subscriptionId]?.t ?: null @@ -4403,12 +4522,27 @@ private void subscribeAll(rtData) { def subscriptionId = null def attribute = null switch (operand.v) { + case 'alarmSystemStatus': + subscriptionId = "$deviceId${operand.v}" + attribute = isHubitat() ? "hsmStatus" : operand.v + break; + case 'alarmSystemAlert': + subscriptionId = "$deviceId${operand.v}" + attribute = "hsmAlert" + break; + case 'alarmSystemEvent': + subscriptionId = "$deviceId${operand.v}" + attribute = "hsmSetArm" + break; + case 'alarmSystemRule': + subscriptionId = "$deviceId${operand.v}" + attribute = "hsmRules" + break; case 'time': case 'date': case 'datetime': case 'mode': case 'powerSource': - case 'alarmSystemStatus': subscriptionId = "$deviceId${operand.v}" attribute = operand.v break @@ -4417,13 +4551,13 @@ private void subscribeAll(rtData) { def routine = getRoutineById(value.c) if (routine) { subscriptionId = "$deviceId${operand.v}${routine.id}" - attribute = "routineExecuted.${routine.id}" + attribute = "routineExecuted${isHubitat() ? "" : ("." + routine.id)}" } } break case 'email': subscriptionId = "$deviceId${operand.v}${hashId(app.id)}" - attribute = "email.${hashId(app.id)}" + attribute = "email${isHubitat() ? "" : ("." + hashId(app.id))}" break case 'ifttt': case 'askAlexa': @@ -4432,14 +4566,16 @@ private void subscribeAll(rtData) { def options = rtData.virtualDevices[operand.v]?.o def item = options ? options[value.c] : value.c if (item) { - subscriptionId = "$deviceId${operand.v}${item}" - attribute = "${operand.v}.${item}" + subscriptionId = "$deviceId${operand.v}${item}" + + def attrVal = isHubitat() ? "" : ".${item}" + attribute = "${operand.v}${attrVal}" switch (operand.v) { case 'askAlexa': - attribute = "askAlexaMacro.${item}" + attribute = "askAlexaMacro${attrVal}" break; case 'echoSistant': - attribute = "echoSistantProfile.${item}" + attribute = "echoSistantProfile${attrVal}" break; } } @@ -4460,7 +4596,7 @@ private void subscribeAll(rtData) { case 'x': if (operand.x && operand.x.startsWith('@')) { def subscriptionId = operand.x - def attribute = "${operand.x.startsWith('@@') ? '@@' + handle() : rtData.instanceId}.${operand.x}" + def attribute = "${operand.x.startsWith('@@') ? '@@' + handle() : rtData.instanceId}${ isHubitat() ? "" : ".${operand.x}"}" def ct = subscriptions[subscriptionId]?.t ?: null if ((ct == 'trigger') || (comparisonType == 'trigger')) { ct = 'trigger' @@ -4627,7 +4763,7 @@ private void subscribeAll(rtData) { //save devices List deviceIdList = rawDevices.collect{ it && it.value ? it.value.id : null } deviceIdList.removeAll{ it == null } - updateDeviceList(deviceIdList) + updateDeviceList(rtData, deviceIdList) //save contacts List contactIdList = rawContacts.collect{ it && it.value ? it.value.id : null } contactIdList.removeAll{ it == null } @@ -4699,7 +4835,11 @@ private getDevice(rtData, idOrName) { if (rtData.locationId == idOrName) return location def device = rtData.devices[idOrName] ?: rtData.devices.find{ it.value.getDisplayName() == idOrName }?.value if (!device) { - if (!rtData.allDevices) rtData.allDevices = parent.listAvailableDevices(true) + if (!rtData.allDevices){ + def msg = timer "Device missing from piston. Loading all from parent..." + rtData.allDevices = parent.listAvailableDevices(true) + if (rtData.logging > 2) debug msg, rtData + } if (rtData.allDevices) { def deviceMap = rtData.allDevices.find{ (idOrName == it.key) || (idOrName == it.value.getDisplayName()) } if (deviceMap) { @@ -4747,9 +4887,9 @@ private Map getDeviceAttribute(rtData, deviceId, attributeName, subDeviceIndex = case 'mode': def mode = location.getCurrentMode(); return [t: 'string', v: hashId(mode.getId()), n: mode.getName()] - case 'alarmSystemStatus': - def v = hubUID ? 'off' : location.currentState("alarmSystemStatus")?.value - def n = hubUID ? 'Disarmed' : rtData.virtualDevices['alarmSystemStatus']?.o[v] + case 'alarmSystemStatus': + def v = isHubitat() ? (rtData.hsmStatus) : location.currentState("alarmSystemStatus")?.value + def n = rtData.virtualDevices['alarmSystemStatus']?.o[v] return [t: 'string', v: v, n: n] } return [t: 'string', v: location.getName().toString()] @@ -4765,7 +4905,9 @@ private Map getDeviceAttribute(rtData, deviceId, attributeName, subDeviceIndex = if (attributeName == 'hue') { value = cast(rtData, cast(rtData, value, 'decimal') * 3.6, attribute.t) } - return [t: attribute.t, v: value, d: deviceId, a: attributeName, i: subDeviceIndex, x: (!!attribute.m || !!trigger) && ((device?.id != (rtData.event.device?:location).id) || (((attributeName == 'orientation') || (attributeName == 'axisX') || (attributeName == 'axisY') || (attributeName == 'axisZ') ? 'threeAxis' : attributeName) != rtData.event.name))] + //have to compare ids and type for hubitat since the locationid can be the same as the deviceid + def deviceMatch = (device?.id == (rtData.event.device?:location).id) && ( isDeviceLocation(device) == isDeviceLocation((rtData.event.device?:location))) + return [t: attribute.t, v: value, d: deviceId, a: attributeName, i: subDeviceIndex, x: (!!attribute.m || !!trigger) && (!deviceMatch || (((attributeName == 'orientation') || (attributeName == 'axisX') || (attributeName == 'axisY') || (attributeName == 'axisZ') ? 'threeAxis' : attributeName) != rtData.event.name))] } return [t: "error", v: "Device '${deviceId}' not found"] } @@ -5015,7 +5157,7 @@ private Map getIncidents(rtData, name) { private initIncidents(rtData) { if (rtData.incidents instanceof List) return; def incidentThreshold = now() - 604800000 - rtData.incidents = hubUID ? [] : location.activeIncidents.collect{[date: it.date.time, title: it.getTitle(), message: it.getMessage(), args: it.getMessageArgs(), sourceType: it.getSourceType()]}.findAll{ it.date >= incidentThreshold } + rtData.incidents = isHubitat() ? [] : location.activeIncidents.collect{[date: it.date.time, title: it.getTitle(), message: it.getMessage(), args: it.getMessageArgs(), sourceType: it.getSourceType()]}.findAll{ it.date >= incidentThreshold } } private Map getVariable(rtData, name) { @@ -6148,9 +6290,9 @@ private func_rainbowvalue(rtData, params) { } def input = evaluateExpression(rtData, params[0], 'integer').v def minInput = evaluateExpression(rtData, params[1], 'integer').v - def minColor = getColor(evaluateExpression(rtData, params[2], 'string').v) + def minColor = getColor(rtData, evaluateExpression(rtData, params[2], 'string').v) def maxInput = evaluateExpression(rtData, params[3], 'integer').v - def maxColor = getColor(evaluateExpression(rtData, params[4], 'string').v) + def maxColor = getColor(rtData, evaluateExpression(rtData, params[4], 'string').v) if (minInput > maxInput) { def x = minInput minInput = maxInput @@ -6520,7 +6662,7 @@ private func_previousage(rtData, params) { def param = evaluateExpression(rtData, params[0], 'device') if ((param.t == 'device') && (param.a) && param.v.size()) { def device = getDevice(rtData, param.v[0]) - if (device && (device.id != location.id)) { + if (device && !isDeviceLocation(device)) { def states = device.statesSince(param.a, new Date(now() - 604500000), [max: 5]) if (states.size() > 1) { def newValue = states[0].getValue() @@ -6552,7 +6694,7 @@ private func_previousvalue(rtData, params) { def attribute = rtData.attributes[param.a] if (attribute) { def device = getDevice(rtData, param.v[0]) - if (device && (device.id != location.id)) { + if (device && !isDeviceLocation(device)) { def states = device.statesSince(param.a, new Date(now() - 604500000), [max: 5]) if (states.size() > 1) { def newValue = states[0].getValue() @@ -7431,7 +7573,8 @@ private utcToLocalDate(dateOrTimeOrString = null) { dateOrTimeOrString = now() } if (dateOrTimeOrString instanceof Long) { - return new Date(dateOrTimeOrString + (location.timeZone ? location.timeZone.getOffset(dateOrTimeOrString) : 0)) + //ST the system time is UTC, hubitat is user's local timezone. No need to convert + return new Date(dateOrTimeOrString + ( (!isHubitat() && location.timeZone) ? location.timeZone.getOffset(dateOrTimeOrString) : 0)) } return null } @@ -7468,6 +7611,16 @@ private localToUtcDate(dateOrTime) { return null } +private safeTimeToday(dateOrTimeOrString, tz = null){ + if(isHubitat()){ + dateOrTimeOrString = dateOrTimeOrString?.trim() ?: "" + if(dateOrTimeOrString.toLowerCase().endsWith('am') || dateOrTimeOrString.toLowerCase().endsWith('pm')){ + dateOrTimeOrString = dateOrTimeOrString[0..-3].trim() + } + } + return timeToday(dateOrTimeOrString, tz) +} + private localToUtcTime(dateOrTimeOrString) { if (dateOrTimeOrString instanceof Date) { //get unix time @@ -7498,7 +7651,7 @@ private localToUtcTime(dateOrTimeOrString) { } catch (all4) { } } - long time = timeToday(dateOrTimeOrString, tz).getTime() + long time = safeTimeToday(dateOrTimeOrString, tz).getTime() //adjust for PM - timeToday has no clue.... dateOrTimeOrString = dateOrTimeOrString.trim().toLowerCase() def twelve = dateOrTimeOrString.startsWith('12') @@ -7636,6 +7789,11 @@ private List hexToRgbArray(hex) { return [0, 0, 0]; } +//hubitat device ids can be the same as the location id +private isDeviceLocation(device){ + return device?.id.toString() == location.id.toString() && (isHubitat() ? ((device?.hubs?.size() ?: 0) > 0) : true) +} + /******************************************************************************/ /*** DEBUG FUNCTIONS ***/ /******************************************************************************/ @@ -7699,8 +7857,14 @@ private log(message, rtData = null, shift = null, err = null, cmd = null, force rtData.logs.push([o: now() - rtData.timestamp, p: prefix2, m: msg + (!!err ? " $err" : ""), c: cmd]) } } - if (hubUID) { - log."$cmd" "$prefix $message" + if (isHubitat()) { + if(err){ + log."$cmd" "$prefix $message $err" + } + else { + log."$cmd" "$prefix $message" + } + } else { log."$cmd" "$prefix $message", err } @@ -7811,9 +7975,9 @@ private getNextNoonTime(rtData) { return localToUtcTime(rightNow - rightNow.mod(86400000) + 43200000) } -private Map getLocalVariables(rtData, vars) { +private Map getLocalVariables(rtData, vars, atomState) { rtData.localVars = [:] - def values = atomicState.vars + def values = atomState.vars for (var in vars) { def variable = [t: var.t, v: var.v ?: (var.t.endsWith(']') ? (values[var.n] instanceof Map ? values[var.n] : {}) : cast(rtData, values[var.n], var.t)), f: !!var.v] //f means fixed value - we won't save this to the state if (rtData && var.v && (var.a == 's') && !var.t.endsWith(']')) { @@ -7833,7 +7997,7 @@ def Map getSystemVariablesAndValues(rtData) { return result } -private static Map getSystemVariables() { +private Map getSystemVariables() { return [ '$args': [t: "dynamic", d: true], '$json': [t: "dynamic", d: true], @@ -7844,6 +8008,7 @@ private static Map getSystemVariables() { '$incidents': [t: "dynamic", d: true], '$shmTripped': [t: "boolean", d: true], "\$currentEventAttribute": [t: "string", v: null], + "\$currentEventDescription": [t: "string", v: null], "\$currentEventDate": [t: "datetime", v: null], "\$currentEventDelay": [t: "integer", v: null], "\$currentEventDevice": [t: "device", v: null], @@ -7916,7 +8081,7 @@ private static Map getSystemVariables() { "\$iftttStatusCode": [t: "integer", v: null], "\$iftttStatusOk": [t: "boolean", v: null], "\$locationMode": [t: "string", d: true], - "\$shmStatus": [t: "string", d: true], + (isHubitat() ? "\$hsmStatus" : "\$shmStatus"): [t: "string", d: true], "\$version": [t: "string", d: true] ].sort{it.key} } @@ -7963,13 +8128,15 @@ private getSystemVariableValue(rtData, name) { case "\$time": def t = localDate(); def h = t.hours; def m = t.minutes; return (h == 0 ? 12 : (h > 12 ? h - 12 : h)) + ":" + (m < 10 ? "0$m" : "$m") + " " + (h <12 ? "A.M." : "P.M.") case "\$time24": def t = localDate(); def h = t.hours; def m = t.minutes; return h + ":" + (m < 10 ? "0$m" : "$m") case "\$random": def result = getRandomValue("\$random") ?: (double)Math.random(); setRandomValue("\$random", result); return result - case "\$randomColor": def result = getRandomValue("\$randomColor") ?: colorUtil?.RANDOM?.rgb; setRandomValue("\$randomColor", result); return result - case "\$randomColorName": def result = getRandomValue("\$randomColorName") ?: colorUtil?.RANDOM?.name; setRandomValue("\$randomColorName", result); return result + case "\$randomColor": def result = getRandomValue("\$randomColor") ?: (colorUtil?.RANDOM ?: getRandomColor(rtData))?.rgb; setRandomValue("\$randomColor", result); return result + case "\$randomColorName": def result = getRandomValue("\$randomColorName") ?: (colorUtil?.RANDOM ?: getRandomColor(rtData))?.name; setRandomValue("\$randomColorName", result); return result case "\$randomLevel": def result = getRandomValue("\$randomLevel") ?: (int)Math.round(100 * Math.random()); setRandomValue("\$randomLevel", result); return result case "\$randomSaturation": def result = getRandomValue("\$randomSaturation") ?: (int)Math.round(50 + 50 * Math.random()); setRandomValue("\$randomSaturation", result); return result case "\$randomHue": def result = getRandomValue("\$randomHue") ?: (int)Math.round(360 * Math.random()); setRandomValue("\$randomHue", result); return result case "\$locationMode": return location.getMode() - case "\$shmStatus": switch (hubUID ? 'off' : location.currentState("alarmSystemStatus")?.value) { case 'off': return 'Disarmed'; case 'stay': return 'Armed/Stay'; case 'away': return 'Armed/Away'; }; return null; + case (isHubitat() ? "\$hsmStatus" : "\$shmStatus"): + if(isHubitat()) { return rtData.hsmStatus } + else switch (location.currentState("alarmSystemStatus")?.value) { case 'off': return 'Disarmed'; case 'stay': return 'Armed/Stay'; case 'away': return 'Armed/Away'; }; return null; } } @@ -7996,3 +8163,30 @@ private void resetRandomValues() { state.temp = state.temp ?: [:] state.temp.randoms = [:] } + +public Map getColorByName(rtData, name){ + return (rtData.colors ?: parent.getColors()).find{ it.name == name } +} +public Map getRandomColor(rtData){ + def colors = (rtData.colors ?: parent.getColors()) + def random = (int)(Math.random() * colors.size()) + return colors[random] +} + +private static Class HubActionClass() { + try { + return 'physicalgraph.device.HubAction' as Class + } catch(all) { + return 'hubitat.device.HubAction' as Class + } +} +private static Class HubProtocolClass() { + try { + return 'physicalgraph.device.Protocol' as Class + } catch(all) { + return 'hubitat.device.Protocol' as Class + } +} +private isHubitat(){ + return hubUID != null +} \ No newline at end of file diff --git a/smartapps/ady624/webcore-storage.src/webcore-storage.groovy b/smartapps/ady624/webcore-storage.src/webcore-storage.groovy index fe8707af..b76982d1 100644 --- a/smartapps/ady624/webcore-storage.src/webcore-storage.groovy +++ b/smartapps/ady624/webcore-storage.src/webcore-storage.groovy @@ -120,6 +120,9 @@ private initialize() { /*** ***/ /******************************************************************************/ +public getStorageSettings(){ + settings +} def initData(devices, contacts) { if (devices) { for(item in devices) { @@ -133,13 +136,22 @@ def initData(devices, contacts) { } def Map listAvailableDevices(raw = false) { + def overrides = commandOverrides() if (raw) { return settings.findAll{ it.key.startsWith("dev:") }.collect{ it.value }.flatten().collectEntries{ dev -> [(hashId(dev.id)): dev]} } else { - return settings.findAll{ it.key.startsWith("dev:") }.collect{ it.value }.flatten().collectEntries{ dev -> [(hashId(dev.id)): dev]}.collectEntries{ id, dev -> [ (id): [ n: dev.getDisplayName(), cn: dev.getCapabilities()*.name, a: dev.getSupportedAttributes().unique{ it.name }.collect{def x = [n: it.name, t: it.getDataType(), o: it.getValues()]; try {x.v = dev.currentValue(x.n);} catch(all) {}; x}, c: dev.getSupportedCommands().unique{ it.getName() }.collect{[n: it.getName(), p: it.getArguments()]} ]]} + return settings.findAll{ it.key.startsWith("dev:") }.collect{ it.value }.flatten().collectEntries{ dev -> [(hashId(dev.id)): dev]}.collectEntries{ id, dev -> [ (id): [ n: dev.getDisplayName(), cn: dev.getCapabilities()*.name, a: dev.getSupportedAttributes().unique{ it.name }.collect{def x = [n: it.name, t: it.getDataType(), o: it.getValues()]; try {x.v = dev.currentValue(x.n);} catch(all) {}; x}, c: dev.getSupportedCommands().unique{ transformCommand(it, overrides) }.collect{[n: transformCommand(it, overrides), p: it.getArguments()]} ]]} } } +private def transformCommand(command, overrides){ + def override = overrides[command.getName()] + if(override && override.s == command.getArguments()?.toString()){ + return override.r + } + return command.getName() +} + def Map getDashboardData() { boolean ok def value @@ -158,6 +170,14 @@ public String mem(showBytes = true) { return Math.round(100.00 * (bytes/ 100000.00)) + "%${showBytes ? " ($bytes bytes)" : ""}" } +/* Push command has multiple overloads in hubitat */ +public Map commandOverrides(){ + return (isHubitat() ? [ + push : [c: "push", s: null , r: "pushMomentary"], + flash : [c: "flash", s: null , r: "flashNative"],//s: command signature + ] : [:]) +} + /******************************************************************************/ /*** ***/ /*** SECURITY METHODS ***/ @@ -189,8 +209,12 @@ def String hashId(id) { return result } +private isHubitat(){ + return hubUID != null +} + /******************************************************************************/ /*** ***/ /*** END OF CODE ***/ /*** ***/ -/******************************************************************************/ +/******************************************************************************/ \ No newline at end of file diff --git a/smartapps/ady624/webcore.src/webcore.groovy b/smartapps/ady624/webcore.src/webcore.groovy index 1e870ec9..a7b6cf76 100644 --- a/smartapps/ady624/webcore.src/webcore.groovy +++ b/smartapps/ady624/webcore.src/webcore.groovy @@ -289,7 +289,7 @@ public static String version() { return "v0.3.108.20180906" } /******************************************************************************/ private static String handle() { return "webCoRE" } private static String domain() { return "webcore.co" } -include 'asynchttp_v1' +if(!isHubitat()) include 'asynchttp_v1' definition( name: "${handle()}", namespace: "ady624", @@ -312,6 +312,7 @@ preferences { page(name: "pageInitializeDashboard") page(name: "pageFinishInstall") page(name: "pageSelectDevices") + page(name: "pageFuelStreams") page(name: "pageSettings") page(name: "pageChangePassword") page(name: "pageSavePassword") @@ -389,6 +390,13 @@ def pageMain() { //trace "*** DO NOT SHARE THIS LINK WITH ANYONE *** Dashboard URL: ${getDashboardInitUrl()}" href "", title: "Dashboard", style: "external", url: getDashboardInitUrl(), description: "Tap to open", image: "https://cdn.rawgit.com/ady624/${handle()}/master/resources/icons/dashboard.png", required: false href "", title: "Register a browser", style: "embedded", url: getDashboardInitUrl(true), description: "Tap to open", image: "https://cdn.rawgit.com/ady624/${handle()}/master/resources/icons/browser-reg.png", required: false + input "customEndpoints", "bool", submitOnChange: true, title: "Use custom endpoints?", default: false, required: true + + if(customEndpoints){ + if(isHubitat()) input "customHubUrl", "string", title: "Custom hub url different from ${isHubitat() ? "https://cloud.hubitat.com" : "https://graph.smartthings.com"}", default: null, required: false + input "customWebcoreInstanceUrl", "string", title: "Custom webcore instance url different from dashboard.webcore.co", default: null, required: false + if(isHubitat()) paragraph "If you enter a custom url above you will have to use a different webcore instance from dashboard.webcore.co as the site is restricted to hubitat and smartthing's cloud" + } } } @@ -530,6 +538,7 @@ private pageSelectDevices() { section ('Select devices by type') { paragraph "Most devices should fall into one of these two categories" + if(isHubitat()) input "dev:all", "capability.*", multiple: true, title: "Which devices", required: false input "dev:actuator", "capability.actuator", multiple: true, title: "Which actuators", required: false input "dev:sensor", "capability.sensor", multiple: true, title: "Which sensors", required: false } @@ -571,13 +580,21 @@ def pageSettings() { def storageApp = getStorageApp() if (storageApp) { section("Available devices") { - app([title: 'Available devices', multiple: false, install: true, uninstall: false], 'storage', 'ady624', "${handle()} Storage") + app([title: isHubitat() ? 'Do not click' : 'Available Devices', multiple: false, install: true, uninstall: false], 'storage', 'ady624', "${handle()} Storage") } } else { section("Available devices") { href "pageSelectDevices", title: "Available devices", description: "Tap here to select which devices are available to pistons" } - } + } + + section("Fuel Streams"){ + input "localFuelStreams", "bool", title: "Use local fuel streams?", defaultValue: isHubitat() ? true : false, submitOnChange: true + if(settings.localFuelStreams){ + href "pageFuelStreams", title: "Fuel Streams", description: "Tap here to manage fuel streams" + } + } + /* section("Integrations") { href "pageIntegrations", title: "Integrations with other services", description: "Tap here to configure your integrations" }*/ @@ -599,6 +616,7 @@ def pageSettings() { input "redirectContactBook", "bool", title: "Redirect all Contact Book requests as PUSH notifications", description: "SmartThings has removed the Contact Book feature and as a result, all uses of Contact Book are by default ignored. By enabling this option, you will get all the existing Contact Book uses fall back onto the PUSH notification system, possibly allowing other people to receive these notifications.", defaultValue: false, required: true input "disabled", "bool", title: "Disable all pistons", description: "Disable all pistons belonging to this instance", defaultValue: false, required: false href "pageRebuildCache", title: "Clean up and rebuild data cache", description: "Tap here to change your clean up and rebuild your data cache" + input "logPistonExecutions", "bool", title: "Log piston executions?", description: "Tap here to change logging pistons in location events", defaultValue: isHubitat() ? false : true, required: false } section(title: "Recovery") { @@ -613,6 +631,14 @@ def pageSettings() { } } +private pageFuelStreams(){ + dynamicPage(name: "pageFuelStreams", title: "", uninstall: false, install: false){ + section(){ + app([title: isHubitat() ? 'Do not click' : 'Fuel Streams', multiple: true, install: true, uninstall: false], 'fuelStreams', 'ady624', "${handle()} Fuel Stream") + } + } +} + private pageChangePassword() { dynamicPage(name: "pageChangePassword", title: "", nextPage: "pageSavePassword") { section() { @@ -819,15 +845,27 @@ private initialize() { state.settings.remove('lifx_groups') state.settings.remove('lifx_locations') } + + if(state.accessToken){ + updateEndpoint(state.accessToken) + } } +private updateEndpoint(accessToken){ + if(isCustomEndpoint()){ + state.endpoint = customServerUrl("?access_token=${accessToken}") + } + else { + state.endpoint = isHubitat() ? apiServerUrl("$hubUID/apps/${app.id}/?access_token=${accessToken}") : apiServerUrl("/api/token/${accessToken}/smartapps/installations/${app.id}/") + } +} private initializeWebCoREEndpoint() { try { if (!state.endpoint) { try { def accessToken = createAccessToken() if (accessToken) { - state.endpoint = hubUID ? apiServerUrl("$hubUID/apps/${app.id}/?access_token=${state.accessToken}") : apiServerUrl("/api/token/${accessToken}/smartapps/installations/${app.id}/") + updateEndpoint(accessToken) } } catch(e) { state.endpoint = null @@ -851,6 +889,7 @@ private subscribeAll() { subscribe(location, "echoSistant", echoSistantHandler) subscribe(location, "HubUpdated", hubUpdatedHandler, [filterEvents: false]) subscribe(location, "summary", summaryHandler, [filterEvents: false]) + if(isHubitat()) subscribe(location, "hsmStatus", hsmHandler, [filterEvents: false]) setPowerSource(getHub()?.isBatteryInUse() ? 'battery' : 'mains') } @@ -886,6 +925,8 @@ mappings { path("/intf/dashboard/presence/create") {action: [GET: "api_intf_dashboard_presence_create"]} path("/intf/dashboard/variable/set") {action: [GET: "api_intf_variable_set"]} path("/intf/dashboard/settings/set") {action: [GET: "api_intf_settings_set"]} + path("/intf/fuelstreams/list") {action: [GET: "api_intf_fuelstreams_list"]} + path("/intf/fuelstreams/get") {action: [GET: "api_intf_fuelstreams_get"]} path("/intf/location/entered") {action: [GET: "api_intf_location_entered"]} path("/intf/location/exited") {action: [GET: "api_intf_location_exited"]} path("/intf/location/updated") {action: [GET: "api_intf_location_updated"]} @@ -904,19 +945,31 @@ private api_get_error_result(error) { ] } +private getHubitatVersion(){ + try{ + return location.getHubs().collectEntries {[it.id, it.getFirmwareVersionString()]} + } + catch(e){ + return location.getHubs().collectEntries {[it.id, "< 1.1.2.112"]} + } +} + private api_get_base_result(deviceVersion = 0, updateCache = false) { def tz = location.getTimeZone() def currentDeviceVersion = state.deviceVersion def Boolean sendDevices = (deviceVersion != currentDeviceVersion) def name = handle() + ' Piston' def incidentThreshold = now() - 604800000 + + def instanceId = hashId(app.id, updateCache) + return [ name: location.name + ' \\ ' + (app.label ?: app.name), instance: [ account: [id: hashId(hubUID ?: app.getAccountId(), updateCache)], pistons: getChildApps().findAll{ it.name == name }.sort{ it.label }.collect{ [ id: hashId(it.id, updateCache), 'name': it.label, 'meta': state[hashId(it.id, updateCache)] ] }, - id: hashId(app.id, updateCache), - locationId: hashId(location.id, updateCache), + id: instanceId, + locationId: hashId(location.id + (isHubitat() ? '-L' : ''), updateCache), name: app.label ?: app.name, uri: state.endpoint, deviceVersion: currentDeviceVersion, @@ -926,15 +979,16 @@ private api_get_base_result(deviceVersion = 0, updateCache = false) { lifx: state.lifx ?: [:], virtualDevices: virtualDevices(updateCache), globalVars: listAvailableVariables(), + fuelStreamUrls: getFuelStreamUrls(instanceId), ] + (sendDevices ? [contacts: [:], devices: listAvailableDevices(false, updateCache)] : [:]), location: [ contactBookEnabled: location.getContactBookEnabled(), - hubs: location.getHubs().collect{ [id: hashId(it.id, updateCache), name: it.name, firmware: hubUID ? 'unknown' : it.getFirmwareVersionString(), physical: it.getType().toString().contains('PHYSICAL'), powerSource: it.isBatteryInUse() ? 'battery' : 'mains' ]}, - incidents: hubUID ? [] : location.activeIncidents.collect{[date: it.date.time, title: it.getTitle(), message: it.getMessage(), args: it.getMessageArgs(), sourceType: it.getSourceType()]}.findAll{ it.date >= incidentThreshold }, - id: hashId(location.id, updateCache), + hubs: location.getHubs().collect{ [id: hashId(it.id, updateCache), name: it.name, firmware: isHubitat() ? getHubitatVersion()[it.id] : it.getFirmwareVersionString(), physical: it.getType().toString().contains('PHYSICAL'), powerSource: it.isBatteryInUse() ? 'battery' : 'mains' ]}, + incidents: isHubitat() ? [] : location.activeIncidents.collect{[date: it.date.time, title: it.getTitle(), message: it.getMessage(), args: it.getMessageArgs(), sourceType: it.getSourceType()]}.findAll{ it.date >= incidentThreshold }, + id: hashId(location.id + (isHubitat() ? '-L' : ''), updateCache), mode: hashId(location.getCurrentMode().id, updateCache), modes: location.getModes().collect{ [id: hashId(it.id, updateCache), name: it.name ]}, - shm: hubUID ? 'off' : location.currentState("alarmSystemStatus")?.value, + shm: isHubitat() ? transformHsmStatus(location.hsmStatus ?: state.hsmStatus) : location.currentState("alarmSystemStatus")?.value, name: location.name, temperatureScale: location.getTemperatureScale(), timeZone: tz ? [ @@ -948,11 +1002,62 @@ private api_get_base_result(deviceVersion = 0, updateCache = false) { ] } +private getFuelStreamUrls(iid){ + if(!useLocalFuelStreams()){ + def region = state.endpoint.contains('graph-eu') ? 'eu' : 'us' + def baseUrl = 'https://api-' + region + '-' + iid[32] + '.webcore.co:9287/fuelStreams' + def headers = [ 'Auth-Token' : iid ] + + return [ + list : [l: false, m: 'POST', h: headers, u: baseUrl + '/list', d: [i : iid]], + get : [l: false, m: 'POST', h: headers, u: baseUrl + '/get', d: [ i: iid ], p: 'f'] + ] + } + + def baseUrl = isCustomEndpoint() ? customServerUrl("/") : + isHubitat() ? apiServerUrl("$hubUID/apps/${app.id}/") + : apiServerUrl("/api/token/${state.accessToken}/smartapps/installations/${app.id}/") + + def params = baseUrl.contains(state.accessToken) ? "" : "access_token=${state.accessToken}" + return [ + list : [l: true, u: baseUrl + "intf/fuelstreams/list?${params}"], + get : [l: true, u: baseUrl + "intf/fuelstreams/get?id={fuelStreamId}${params ? "&" + params : ""}", p: 'fuelStreamId'] + ] +} + +private boolean useLocalFuelStreams(){ + return settings.localFuelStreams != null ? settings.localFuelStreams : (isHubitat() ? true : false) +} + +private String transformHsmStatus(status){ + switch(status){ + case "disarmed": + case "allDisarmed": + return "off" + break; + case "armedHome": + return "stay" + break; + case "armedAway": + return "away" + break; + default: + return "Unknown" + } +} + private api_intf_dashboard_load() { def result recoveryHandler() //install storage app def storageApp = getStorageApp(true) + if(storageApp && isHubitat() && storageApp.getStorageSettings() != null){ //migrate off of storage app + storageApp.getStorageSettings().findAll { it.key.startsWith('dev:') }.each { + app.updateSetting(it.key, [type: 'capability', value: it.value.collect { it.id }]) + } + state.migratedStorage = true + app.deleteChildApp(storageApp.id) + } //debug "Dashboard: Request received to initialize instance" if (verifySecurityToken(params.token)) { result = api_get_base_result(params.dev, true) @@ -1010,7 +1115,7 @@ private api_intf_dashboard_piston_create() { if (params.author || params.bin) { piston.config([bin: params.bin, author: params.author, initialVersion: version()]) } - if (hubUID) piston.installed() + if (isHubitat() && !piston.isInstalled()) piston.installed() result = [status: "ST_SUCCESS", id: hashId(piston.id)] } else { result = api_get_error_result("ERR_INVALID_TOKEN") @@ -1044,7 +1149,7 @@ private api_intf_dashboard_piston_get() { comparisons: comparisons(), functions: functions(), colors: [ - standard: colorUtil?.ALL + standard: colorUtil?.ALL ?: getColors() ], ] } @@ -1056,7 +1161,24 @@ private api_intf_dashboard_piston_get() { } //for accuracy, use the time as close as possible to the render result.now = now() - render contentType: "application/javascript;charset=utf-8", data: "${params.callback}(${groovy.json.JsonOutput.toJson(result)})" + def jsonData = groovy.json.JsonOutput.toJson(result) + + if(isHubitat() && (!isCustomEndpoint() || customHubUrl.contains(hubUID))){ + //data saver for hubitat ~100K limit + def responseLength = jsonData.getBytes("UTF-8").length + if(responseLength > 100 * 1024){ //these are loaded anyway right after loading the piston + log.warn "Trimming ${ (int)(responseLength/1024) }KB response to smaller size" + result.instance = null + result.data?.logs = [] + result.data?.stats?.timing = [] + //for accuracy, use the time as close as possible to the render + result.now = now() + jsonData = groovy.json.JsonOutput.toJson(result) + } + } + + //log.debug "Trimmed resonse length: ${jsonData.getBytes("UTF-8").length}" + render contentType: "application/javascript;charset=utf-8", data: "${params.callback}(${jsonData})" } @@ -1224,7 +1346,7 @@ private api_intf_dashboard_piston_pause() { if (verifySecurityToken(params.token)) { def piston = getChildApps().find{ hashId(it.id) == params.id }; if (piston) { - def rtData = piston.pause() + def rtData = piston.pausePiston() updateRunTimeData(rtData) //update the state because it will overwrite the atomicState //state[piston.id] = state[piston.id] @@ -1394,7 +1516,7 @@ private api_intf_dashboard_piston_delete() { if (verifySecurityToken(params.token)) { def piston = getChildApps().find{ hashId(it.id) == params.id }; if (piston) { - app.deleteChildApp(piston); + app.deleteChildApp(isHubitat() ? piston.id : piston) result = [status: "ST_SUCCESS"] state.remove(params.id) state.remove('sph${params.id}') @@ -1470,6 +1592,60 @@ private api_intf_variable_set() { render contentType: "application/javascript;charset=utf-8", data: "${params.callback}(${groovy.json.JsonOutput.toJson(result)})" } +public resetFuelStreamList(){ + state.fuelStreams = [] +} + +public writeToFuelStream(req){ + def name = "${handle()} Fuel Stream" + def streamName = "${(req.c ?: "")}||${req.n}" + + def result = getChildApps().find{ it.name == name && it.label.contains(streamName)} + def fuelStreams = isHubitat() ? [] : atomicState.fuelStreams ?: [] + + if(!result){ + if(fuelStreams.find{ it.contains(streamName) } ?: false){ //bug in smartthings doesn't remember state,childapps between multiple calls in the same piston + error "Found duplicate stream, not adding point" + return + } + def id = (getChildApps().findAll{ it.name == name }.collect{ it.label.split(' - ')[0].toInteger()}.max() ?: 0) + 1 + try { + result = addChildApp('ady624', name, "$id - $streamName") + if(!isHubitat()){ + fuelStreams = getChildApps().find{ it.name == name }.collect { it.label } + fuelStreams << result.label + atomicState.fuelStreams = fuelStreams + } + result.createStream([id: id, name: req.n, canister: req.c ?: ""]) + } + catch(e){ + error "Please install the webCoRE Fuel Streams app for local Fuel Streams" + return + } + } + + result.updateFuelStream(req) +} + +private api_intf_fuelstreams_list() { + def result = [] + def name = "${handle()} Fuel Stream" + result = getChildApps().findAll{ it.name == name }*.getFuelStream() + + render contentType: "application/javascript;charset=utf-8", data: "${params.callback}(${groovy.json.JsonOutput.toJson(["fuelStreams" : result])})" +} + +private api_intf_fuelstreams_get() { + def result = [] + def id = params.id + + def name = "${handle()} Fuel Stream" + def stream = getChildApps().find { it.name == name && it.label.startsWith("$id -")} + result = stream.listFuelStreamData() + + render contentType: "application/javascript;charset=utf-8", data: "${params.callback}(${groovy.json.JsonOutput.toJson(["points" : result])})" +} + private api_intf_settings_set() { def result debug "Dashboard: Request received to set settings" @@ -1520,7 +1696,7 @@ private api_intf_dashboard_piston_activity() { def api_ifttt() { def data = [:] - def remoteAddr = request.getHeader("X-FORWARDED-FOR") ?: request.getRemoteAddr() + def remoteAddr = isHubitat() ? "UNKNOWN" : request.getHeader("X-FORWARDED-FOR") ?: request.getRemoteAddr() if (params) { data.params = [:] for(param in params) { @@ -1533,7 +1709,7 @@ def api_ifttt() { data.remoteAddr = remoteAddr def eventName = params?.eventName if (eventName) { - if (!hubUID) sendLocationEvent([name: "ifttt", value: eventName, isStateChange: true, linkText: "IFTTT event", descriptionText: "${handle()} has received an IFTTT event: $eventName", data: data]) + sendLocationEvent([name: "ifttt", value: eventName, isStateChange: true, linkText: "IFTTT event", descriptionText: "${handle()} has received an IFTTT event: $eventName", data: data]) } render contentType: "text/html", data: "Received event $eventName." } @@ -1544,7 +1720,7 @@ def api_email() { def from = data.from ?: '' def pistonId = params?.pistonId if (pistonId) { - if (!hubUID) sendLocationEvent([name: "email", value: pistonId, isStateChange: true, linkText: "Email event", descriptionText: "${handle()} has received an email from $from", data: data]) + sendLocationEvent([name: "email", value: pistonId, isStateChange: true, linkText: "Email event", descriptionText: "${handle()} has received an email from $from", data: data]) } render contentType: "text/plain", data: "OK" } @@ -1552,7 +1728,7 @@ def api_email() { private api_execute() { def result = [:] def data = [:] - def remoteAddr = request.getHeader("X-FORWARDED-FOR") ?: request.getRemoteAddr() + def remoteAddr = isHubitat() ? "UNKNOWN" : request.getHeader("X-FORWARDED-FOR") ?: request.getRemoteAddr() debug "Dashboard: Request received to execute a piston from IP $remoteAddr" if (params) { data = [:] @@ -1567,7 +1743,7 @@ private api_execute() { def pistonIdOrName = params?.pistonIdOrName def piston = getChildApps().find{ (it.label == pistonIdOrName) || (hashId(it.id) == pistonIdOrName) }; if (piston) { - if (!hubUID) sendLocationEvent(name: hashId(piston.id), value: remoteAddr, isStateChange: true, displayed: false, linkText: "Execute event", descriptionText: "External piston execute request from IP $remoteAddr", data: data) + sendLocationEvent(name: hashId(piston.id), value: remoteAddr, isStateChange: true, displayed: false, linkText: "Execute event", descriptionText: "External piston execute request from IP $remoteAddr", data: data) result.result = 'OK' } else { result.result = 'ERROR' @@ -1592,7 +1768,7 @@ def recoveryHandler() { if (failedPistons.size()) { for (piston in failedPistons) { warn "Piston $piston.name was sent a recovery signal because it was ${now() - piston.meta.n}ms late" - if (!hubUID) sendLocationEvent(name: piston.id, value: 'recovery', isStateChange: true, displayed: false, linkText: "Recovery event", descriptionText: "Recovery event for piston $piston.name") + sendLocationEvent(name: piston.id, value: 'recovery', isStateChange: true, displayed: false, linkText: "Recovery event", descriptionText: "Recovery event for piston $piston.name") } } if (state.version != version()) { @@ -1635,17 +1811,23 @@ private cleanUp() { } private getStorageApp(install = false) { + if(isHubitat() && state.migratedStorage) return null def name = handle() + ' Storage' def storageApp = getChildApps().find{ it.name == name } + def label = "${app.label} Devices" if (storageApp) { - if (app.label != storageApp.label) { - storageApp.updateLabel(app.label) + if (label != storageApp.label) { + storageApp.updateLabel(label) } return storageApp } if (!install) return null + if(isHubitat()){ + state.migratedStorage = true + return null + } try { - storageApp = addChildApp("ady624", name, app.label) + storageApp = addChildApp("ady624", name, label) } catch (all) { error "Please install the webCoRE Storage SmartApp for better performance" return null @@ -1664,6 +1846,7 @@ private getStorageApp(install = false) { } private getDashboardApp(install = false) { + if(isHubitat()) return null def name = handle() + ' Dashboard' def label = app.label + ' (dashboard)' def dashboardApp = getChildApps().find{ it.name == name } @@ -1681,10 +1864,32 @@ private getDashboardApp(install = false) { return dashboardApp } +def customServerUrl(path){ + path ?: "" + if(!path.startsWith("/")){ + path = "/" + path + } + + if(customHubUrl.contains(hubUID)){ + return customHubUrl + "/" + app.id + path + } + return customHubUrl + "/apps/api/" + app.id + path +} + + private String getDashboardInitUrl(register = false) { def url = register ? getDashboardRegistrationUrl() : getDashboardUrl() if (!url) return null - return url + (register ? "register/" : "init/") + (apiServerUrl("").replace("https://", '').replace(".api.smartthings.com", "").replace(":443", "").replace("/", "") + ((hubUID ?: state.accessToken) + app.id).replace("-", "") + (hubUID ? '/?access_token=' + state.accessToken : '')).bytes.encodeBase64() + if(isCustomEndpoint()){ + return url + (register ? "register/" : "init/") + ( + customServerUrl('/?access_token=' + state.accessToken) + ).bytes.encodeBase64() + } + else { + return url + (register ? "register/" : "init/") + + (apiServerUrl("").replace("https://", '').replace(".api.smartthings.com", "").replace(":443", "").replace("/", "") + + ((hubUID ?: state.accessToken) + app.id).replace("-", "") + (isHubitat() ? '/?access_token=' + state.accessToken : '')).bytes.encodeBase64() + } } private String getDashboardRegistrationUrl() { @@ -1698,10 +1903,11 @@ public Map listAvailableDevices(raw = false, updateCache = false) { if (storageApp) { result = storageApp.listAvailableDevices(raw) } else { + def overrides = commandOverrides() if (raw) { result = settings.findAll{ it.key.startsWith("dev:") }.collect{ it.value }.flatten().collectEntries{ dev -> [(hashId(dev.id, updateCache)): dev]} } else { - result = settings.findAll{ it.key.startsWith("dev:") }.collect{ it.value }.flatten().collectEntries{ dev -> [(hashId(dev.id, updateCache)): dev]}.collectEntries{ id, dev -> [ (id): [ n: dev.getDisplayName(), cn: dev.getCapabilities()*.name, a: dev.getSupportedAttributes().unique{ it.name }.collect{def x = [n: it.name, t: it.getDataType(), o: it.getValues()]; try {x.v = dev.currentValue(x.n);} catch(all) {}; x}, c: dev.getSupportedCommands().unique{ it.getName() }.collect{[n: it.getName(), p: it.getArguments()]} ]]} + result = settings.findAll{ it.key.startsWith("dev:") }.collect{ it.value }.flatten().collectEntries{ dev -> [(hashId(dev.id, updateCache)): dev]}.collectEntries{ id, dev -> [ (id): [ n: dev.getDisplayName(), cn: dev.getCapabilities()*.name, a: dev.getSupportedAttributes().unique{ it.name }.collect{def x = [n: it.name, t: it.getDataType(), o: it.getValues()]; try {x.v = dev.currentValue(x.n);} catch(all) {}; x}, c: dev.getSupportedCommands().unique{ transformCommand(it, overrides) }.collect{[n: transformCommand(it, overrides), p: it.getArguments()]} ]]} } } List presenceDevices = getChildDevices() @@ -1715,6 +1921,15 @@ public Map listAvailableDevices(raw = false, updateCache = false) { return result } +private def transformCommand(command, overrides){ + def override = overrides[command.getName()] + if(override && override.s == command.getArguments()?.toString()){ + return override.r + } + return command.getName() +} + + private setPowerSource(powerSource, atomic = true) { if (state.powerSource == powerSource) return if (atomic) { @@ -1722,7 +1937,7 @@ private setPowerSource(powerSource, atomic = true) { } else { state.powerSource = powerSource } - if (!hubUID) sendLocationEvent([name: 'powerSource', value: powerSource, isStateChange: true, linkText: "webCoRE power source event", descriptionText: "${handle()} has detected a new power source: $powerSource"]) + sendLocationEvent([name: 'powerSource', value: powerSource, isStateChange: true, linkText: "webCoRE power source event", descriptionText: "${handle()} has detected a new power source: $powerSource"]) } private Map listAvailableVariables() { @@ -1796,7 +2011,7 @@ private String generatePistonName() { } private ping() { - if (!hubUID) sendLocationEvent( [name: handle(), value: 'ping', isStateChange: true, displayed: false, linkText: "${handle()} ping reply", descriptionText: "${handle()} has received a ping reply and is replying with a pong", data: [id: hashId(app.id), name: app.label]] ) + sendLocationEvent( [name: handle(), value: 'ping', isStateChange: true, displayed: false, linkText: "${handle()} ping reply", descriptionText: "${handle()} has received a ping reply and is replying with a pong", data: [id: hashId(app.id), name: app.label]] ) } private getLogging() { @@ -1867,7 +2082,7 @@ private testLifx() { private registerInstance() { def accountId = hashId(hubUID ?: app.getAccountId()) - def locationId = hashId(location.id) + def locationId = hashId(location.id + (isHubitat() ? '-L' : '')) def instanceId = hashId(app.id) def endpoint = state.endpoint def region = endpoint.contains('graph-eu') ? 'eu' : 'us'; @@ -1877,7 +2092,8 @@ private registerInstance() { def pa = lpa.size() List lpd = pistons.findAll{ !it.a }.collect{ it.id } def pd = pistons.size() - pa - if (asynchttp_v1) asynchttp_v1.put(instanceRegistrationHandler, [ + + def params = [ uri: "https://api-${region}-${instanceId[32]}.webcore.co:9247", path: '/instance/register', headers: ['ST' : instanceId], @@ -1893,7 +2109,18 @@ private registerInstance() { pd: pd, lpd: lpd.join(',') ] - ]) + ] + if (asynchttp_v1) + { + asynchttp_v1.put(instanceRegistrationHandler, params) + } + else { + params << [contentType: 'application/json', requestContentType: 'application/json'] + try{ + httpPut(params) { res -> } + } + catch(e) {} + } } private initSunriseAndSunset() { @@ -1934,7 +2161,13 @@ public Boolean isInstalled() { public String getDashboardUrl() { if (!state.endpoint) return null - return "https://dashboard.${domain()}/" + + if(customEndpoints && (customWebcoreInstanceUrl ?: "") != ""){ + return customWebcoreInstanceUrl + "/" + } + else { + return "https://dashboard.${domain()}/" + } } public refreshDevices() { @@ -1955,8 +2188,9 @@ public Map getRunTimeData(semaphore = null, fetchWrappers = false) { semaphore = semaphore ?: 0 def semaphoreDelay = 0 def semaphoreName = semaphore ? "sph$semaphore" : '' - if (semaphore) { - def waited = false + + def waited = false + if (semaphore) { //if we need to wait for a semaphore, we do it here def lastSemaphore while (semaphore) { @@ -1980,7 +2214,8 @@ public Map getRunTimeData(semaphore = null, fetchWrappers = false) { semaphoreDelay: semaphoreDelay, commands: [ physical: commands(), - virtual: virtualCommands() + virtual: virtualCommands(), + overrides: commandOverrides() ], comparisons: comparisons(), coreVersion: version(), @@ -1998,8 +2233,14 @@ public Map getRunTimeData(semaphore = null, fetchWrappers = false) { started: startTime, ended: now(), generatedIn: now() - startTime, - redirectContactBook: settings.redirectContactBook - ] + redirectContactBook: settings.redirectContactBook, + logPistonExecutions: settings.logPistonExecutions, + useLocalFuelStreams : useLocalFuelStreams(), + waitedAtSemaphore : waited + ] + (isHubitat() ? [ + hsmStatus: state.hsmStatus ?: location.hsmStatus, + colors: getColors() + ] : [:]) } public void updateRunTimeData(data) { @@ -2068,7 +2309,7 @@ public void updateRunTimeData(data) { public pausePiston(pistonId) { def piston = getChildApps().find{ hashId(it.id) == pistonId }; if (piston) { - def rtData = piston.pause() + def rtData = piston.pausePiston() updateRunTimeData(rtData) } } @@ -2091,11 +2332,11 @@ public executePiston(pistonId, data, source) { } private sendVariableEvent(variable) { - if (!hubUID) sendLocationEvent([name: variable.name.startsWith('@@') ? '@@' + handle() : hashId(app.id), value: variable.name, isStateChange: true, displayed: false, linkText: "${handle()} global variable ${variable.name} changed", descriptionText: "${handle()} global variable ${variable.name} changed", data: [id: hashId(app.id), name: app.label, event: 'variable', variable: variable]]) + sendLocationEvent([name: variable.name.startsWith('@@') ? '@@' + handle() : hashId(app.id), value: variable.name, isStateChange: true, displayed: false, linkText: "${handle()} global variable ${variable.name} changed", descriptionText: "${handle()} global variable ${variable.name} changed", data: [id: hashId(app.id), name: app.label, event: 'variable', variable: variable]]) } private broadcastPistonList() { - if (!hubUID) sendLocationEvent([name: handle(), value: 'pistonList', isStateChange: true, displayed: false, data: [id: hashId(app.id), name: app.label, pistons: getChildApps().findAll{ it.name == "${handle()} Piston" }.collect{[id: hashId(it.id), name: it.label]}]]) + sendLocationEvent([name: handle(), value: 'pistonList', isStateChange: true, displayed: false, data: [id: hashId(app.id), name: app.label, pistons: getChildApps().findAll{ it.name == "${handle()} Piston" }.collect{[id: hashId(it.id), name: it.label]}]]) } def webCoREHandler(event) { @@ -2185,7 +2426,9 @@ def NewIncidentHandler(evt) { //log.error "$evt.name >>> ${evt.jsonData}" } - +def hsmHandler(evt){ + state.hsmStatus = evt.value +} def lifxHandler(response, cbkData) { if ((response.status == 200)) { @@ -2309,7 +2552,7 @@ private debug(message, shift = null, err = null, cmd = null) { } else if (cmd == "warn") { log.warn "$prefix$message", err } else if (cmd == "error") { - if (hubUID) { log.error "$prefix$message" } else { log.error "$prefix$message", err } + if (isHubitat()) { log.error "$prefix$message $err" } else { log.error "$prefix$message", err } } else { log.debug "$prefix$message", err } @@ -2320,19 +2563,15 @@ private warn(message, shift = null, err = null) { debug message, shift, err, 'wa private error(message, shift = null, err = null) { debug message, shift, err, 'error' } private timer(message, shift = null, err = null) { debug message, shift, err, 'timer' } - - - - - - - +private isCustomEndpoint(){ + customEndpoints && (customHubUrl ?: "") != "" +} /******************************************************************************/ /*** DATABASE ***/ /******************************************************************************/ -private static Map capabilities() { +private Map capabilities() { //n = name //d = friendly devices name //a = default attribute @@ -2340,7 +2579,7 @@ private static Map capabilities() { //m = momentary //s = number of subdevices //i = subdevice index in event data - return [ + def capabilities = [ accelerationSensor : [ n: "Acceleration Sensor", d: "acceleration sensors", a: "acceleration", ], actuator : [ n: "Actuator", d: "actuators", ], alarm : [ n: "Alarm", d: "alarms and sirens", a: "alarm", c: ["off", "strobe", "siren", "both"], ], @@ -2411,12 +2650,24 @@ private static Map capabilities() { valve : [ n: "Valve", d: "valves", a: "valve", c: ["close", "open"], ], voltageMeasurement : [ n: "Voltage Measurement", d: "voltmeters", a: "voltage", ], waterSensor : [ n: "Water Sensor", d: "water and leak sensors", a: "water", ], - windowShade : [ n: "Window Shade", d: "automatic window shades", a: "windowShade", c: ["close", "open", "presetPosition"], ], - ] -} - -private static Map attributes() { - return [ + windowShade : [ n: "Window Shade", d: "automatic window shades", a: "windowShade", c: ["close", "open", "presetPosition"], ] + ] + (isHubitat() ? [ + doubleTapableButton : [ n: "Double Tapable Button", d: "double tapable buttons", a: "doubleTapped", c: ["doubleTap"], ], + holdableButton : [ n: "Holdable Button", d: "holdable buttons", a: "held", c: ["hold"] ], + momentary : [ n: "Momentary", d: "momentary switches", c: ["pushMomentary"], ], + pushableButton : [ n: "Pushable Button", d: "pushable buttons", a: "pushed", c: ["push"], ] + + ] : [:]) + + if(isHubitat()){ + capabilities.remove('button') + } + + return capabilities +} + +private Map attributes() { + def attrs = [ acceleration : [ n: "acceleration", t: "enum", o: ["active", "inactive"], ], activities : [ n: "activities", t: "object", ], alarm : [ n: "alarm", t: "enum", o: ["both", "off", "siren", "strobe"], ], @@ -2511,10 +2762,29 @@ private static Map attributes() { speed : [ n: "speed", t: "decimal", r: [null, null], u: "ft/s", ], speedMetric : [ n: "speed (metric)", t: "decimal", r: [null, null], u: "m/s", ], bearing : [ n: "bearing", t: "decimal", r: [0, 360], u: "°", ], - ] + ] + (isHubitat() ? [ + doubleTapped : [ n: "double tapped button", t: "integer", c: "doubleTapableButton" ], + held : [ n: "held button", t: "integer", c: "holdableButton" ], + pushed : [ n: "pushed button", t: "integer", c: "pushableButton" ] + ] : [:]) + + if(isHubitat()){ + attrs.remove('button') + attrs.remove('holdableButton') + } + + return attrs } -private static Map commands() { +/* Push command has multiple overloads in hubitat */ +public Map commandOverrides(){ + return (isHubitat() ? [ //s: command signature + push : [c: "push", s: null , r: "pushMomentary"], + flash : [c: "flash", s: null , r: "flashNative"] //flash native command conflicts with flash emulated command. Also needs "o" option on command described later + ] : [:]) +} + +private Map commands() { return [ auto : [ n: "Set to Auto", a: "thermostatMode", v: "auto", ], beep : [ n: "Beep", ], @@ -2632,16 +2902,22 @@ private static Map commands() { low : [ n: "Set to Low", ], med : [ n: "Set to Medium", ], high : [ n: "Set to High", ], - ] + ] + (isHubitat() ? [ + doubleTap : [ n: "Double Tap", d: "Double tap button {0}", a: "doubleTapped", p:[[n: "Button #", t: "integer"]] ], + flashNative : [ n: "Flash", ], + hold : [ n: "Hold", d: "Hold Button {0}", a: "held", p: [[n:"Button #", t: "integer"]] ], + push : [ n: "Push", d: "Push button {0}", a: "pushed", p:[[n: "Button #", t: "integer"]] ], + pushMomentary : [ n: "Push" ] + ] : [:]) } -private static Map virtualCommands() { +private Map virtualCommands() { //a = aggregate //d = display //n = name //t = type List tileIndexes = ['1','2','3','4','5','6','7','8','9','10','11','12','13','14','15','16'] - return [ + def commands = [ noop : [ n: "No operation", a: true, i: "circle", d: "No operation", ], wait : [ n: "Wait...", a: true, i: "clock", is: "r", d: "Wait {0}", p: [[n:"Duration", t:"duration"]], ], waitRandom : [ n: "Wait randomly...", a: true, i: "clock", is: "r", d: "Wait randomly between {0} and {1}", p: [[n:"At least", t:"duration"],[n:"At most", t:"duration"]], ], @@ -2723,7 +2999,19 @@ private static Map virtualCommands() { ] : [:]) + (getLifxToken() ? [ lifxScene: [n: "Activate LIFX scene", p: ["Scene:lifxScenes"], l: true, dd: "Activate LIFX Scene '{0}'", aggregated: true], - ] : [:])*/ + ] : [:])*/ + + if(isHubitat()){ + commands += [ + setAlarmSystemStatus : [ n: "Set Hubitat Safety Monitor status...", a: true, i: "", d: "Set Hubitat Safety Monitor status to {0}", p: [[n:"Status", t:"enum", o: getAlarmSystemStatusActions().collect {[n: it.value, v: it.key]}]], ], + //keep emulated flash to not break old pistons + emulatedFlash : [ n: "(Old do not use) Emulated Flash", r: ["on", "off"], i: "toggle-on", d: "(Old do not use)Flash on {0} / off {1} for {2} times{3}", p: [[n:"On duration",t:"duration"],[n:"Off duration",t:"duration"],[n:"Number of flashes",t:"integer"], [n:"Only if switch is...", t:"enum",o:["on","off"], d:" if already {v}"]], ], + //add back emulated flash with "o" option so that it overrides the native flash command + flash : [ n: "Flash...", r: ["on", "off"], i: "toggle-on", d: "Flash on {0} / off {1} for {2} times{3}", p: [[n:"On duration",t:"duration"],[n:"Off duration",t:"duration"],[n:"Number of flashes",t:"integer"], [n:"Only if switch is...", t:"enum",o:["on","off"], d:" if already {v}"]], o: true /*override physical command*/ ] + ] + } + + return commands } @@ -2931,14 +3219,54 @@ private Map getLocationModeOptions(updateCache = false) { } return result } +private static Map getAlarmSystemStatusActions() { + return [ + armAll: "Arm All", + armRules: "Arm Monitor Rules", + armHome: "Arm Home", + armAway: "Arm Away", + disarmAll: "Disarm All", + disarmRules: "Disarm Monitor Rules", + disarm: "Disarm", + cancelAlerts: "Cancel Alerts" + ] +} + private static Map getAlarmSystemStatusOptions() { - return [ - off: "Disarmed", + return [ + off: "Disarmed", stay: "Armed/Stay", away: "Armed/Away" ] } +private static Map getHubitatAlarmSystemStatusOptions() { + return [ + armedAway: "Armed Away", + armedHome: "Armed Home", + disarmed: "Disarmed", + allDisarmed: "All Disarmed" + ] +} + +private static Map getAlarmSystemAlertOptions() { + return [ + intrusion: "Intrusion Away", + "intrusion-home": "Intrusion Home", + smoke: "Smoke", + water: "Water", + rule: "Rule" + ] +} + +private static Map getAlarmSystemRuleOptions() { + return [ + armedRule: "Armed Rule", + disarmedRule: "Disarmed Rule" + ] +} + + private Map getRoutineOptions(updateCache = false) { def routines = location.helloHome?.getPhrases() def result = [:] @@ -2973,6 +3301,162 @@ private Map virtualDevices(updateCache = false) { mode: [ n: 'Location mode', t: 'enum', o: getLocationModeOptions(updateCache), x: true], tile: [ n: 'Piston tile', t: 'enum', o: ['1':'1','2':'2','3':'3','4':'4','5':'5','6':'6','7':'7','8':'8','9':'9','10':'10','11':'11','12':'12','13':'13','14':'14','15':'15','16':'16'], m: true ], routine: [ n: 'Routine', t: 'enum', o: getRoutineOptions(updateCache), m: true], - alarmSystemStatus: [ n: 'Smart Home Monitor status', t: 'enum', o: getAlarmSystemStatusOptions(), x: true], + alarmSystemStatus: [ n: 'Smart Home Monitor status', t: 'enum', o: getAlarmSystemStatusOptions(), x: true] + ] + (isHubitat() ? [ + alarmSystemStatus: [ n: 'Hubitat Safety Monitor status',t: 'enum', o: getHubitatAlarmSystemStatusOptions(), ac: getAlarmSystemStatusActions(), x: true], //ac - actions. hubitat doesn't reuse the status for actions + alarmSystemEvent: [ n: 'Hubitat Safety Monitor event',t: 'enum', o: getAlarmSystemStatusActions(), m: true], + alarmSystemAlert: [ n: 'Hubitat Safety Monitor alert',t: 'enum', o: getAlarmSystemAlertOptions(), m: true], + alarmSystemRule: [ n: 'Hubitat Safety Monitor rule',t: 'enum', o: getAlarmSystemRuleOptions(), m: true] + ] : [:]) +} + +public List getColors(){ + return [ + [name:"Alice Blue", rgb:"#F0F8FF", h:208, s:100, l:97], + [name:"Antique White", rgb:"#FAEBD7", h:34, s:78, l:91], + [name:"Aqua", rgb:"#00FFFF", h:180, s:100, l:50], + [name:"Aquamarine", rgb:"#7FFFD4", h:160, s:100, l:75], + [name:"Azure", rgb:"#F0FFFF", h:180, s:100, l:97], + [name:"Beige", rgb:"#F5F5DC", h:60, s:56, l:91], + [name:"Bisque", rgb:"#FFE4C4", h:33, s:100, l:88], + [name:"Blanched Almond", rgb:"#FFEBCD", h:36, s:100, l:90], + [name:"Blue", rgb:"#0000FF", h:240, s:100, l:50], + [name:"Blue Violet", rgb:"#8A2BE2", h:271, s:76, l:53], + [name:"Brown", rgb:"#A52A2A", h:0, s:59, l:41], + [name:"Burly Wood", rgb:"#DEB887", h:34, s:57, l:70], + [name:"Cadet Blue", rgb:"#5F9EA0", h:182, s:25, l:50], + [name:"Chartreuse", rgb:"#7FFF00", h:90, s:100, l:50], + [name:"Chocolate", rgb:"#D2691E", h:25, s:75, l:47], + [name:"Cool White", rgb:"#F3F6F7", h:187, s:19, l:96], + [name:"Coral", rgb:"#FF7F50", h:16, s:100, l:66], + [name:"Corn Flower Blue", rgb:"#6495ED", h:219, s:79, l:66], + [name:"Corn Silk", rgb:"#FFF8DC", h:48, s:100, l:93], + [name:"Crimson", rgb:"#DC143C", h:348, s:83, l:58], + [name:"Cyan", rgb:"#00FFFF", h:180, s:100, l:50], + [name:"Dark Blue", rgb:"#00008B", h:240, s:100, l:27], + [name:"Dark Cyan", rgb:"#008B8B", h:180, s:100, l:27], + [name:"Dark Golden Rod", rgb:"#B8860B", h:43, s:89, l:38], + [name:"Dark Gray", rgb:"#A9A9A9", h:0, s:0, l:66], + [name:"Dark Green", rgb:"#006400", h:120, s:100, l:20], + [name:"Dark Khaki", rgb:"#BDB76B", h:56, s:38, l:58], + [name:"Dark Magenta", rgb:"#8B008B", h:300, s:100, l:27], + [name:"Dark Olive Green", rgb:"#556B2F", h:82, s:39, l:30], + [name:"Dark Orange", rgb:"#FF8C00", h:33, s:100, l:50], + [name:"Dark Orchid", rgb:"#9932CC", h:280, s:61, l:50], + [name:"Dark Red", rgb:"#8B0000", h:0, s:100, l:27], + [name:"Dark Salmon", rgb:"#E9967A", h:15, s:72, l:70], + [name:"Dark Sea Green", rgb:"#8FBC8F", h:120, s:25, l:65], + [name:"Dark Slate Blue", rgb:"#483D8B", h:248, s:39, l:39], + [name:"Dark Slate Gray", rgb:"#2F4F4F", h:180, s:25, l:25], + [name:"Dark Turquoise", rgb:"#00CED1", h:181, s:100, l:41], + [name:"Dark Violet", rgb:"#9400D3", h:282, s:100, l:41], + [name:"Daylight White", rgb:"#CEF4FD", h:191, s:9, l:90], + [name:"Deep Pink", rgb:"#FF1493", h:328, s:100, l:54], + [name:"Deep Sky Blue", rgb:"#00BFFF", h:195, s:100, l:50], + [name:"Dim Gray", rgb:"#696969", h:0, s:0, l:41], + [name:"Dodger Blue", rgb:"#1E90FF", h:210, s:100, l:56], + [name:"Fire Brick", rgb:"#B22222", h:0, s:68, l:42], + [name:"Floral White", rgb:"#FFFAF0", h:40, s:100, l:97], + [name:"Forest Green", rgb:"#228B22", h:120, s:61, l:34], + [name:"Fuchsia", rgb:"#FF00FF", h:300, s:100, l:50], + [name:"Gainsboro", rgb:"#DCDCDC", h:0, s:0, l:86], + [name:"Ghost White", rgb:"#F8F8FF", h:240, s:100, l:99], + [name:"Gold", rgb:"#FFD700", h:51, s:100, l:50], + [name:"Golden Rod", rgb:"#DAA520", h:43, s:74, l:49], + [name:"Gray", rgb:"#808080", h:0, s:0, l:50], + [name:"Green", rgb:"#008000", h:120, s:100, l:25], + [name:"Green Yellow", rgb:"#ADFF2F", h:84, s:100, l:59], + [name:"Honeydew", rgb:"#F0FFF0", h:120, s:100, l:97], + [name:"Hot Pink", rgb:"#FF69B4", h:330, s:100, l:71], + [name:"Indian Red", rgb:"#CD5C5C", h:0, s:53, l:58], + [name:"Indigo", rgb:"#4B0082", h:275, s:100, l:25], + [name:"Ivory", rgb:"#FFFFF0", h:60, s:100, l:97], + [name:"Khaki", rgb:"#F0E68C", h:54, s:77, l:75], + [name:"Lavender", rgb:"#E6E6FA", h:240, s:67, l:94], + [name:"Lavender Blush", rgb:"#FFF0F5", h:340, s:100, l:97], + [name:"Lawn Green", rgb:"#7CFC00", h:90, s:100, l:49], + [name:"Lemon Chiffon", rgb:"#FFFACD", h:54, s:100, l:90], + [name:"Light Blue", rgb:"#ADD8E6", h:195, s:53, l:79], + [name:"Light Coral", rgb:"#F08080", h:0, s:79, l:72], + [name:"Light Cyan", rgb:"#E0FFFF", h:180, s:100, l:94], + [name:"Light Golden Rod Yellow", rgb:"#FAFAD2", h:60, s:80, l:90], + [name:"Light Gray", rgb:"#D3D3D3", h:0, s:0, l:83], + [name:"Light Green", rgb:"#90EE90", h:120, s:73, l:75], + [name:"Light Pink", rgb:"#FFB6C1", h:351, s:100, l:86], + [name:"Light Salmon", rgb:"#FFA07A", h:17, s:100, l:74], + [name:"Light Sea Green", rgb:"#20B2AA", h:177, s:70, l:41], + [name:"Light Sky Blue", rgb:"#87CEFA", h:203, s:92, l:75], + [name:"Light Slate Gray", rgb:"#778899", h:210, s:14, l:53], + [name:"Light Steel Blue", rgb:"#B0C4DE", h:214, s:41, l:78], + [name:"Light Yellow", rgb:"#FFFFE0", h:60, s:100, l:94], + [name:"Lime", rgb:"#00FF00", h:120, s:100, l:50], + [name:"Lime Green", rgb:"#32CD32", h:120, s:61, l:50], + [name:"Linen", rgb:"#FAF0E6", h:30, s:67, l:94], + [name:"Maroon", rgb:"#800000", h:0, s:100, l:25], + [name:"Medium Aquamarine", rgb:"#66CDAA", h:160, s:51, l:60], + [name:"Medium Blue", rgb:"#0000CD", h:240, s:100, l:40], + [name:"Medium Orchid", rgb:"#BA55D3", h:288, s:59, l:58], + [name:"Medium Purple", rgb:"#9370DB", h:260, s:60, l:65], + [name:"Medium Sea Green", rgb:"#3CB371", h:147, s:50, l:47], + [name:"Medium Slate Blue", rgb:"#7B68EE", h:249, s:80, l:67], + [name:"Medium Spring Green", rgb:"#00FA9A", h:157, s:100, l:49], + [name:"Medium Turquoise", rgb:"#48D1CC", h:178, s:60, l:55], + [name:"Medium Violet Red", rgb:"#C71585", h:322, s:81, l:43], + [name:"Midnight Blue", rgb:"#191970", h:240, s:64, l:27], + [name:"Mint Cream", rgb:"#F5FFFA", h:150, s:100, l:98], + [name:"Misty Rose", rgb:"#FFE4E1", h:6, s:100, l:94], + [name:"Moccasin", rgb:"#FFE4B5", h:38, s:100, l:85], + [name:"Navajo White", rgb:"#FFDEAD", h:36, s:100, l:84], + [name:"Navy", rgb:"#000080", h:240, s:100, l:25], + [name:"Old Lace", rgb:"#FDF5E6", h:39, s:85, l:95], + [name:"Olive", rgb:"#808000", h:60, s:100, l:25], + [name:"Olive Drab", rgb:"#6B8E23", h:80, s:60, l:35], + [name:"Orange", rgb:"#FFA500", h:39, s:100, l:50], + [name:"Orange Red", rgb:"#FF4500", h:16, s:100, l:50], + [name:"Orchid", rgb:"#DA70D6", h:302, s:59, l:65], + [name:"Pale Golden Rod", rgb:"#EEE8AA", h:55, s:67, l:80], + [name:"Pale Green", rgb:"#98FB98", h:120, s:93, l:79], + [name:"Pale Turquoise", rgb:"#AFEEEE", h:180, s:65, l:81], + [name:"Pale Violet Red", rgb:"#DB7093", h:340, s:60, l:65], + [name:"Papaya Whip", rgb:"#FFEFD5", h:37, s:100, l:92], + [name:"Peach Puff", rgb:"#FFDAB9", h:28, s:100, l:86], + [name:"Peru", rgb:"#CD853F", h:30, s:59, l:53], + [name:"Pink", rgb:"#FFC0CB", h:350, s:100, l:88], + [name:"Plum", rgb:"#DDA0DD", h:300, s:47, l:75], + [name:"Powder Blue", rgb:"#B0E0E6", h:187, s:52, l:80], + [name:"Purple", rgb:"#800080", h:300, s:100, l:25], + [name:"Red", rgb:"#FF0000", h:0, s:100, l:50], + [name:"Rosy Brown", rgb:"#BC8F8F", h:0, s:25, l:65], + [name:"Royal Blue", rgb:"#4169E1", h:225, s:73, l:57], + [name:"Saddle Brown", rgb:"#8B4513", h:25, s:76, l:31], + [name:"Salmon", rgb:"#FA8072", h:6, s:93, l:71], + [name:"Sandy Brown", rgb:"#F4A460", h:28, s:87, l:67], + [name:"Sea Green", rgb:"#2E8B57", h:146, s:50, l:36], + [name:"Sea Shell", rgb:"#FFF5EE", h:25, s:100, l:97], + [name:"Sienna", rgb:"#A0522D", h:19, s:56, l:40], + [name:"Silver", rgb:"#C0C0C0", h:0, s:0, l:75], + [name:"Sky Blue", rgb:"#87CEEB", h:197, s:71, l:73], + [name:"Slate Blue", rgb:"#6A5ACD", h:248, s:53, l:58], + [name:"Slate Gray", rgb:"#708090", h:210, s:13, l:50], + [name:"Snow", rgb:"#FFFAFA", h:0, s:100, l:99], + [name:"Soft White", rgb:"#B6DA7C", h:83, s:44, l:67], + [name:"Spring Green", rgb:"#00FF7F", h:150, s:100, l:50], + [name:"Steel Blue", rgb:"#4682B4", h:207, s:44, l:49], + [name:"Tan", rgb:"#D2B48C", h:34, s:44, l:69], + [name:"Teal", rgb:"#008080", h:180, s:100, l:25], + [name:"Thistle", rgb:"#D8BFD8", h:300, s:24, l:80], + [name:"Tomato", rgb:"#FF6347", h:9, s:100, l:64], + [name:"Turquoise", rgb:"#40E0D0", h:174, s:72, l:56], + [name:"Violet", rgb:"#EE82EE", h:300, s:76, l:72], + [name:"Warm White", rgb:"#DAF17E", h:72, s:20, l:72], + [name:"Wheat", rgb:"#F5DEB3", h:39, s:77, l:83], + [name:"White", rgb:"#FFFFFF", h:0, s:0, l:100], + [name:"White Smoke", rgb:"#F5F5F5", h:0, s:0, l:96], + [name:"Yellow", rgb:"#FFFF00", h:60, s:100, l:50], + [name:"Yellow Green", rgb:"#9ACD32", h:80, s:61, l:50] ] } + +private isHubitat(){ + return hubUID != null +}