From 3eb81294de5f1036579866baa14a2e48cb1920d6 Mon Sep 17 00:00:00 2001 From: ajayjohn <1575879+ajayjohn@users.noreply.github.com> Date: Sun, 25 Mar 2018 15:11:30 -0500 Subject: [PATCH 01/55] Update webcore-dashboard.groovy --- smartapps/ady624/webcore-dashboard.src/webcore-dashboard.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/smartapps/ady624/webcore-dashboard.src/webcore-dashboard.groovy b/smartapps/ady624/webcore-dashboard.src/webcore-dashboard.groovy index e174c71b..6e5a26c6 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.104.20180323" } /*** webCoRE DEFINITION ***/ /******************************************************************************/ private static String handle() { return "webCoRE" } -include 'asynchttp_v1' +//include 'asynchttp_v1' definition( name: "${handle()} Dashboard", namespace: "ady624", From 078f1ad9a99e967ae350d3bb39122680a6c18acb Mon Sep 17 00:00:00 2001 From: ajayjohn <1575879+ajayjohn@users.noreply.github.com> Date: Sun, 25 Mar 2018 15:14:24 -0500 Subject: [PATCH 02/55] Update webcore-piston.groovy --- .../webcore-piston.src/webcore-piston.groovy | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/smartapps/ady624/webcore-piston.src/webcore-piston.groovy b/smartapps/ady624/webcore-piston.src/webcore-piston.groovy index ede6a91a..a5043777 100644 --- a/smartapps/ady624/webcore-piston.src/webcore-piston.groovy +++ b/smartapps/ady624/webcore-piston.src/webcore-piston.groovy @@ -285,7 +285,7 @@ public static String version() { return "v0.3.104.20180323" } /*** webCoRE DEFINITION ***/ /******************************************************************************/ private static String handle() { return "webCoRE" } -include 'asynchttp_v1' +//include 'asynchttp_v1' definition( name: "${handle()} Piston", namespace: "ady624", @@ -809,7 +809,7 @@ def handleEvents(event) { } checkVersion(rtData) setTimeoutRecoveryHandler('timeoutRecoveryHandler_webCoRE') - //runIn(30, timeRecoveryHandler) + //runIn(30.toInteger(), timeRecoveryHandler) if (rtData.semaphoreDelay) { warn "Piston waited at a semaphore for ${rtData.semaphoreDelay}ms", rtData } @@ -1134,7 +1134,7 @@ 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.toInteger(), timeHandler, [data: next]) //runIn(t + 30, timeRecoveryHandler, [data: next]) } else { rtData.stats.nextSchedule = 0 @@ -1653,7 +1653,7 @@ private Boolean executeTask(rtData, devices, statement, task, async) { return false } else { if (rtData.logging > 1) trace "Waiting for ${delay}ms", rtData - pause(delay) + pauseExecution(delay) } } tracePoint(rtData, "t:${task.$}", now() - t, delay) @@ -1744,7 +1744,7 @@ private executePhysicalCommand(rtData, device, command, params = [], delay = nul error "Error while executing physical command $device.$command($params):", rtData, null, all } if (rtData.piston.o?.ced) { - pause(rtData.piston.o.ced) + pauseExecution(rtData.piston.o.ced) if (rtData.logging > 2) debug "Injected a ${rtData.piston.o.ced}ms delay after [$device].$command(${params ? "$params" : ''})", rtData } } @@ -2886,9 +2886,9 @@ 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( + sendHubCommand(new hubitat.device.HubAction( "wake on lan $mac", - physicalgraph.device.Protocol.LAN, + hubitat.device.Protocol.LAN, null, secureCode ? [secureCode: secureCode] : [:] )) @@ -3151,7 +3151,7 @@ private long vcmd_lifxPulse(rtData, device, params) { } -public localHttpRequestHandler(physicalgraph.device.HubResponse hubResponse) { +public localHttpRequestHandler(hubitat.device.HubResponse hubResponse) { def responseCode = '' for (header in hubResponse.headers) { if (header.key.startsWith('http')) { @@ -3248,7 +3248,7 @@ private long vcmd_httpRequest(rtData, device, params) { query: method == "GET" ? data : null, //thank you @destructure00 body: method != "GET" ? data : null //thank you @destructure00 ] - sendHubCommand(new physicalgraph.device.HubAction(requestParams, null, [callback: localHttpRequestHandler])) + sendHubCommand(new hubitat.device.HubAction(requestParams, null, [callback: localHttpRequestHandler])) return 20000 } catch (all) { error "Error executing internal web request: ", rtData, null, all From 48d5408be4e20eed5dc2faffdcbb7d94b2069df3 Mon Sep 17 00:00:00 2001 From: ajayjohn <1575879+ajayjohn@users.noreply.github.com> Date: Sun, 25 Mar 2018 15:15:14 -0500 Subject: [PATCH 03/55] Update webcore-storage.groovy From 6b23e57a13c1c7915076dc5efce37004a7e187cd Mon Sep 17 00:00:00 2001 From: ajayjohn <1575879+ajayjohn@users.noreply.github.com> Date: Sun, 25 Mar 2018 15:15:58 -0500 Subject: [PATCH 04/55] Update webcore.groovy --- smartapps/ady624/webcore.src/webcore.groovy | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/smartapps/ady624/webcore.src/webcore.groovy b/smartapps/ady624/webcore.src/webcore.groovy index 8ba3ea4c..b9c9c02a 100644 --- a/smartapps/ady624/webcore.src/webcore.groovy +++ b/smartapps/ady624/webcore.src/webcore.groovy @@ -285,7 +285,7 @@ public static String version() { return "v0.3.104.20180323" } /******************************************************************************/ private static String handle() { return "webCoRE" } private static String domain() { return "webcore.co" } -include 'asynchttp_v1' +//include 'asynchttp_v1' definition( name: "${handle()}", namespace: "ady624", @@ -1897,7 +1897,7 @@ private testLifx() { requestContentType: "application/json" ] if (asynchttp_v1) asynchttp_v1.get(lifxHandler, requestParams, [request: 'scenes']) - pause(250) + pauseExecution(250) requestParams.path = "/v1/lights/all" if (asynchttp_v1) asynchttp_v1.get(lifxHandler, requestParams, [request: 'lights']) return true @@ -2006,7 +2006,7 @@ public Map getRunTimeData(semaphore = null, fetchWrappers = false) { break } waited = true - pause(250) + pauseExecution(250) } } def storageApp = !!fetchWrappers ? getStorageApp() : null @@ -2152,7 +2152,7 @@ def webCoREHandler(event) { switch (event.value) { case 'poll': int delay = (int) Math.round(2000 * Math.random()) - pause(delay) + pauseExecution(delay) broadcastPistonList() break; /* case 'ping': From 6c604c38a7101ebc04a3a72c1cdcc2b68ee2f4be Mon Sep 17 00:00:00 2001 From: ajayjohn <1575879+ajayjohn@users.noreply.github.com> Date: Sun, 25 Mar 2018 16:03:28 -0500 Subject: [PATCH 05/55] Update webcore-piston.groovy --- smartapps/ady624/webcore-piston.src/webcore-piston.groovy | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/smartapps/ady624/webcore-piston.src/webcore-piston.groovy b/smartapps/ady624/webcore-piston.src/webcore-piston.groovy index a5043777..83129742 100644 --- a/smartapps/ady624/webcore-piston.src/webcore-piston.groovy +++ b/smartapps/ady624/webcore-piston.src/webcore-piston.groovy @@ -771,16 +771,16 @@ try { } } +/* //new and improved timeout recovery management 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]) @@ -808,8 +808,8 @@ def handleEvents(event) { return; } checkVersion(rtData) - setTimeoutRecoveryHandler('timeoutRecoveryHandler_webCoRE') - //runIn(30.toInteger(), timeRecoveryHandler) + //setTimeoutRecoveryHandler('timeoutRecoveryHandler_webCoRE') + runIn(30.toInteger(), timeRecoveryHandler) if (rtData.semaphoreDelay) { warn "Piston waited at a semaphore for ${rtData.semaphoreDelay}ms", rtData } From da6f7f7cafb81e7399b34e7453d63eec17ec3c27 Mon Sep 17 00:00:00 2001 From: jp0550 Date: Wed, 28 Mar 2018 13:49:25 -0500 Subject: [PATCH 06/55] Fix piston pause, delete, schedules, and dash --- .../webcore-dashboard.groovy | 21 +++++++++++++-- .../webcore-piston.src/webcore-piston.groovy | 26 +++++++------------ smartapps/ady624/webcore.src/webcore.groovy | 8 +++--- 3 files changed, 32 insertions(+), 23 deletions(-) diff --git a/smartapps/ady624/webcore-dashboard.src/webcore-dashboard.groovy b/smartapps/ady624/webcore-dashboard.src/webcore-dashboard.groovy index 6e5a26c6..c035c5a2 100644 --- a/smartapps/ady624/webcore-dashboard.src/webcore-dashboard.groovy +++ b/smartapps/ady624/webcore-dashboard.src/webcore-dashboard.groovy @@ -147,6 +147,23 @@ private void broadcastEvent(deviceId, eventName, eventValue, eventTime) { def iid = state.instanceId def region = state.region ?: 'us' if (!iid || !iid.startsWith(':') || !iid.endsWith(':')) return + + 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] + ] + + log.trace(params) + + httpPut(params){ + resp ->resp.data + log.info("broadcastEvent response :: ${resp.data}") + } + + /* asynchttp_v1.put(null, [ uri: "https://api-${region}-${iid[32]}.webcore.co:9237", path: '/event/sink', @@ -157,7 +174,7 @@ private void broadcastEvent(deviceId, eventName, eventValue, eventTime) { v: eventValue, t: eventTime ] - ]) + ])*/ } /******************************************************************************/ @@ -195,4 +212,4 @@ def String hashId(id) { /*** ***/ /*** END OF CODE ***/ /*** ***/ -/******************************************************************************/ +/******************************************************************************/ \ 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 83129742..790d42c7 100644 --- a/smartapps/ady624/webcore-piston.src/webcore-piston.groovy +++ b/smartapps/ady624/webcore-piston.src/webcore-piston.groovy @@ -604,7 +604,7 @@ def setBin(bin) { return [:] } -Map pause() { +Map pausePiston() { state.active = false def rtData = getRunTimeData() def msg = timer "Piston successfully stopped", null, -1 @@ -614,8 +614,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 = [:] @@ -771,13 +771,6 @@ try { } } -/* -//new and improved timeout recovery management -def timeoutRecoveryHandler_webCoRE(event) { - timeHandler([t:now()], true) -} -*/ - def timeRecoveryHandler(event) { timeHandler(event, true) } @@ -789,7 +782,7 @@ def executeHandler(event) { //entry point for all events def handleEvents(event) { //cancel all pending jobs, we'll handle them later - //unschedule(timeHandler) + unschedule(timeHandler) if (!state.active) return def startTime = now() state.lastExecuted = startTime @@ -808,8 +801,7 @@ def handleEvents(event) { return; } checkVersion(rtData) - //setTimeoutRecoveryHandler('timeoutRecoveryHandler_webCoRE') - runIn(30.toInteger(), timeRecoveryHandler) + runIn(30.toInteger(), timeRecoveryHandler) if (rtData.semaphoreDelay) { warn "Piston waited at a semaphore for ${rtData.semaphoreDelay}ms", rtData } @@ -880,7 +872,7 @@ def handleEvents(event) { def delay = event.schedule.t - now() if (syncTime && (delay > 0)) { if (rtData.logging > 2) debug "Fast executing schedules, waiting for ${delay}ms to sync up", rtData - pause delay + pauseExecution delay } success = executeEvent(rtData, event) syncTime = true @@ -1135,11 +1127,11 @@ private processSchedules(rtData, scheduleJob = false) { 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.toInteger(), timeHandler, [data: next]) - //runIn(t + 30, timeRecoveryHandler, [data: next]) + runIn((t+30).toInteger(), timeRecoveryHandler, [data: next]) } else { rtData.stats.nextSchedule = 0 //remove the recovery - //unschedule(timeRecoveryHandler) + unschedule(timeRecoveryHandler) } } if (rtData.piston.o?.pep) atomicState.schedules = schedules @@ -7949,4 +7941,4 @@ private void setRandomValue(name, value) { private void resetRandomValues() { state.temp = state.temp ?: [:] state.temp.randoms = [:] -} +} \ No newline at end of file diff --git a/smartapps/ady624/webcore.src/webcore.groovy b/smartapps/ady624/webcore.src/webcore.groovy index b9c9c02a..0e39e1ab 100644 --- a/smartapps/ady624/webcore.src/webcore.groovy +++ b/smartapps/ady624/webcore.src/webcore.groovy @@ -1029,7 +1029,7 @@ private api_intf_dashboard_piston_create() { def result debug "Dashboard: Request received to generate a new piston name" if (verifySecurityToken(params.token)) { - def piston = addChildApp("ady624", "${handle()} Piston", params.name?:generatePistonName()) + def piston = addChildApp("ady624", "${handle()} Piston", params.name?:generatePistonName(), [:]) if (params.author || params.bin) { piston.config([bin: params.bin, author: params.author, initialVersion: version()]) } @@ -1247,7 +1247,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] @@ -1417,7 +1417,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(piston.id); result = [status: "ST_SUCCESS"] state.remove(params.id) state.remove('sph${params.id}') @@ -2105,7 +2105,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) } } From 5a98729d9b97fbc1d1e2e9f4aedadd88d731ad38 Mon Sep 17 00:00:00 2001 From: jp0550 Date: Sun, 8 Apr 2018 21:43:27 -0500 Subject: [PATCH 07/55] Support routine events --- smartapps/ady624/webcore-piston.src/webcore-piston.groovy | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/smartapps/ady624/webcore-piston.src/webcore-piston.groovy b/smartapps/ady624/webcore-piston.src/webcore-piston.groovy index 790d42c7..4729f295 100644 --- a/smartapps/ady624/webcore-piston.src/webcore-piston.groovy +++ b/smartapps/ady624/webcore-piston.src/webcore-piston.groovy @@ -4284,13 +4284,14 @@ private traverseExpressions(node, closure, param, parentNode = null) { } private getRoutineById(routineId) { - def routines = location.helloHome?.getPhrases() + return [ id : routineId ] + /*def routines = location.helloHome?.getPhrases() for(routine in routines) { if (routine && routine?.label && (hashId(routine.id) == routineId)) { return routine } } - return null + return null */ } private void updateDeviceList(deviceIdList) { @@ -4397,7 +4398,7 @@ private void subscribeAll(rtData) { def routine = getRoutineById(value.c) if (routine) { subscriptionId = "$deviceId${operand.v}${routine.id}" - attribute = "routineExecuted.${routine.id}" + attribute = "routineExecuted" } } break From 71748cc9f6b45ebd9affb268e9d2e49dfeca17aa Mon Sep 17 00:00:00 2001 From: jp0550 Date: Sun, 8 Apr 2018 21:42:36 -0500 Subject: [PATCH 08/55] Hubitat button implementation --- smartapps/ady624/webcore.src/webcore.groovy | 25 ++++++++++++--------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/smartapps/ady624/webcore.src/webcore.groovy b/smartapps/ady624/webcore.src/webcore.groovy index 0e39e1ab..473feaec 100644 --- a/smartapps/ady624/webcore.src/webcore.groovy +++ b/smartapps/ady624/webcore.src/webcore.groovy @@ -1543,7 +1543,7 @@ private api_intf_dashboard_piston_activity() { def api_ifttt() { def data = [:] - def remoteAddr = request.getHeader("X-FORWARDED-FOR") ?: request.getRemoteAddr() + def remoteAddr = "UNKNOWN" /*request.getHeader("X-FORWARDED-FOR") ?: request.getRemoteAddr() */ if (params) { data.params = [:] for(param in params) { @@ -1575,7 +1575,8 @@ def api_email() { private api_execute() { def result = [:] def data = [:] - def remoteAddr = request.getHeader("X-FORWARDED-FOR") ?: request.getRemoteAddr() + log.debug request + def remoteAddr = "UNKOWN" /*request.getHeader("X-FORWARDED-FOR") ?: request.getRemoteAddr()*/ debug "Dashboard: Request received to execute a piston from IP $remoteAddr" if (params) { data = [:] @@ -2384,8 +2385,7 @@ private static Map capabilities() { audioNotification : [ n: "Audio Notification", d: "audio notification devices", c: ["playText", "playTextAndResume", "playTextAndRestore", "playTrack", "playTrackAndResume", "playTrackAndRestore"], ], battery : [ n: "Battery", d: "battery powered devices", a: "battery", ], beacon : [ n: "Beacon", d: "beacons", a: "presence", ], - bulb : [ n: "Bulb", d: "bulbs", a: "switch", c: ["off", "on"], ], - button : [ n: "Button", d: "buttons", a: "button", m: true, s: "numberOfButtons,numButtons", i: "buttonNumber", ], + bulb : [ n: "Bulb", d: "bulbs", a: "switch", c: ["off", "on"], ], carbonDioxideMeasurement : [ n: "Carbon Dioxide Measurement", d: "carbon dioxide sensors", a: "carbonDioxide", ], carbonMonoxideDetector : [ n: "Carbon Monoxide Detector", d: "carbon monoxide detectors", a: "carbonMonoxide", ], colorControl : [ n: "Color Control", d: "adjustable color lights", a: "color", c: ["setColor", "setHue", "setSaturation"], ], @@ -2394,10 +2394,11 @@ private static Map capabilities() { consumable : [ n: "Consumable", d: "consumables", a: "consumableStatus", c: ["setConsumableStatus"], ], contactSensor : [ n: "Contact Sensor", d: "contact sensors", a: "contact", ], doorControl : [ n: "Door Control", d: "automatic doors", a: "door", c: ["close", "open"], ], + doubleTapableButton : [ n: "Double Tapable Button", d: "double tapable buttons", a: "doubleTapped", c: ["doubleTap"], ], energyMeter : [ n: "Energy Meter", d: "energy meters", a: "energy", ], estimatedTimeOfArrival : [ n: "Estimated Time of Arrival", d: "moving devices (ETA)", a: "eta", ], garageDoorControl : [ n: "Garage Door Control", d: "automatic garage doors", a: "door", c: ["close", "open"], ], - holdableButton : [ n: "Holdable Button", d: "holdable buttons", a: "button", m: true, s: "numberOfButtons,numButtons", i: "buttonNumber", ], + holdableButton : [ n: "Holdable Button", d: "holdable buttons", a: "held", c: ["hold"] ], illuminanceMeasurement : [ n: "Illuminance Measurement", d: "illuminance sensors", a: "illuminance", ], imageCapture : [ n: "Image Capture", d: "cameras, imaging devices", a: "image", c: ["take"], ], indicator : [ n: "Indicator", d: "indicator devices", a: "indicatorStatus", c: ["indicatorNever", "indicatorWhenOn", "indicatorWhenOff"], ], @@ -2416,6 +2417,7 @@ private static Map capabilities() { powerMeter : [ n: "Power Meter", d: "power meters", a: "power", ], powerSource : [ n: "Power Source", d: "multisource powered devices", a: "powerSource", ], presenceSensor : [ n: "Presence Sensor", d: "presence sensors", a: "presence", ], + pushableButton : [ n: "Pushable Button", d: "pushable buttons", a: "pushed", c: ["push"], ], refresh : [ n: "Refresh", d: "refreshable devices", c: ["refresh"], ], relativeHumidityMeasurement : [ n: "Relative Humidity Measurement", d: "humidity sensors", a: "humidity", ], relaySwitch : [ n: "Relay Switch", d: "relay switches", a: "switch", c: ["off", "on"], ], @@ -2460,8 +2462,7 @@ private static Map attributes() { axisX : [ n: "X axis", t: "integer", r: [-1024, 1024], s: "threeAxis", ], axisY : [ n: "Y axis", t: "integer", r: [-1024, 1024], s: "threeAxis", ], axisZ : [ n: "Z axis", t: "integer", r: [-1024, 1024], s: "threeAxis", ], - battery : [ n: "battery", t: "integer", r: [0, 100], u: "%", ], - button : [ n: "button", t: "enum", o: ["pushed", "held"], c: "button", m: true, s: "numberOfButtons,numButtons", i: "buttonNumber" ], + battery : [ n: "battery", t: "integer", r: [0, 100], u: "%", ], carbonDioxide : [ n: "carbon dioxide", t: "decimal", r: [0, null], ], carbonMonoxide : [ n: "carbon monoxide", t: "enum", o: ["clear", "detected", "tested"], ], color : [ n: "color", t: "color", ], @@ -2471,12 +2472,13 @@ private static Map attributes() { coolingSetpoint : [ n: "cooling setpoint", t: "decimal", r: [-127, 127], u: '°?', ], currentActivity : [ n: "current activity", t: "string", ], door : [ n: "door", t: "enum", o: ["closed", "closing", "open", "opening", "unknown"], p: true, ], + doubleTapped : [ n: "double tapped button", t: "integer", c: "doubleTapableButton" ], energy : [ n: "energy", t: "decimal", r: [0, null], u: "kWh", ], eta : [ n: "ETA", t: "datetime", ], goal : [ n: "goal", t: "integer", r: [0, null], ], heatingSetpoint : [ n: "heating setpoint", t: "decimal", r: [-127, 127], u: '°?', ], - hex : [ n: "hexadecimal code", t: "hexcolor", ], - holdableButton : [ n: "holdable button", t: "enum", o: ["held", "pushed"], c: "holdableButton", m: true, ], + held : [ n: "held button", t: "integer", c: "holdableButton" ], + hex : [ n: "hexadecimal code", t: "hexcolor", ], hue : [ n: "hue", t: "integer", r: [0, 360], u: "°", ], humidity : [ n: "relative humidity", t: "integer", r: [0, 100], u: "%", ], illuminance : [ n: "illuminance", t: "integer", r: [0, null], u: "lux", ], @@ -2497,6 +2499,7 @@ private static Map attributes() { power : [ n: "power", t: "decimal", u: "W", ], powerSource : [ n: "power source", t: "enum", o: ["battery", "dc", "mains", "unknown"], ], presence : [ n: "presence", t: "enum", o: ["not present", "present"], ], + pushed : [ n: "pushed button", t: "integer", c: "pushableButton" ], rssi : [ n: "signal strength", t: "integer", r: [0, 100], u: "%", ], saturation : [ n: "saturation", t: "integer", r: [0, 100], u: "%", ], schedule : [ n: "schedule", t: "object", ], @@ -2561,6 +2564,7 @@ private static Map commands() { configure : [ n: "Configure", i: 'gear', ], cool : [ n: "Set to Cool", i: 'asterisk', a: "thermostatMode", v: "cool", ], deviceNotification : [ n: "Send device notification...", d: "Send device notification \"{0}\"", p: [[n:"Message",t:"string"]], ], + doubleTap : [ n: "Double Tap", d: "Double tap button {0}", a: "doubleTapped", p:[[n: "Button #", t: "integer"]] ], emergencyHeat : [ n: "Set to Emergency Heat", a: "thermostatMode", v: "emergency heat", ], fanAuto : [ n: "Set fan to Auto", a: "thermostatFanMode", v: "auto", ], fanCirculate : [ n: "Set fan to Circulate", a: "thermostatFanMode", v: "circulate", ], @@ -2568,6 +2572,7 @@ private static Map commands() { getAllActivities : [ n: "Get all activities", ], getCurrentActivity : [ n: "Get current activity", ], heat : [ n: "Set to Heat", i: 'fire', a: "thermostatMode", v: "heat", ], + hold : [ n: "Hold", d: "Hold Button {0}", a: "held", p: [[n:"Button #", t: "integer"]] ], indicatorNever : [ n: "Disable indicator", ], indicatorWhenOff : [ n: "Enable indicator when off", ], indicatorWhenOn : [ n: "Enable indicator when on", ], @@ -2588,7 +2593,7 @@ private static Map commands() { poll : [ n: "Poll", i: 'question', ], presetPosition : [ n: "Move to preset position", a: "windowShade", v: "partially open", ], previousTrack : [ n: "Previous track", ], - push : [ n: "Push", ], + push : [ n: "Push", d: "Push button {0}", a: "pushed", p:[[n: "Button #", t: "integer"]] ], refresh : [ n: "Refresh", i: 'refresh', ], restoreTrack : [ n: "Restore track...", d: "Restore track {0}", p: [[n:"Track URL",t:"url"]], ], resumeTrack : [ n: "Resume track...", d: "Resume track {0}", p: [[n:"Track URL",t:"url"]], ], From 2a020eab3fc3645f4ac261c34f8d73a8bc6d1142 Mon Sep 17 00:00:00 2001 From: jp0550 Date: Mon, 9 Apr 2018 03:22:03 -0500 Subject: [PATCH 09/55] Fix momentary capability --- .../webcore-piston.src/webcore-piston.groovy | 22 ++++++++++--------- .../webcore-storage.groovy | 11 ++++++++-- smartapps/ady624/webcore.src/webcore.groovy | 16 ++++++++++---- 3 files changed, 33 insertions(+), 16 deletions(-) diff --git a/smartapps/ady624/webcore-piston.src/webcore-piston.groovy b/smartapps/ady624/webcore-piston.src/webcore-piston.groovy index 4729f295..dc340b10 100644 --- a/smartapps/ady624/webcore-piston.src/webcore-piston.groovy +++ b/smartapps/ady624/webcore-piston.src/webcore-piston.groovy @@ -1610,20 +1610,22 @@ private Boolean executeTask(rtData, devices, statement, task, async) { params.push p } - def vcmd = rtData.commands.virtual[task.c] + def command = task.c == "pushMomentary" ? "push" : 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)) { + 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 } @@ -1652,15 +1654,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 } diff --git a/smartapps/ady624/webcore-storage.src/webcore-storage.groovy b/smartapps/ady624/webcore-storage.src/webcore-storage.groovy index 12231eb8..2f407fb3 100644 --- a/smartapps/ady624/webcore-storage.src/webcore-storage.groovy +++ b/smartapps/ady624/webcore-storage.src/webcore-storage.groovy @@ -161,10 +161,17 @@ def Map listAvailableDevices(raw = false) { 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) }.collect{[n: transformCommand(it), p: it.getArguments()]} ]]} } } +private def transformCommand(command){ + if(command.getName() == "push" && (command.getArguments()?.size() ?: 0) == 0){ + return "pushMomentary" + } + return command.getName() +} + def Map getDashboardData() { boolean ok def value @@ -231,4 +238,4 @@ def String hashId(id) { /*** ***/ /*** 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 473feaec..1c47b676 100644 --- a/smartapps/ady624/webcore.src/webcore.groovy +++ b/smartapps/ady624/webcore.src/webcore.groovy @@ -1575,7 +1575,6 @@ def api_email() { private api_execute() { def result = [:] def data = [:] - log.debug request def remoteAddr = "UNKOWN" /*request.getHeader("X-FORWARDED-FOR") ?: request.getRemoteAddr()*/ debug "Dashboard: Request received to execute a piston from IP $remoteAddr" if (params) { @@ -1705,6 +1704,7 @@ private getDashboardApp(install = false) { return dashboardApp } + private String getDashboardInitUrl(register = false) { def url = register ? getDashboardRegistrationUrl() : getDashboardUrl() if (!url) return null @@ -1725,7 +1725,7 @@ public Map listAvailableDevices(raw = false, updateCache = false) { 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) }.collect{[n: transformCommand(it), p: it.getArguments()]} ]]} } } List presenceDevices = getChildDevices() @@ -1739,6 +1739,13 @@ public Map listAvailableDevices(raw = false, updateCache = false) { return result } +private def transformCommand(command){ + if(command.getName() == "push" && (command.getArguments()?.size() ?: 0) == 0){ + return "pushMomentary" + } + return command.getName() +} + private Map listAvailableContacts(raw = false, updateCache = false) { def storageApp = getStorageApp() if (storageApp) return storageApp.listAvailableContacts(raw) @@ -2407,7 +2414,7 @@ private static Map capabilities() { lock : [ n: "Lock", d: "electronic locks", a: "lock", c: ["lock", "unlock"], s:"numberOfCodes,numCodes", i: "usedCode", ], lockOnly : [ n: "Lock Only", d: "electronic locks (lock only)", a: "lock", c: ["lock"], ], mediaController : [ n: "Media Controller", d: "media controllers", a: "currentActivity", c: ["startActivity", "getAllActivities", "getCurrentActivity"], ], - momentary : [ n: "Momentary", d: "momentary switches", c: ["push"], ], + momentary : [ n: "Momentary", d: "momentary switches", c: ["pushMomentary"], ], motionSensor : [ n: "Motion Sensor", d: "motion sensors", a: "motion", ], musicPlayer : [ n: "Music Player", d: "music players", a: "status", c: ["mute", "nextTrack", "pause", "play", "playTrack", "previousTrack", "restoreTrack", "resumeTrack", "setLevel", "setTrack", "stop", "unmute"], ], notification : [ n: "Notification", d: "notification devices", c: ["deviceNotification"], ], @@ -2594,6 +2601,7 @@ private static Map commands() { presetPosition : [ n: "Move to preset position", a: "windowShade", v: "partially open", ], previousTrack : [ n: "Previous track", ], push : [ n: "Push", d: "Push button {0}", a: "pushed", p:[[n: "Button #", t: "integer"]] ], + pushMomentary : [ n: "Push" ], refresh : [ n: "Refresh", i: 'refresh', ], restoreTrack : [ n: "Restore track...", d: "Restore track {0}", p: [[n:"Track URL",t:"url"]], ], resumeTrack : [ n: "Resume track...", d: "Resume track {0}", p: [[n:"Track URL",t:"url"]], ], @@ -3011,4 +3019,4 @@ private Map virtualDevices(updateCache = false) { routine: [ n: 'Routine', t: 'enum', o: getRoutineOptions(updateCache), m: true], alarmSystemStatus: [ n: 'Smart Home Monitor status', t: 'enum', o: getAlarmSystemStatusOptions(), x: true], ] -} +} \ No newline at end of file From f9fe08bb5e2c2406f56ba543c947de7fe5eeefb8 Mon Sep 17 00:00:00 2001 From: jp0550 Date: Mon, 9 Apr 2018 16:53:19 -0500 Subject: [PATCH 10/55] Support custom urls --- smartapps/ady624/webcore.src/webcore.groovy | 47 +++++++++++++++++++-- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/smartapps/ady624/webcore.src/webcore.groovy b/smartapps/ady624/webcore.src/webcore.groovy index 1c47b676..8bafb970 100644 --- a/smartapps/ady624/webcore.src/webcore.groovy +++ b/smartapps/ady624/webcore.src/webcore.groovy @@ -386,6 +386,11 @@ 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", title: "Use custom endpoints?", default: false, required: true + input "customHubUrl", "string", title: "Custom hub url different from https://cloud.hubitat.com", default: null, required: false + input "customWebcoreInstanceUrl", "string", title: "Custom webcore instance url different from dashboard.webcore.co", default: null, required: false + paragraph "If you enter a custom url above you will have to use a different webcore instance from dashboard.webcore.co as they restrict their api to hubitat and smartthing's cloud" + } } @@ -808,6 +813,14 @@ def installed() { } def updated() { + if(state.accessToken){ + if(customEndpoints && (customHubUrl ?: "") != ""){ + state.endpoint = customServerUrl("?access_token=${state.accessToken}") + } + else { + state.endpoint = hubUID ? apiServerUrl("$hubUID/apps/${app.id}/?access_token=${state.accessToken}") : apiServerUrl("/api/token/${accessToken}/smartapps/installations/${app.id}/") + } + } warn "Updating webCoRE ${version()}" unsubscribe() unschedule() @@ -850,7 +863,12 @@ private initializeWebCoREEndpoint() { 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}/") + if(customEndpoints && (customHubUrl ?: "") != ""){ + state.endpoint = customServerUrl("?access_token=${state.accessToken}") + } + else { + state.endpoint = hubUID ? apiServerUrl("$hubUID/apps/${app.id}/?access_token=${state.accessToken}") : apiServerUrl("/api/token/${accessToken}/smartapps/installations/${app.id}/") + } } } catch(e) { state.endpoint = null @@ -1704,11 +1722,28 @@ private getDashboardApp(install = false) { return dashboardApp } +def customServerUrl(path){ + path ?: "" + if(!path.startsWith("/")){ + path = "/" + 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(customEndpoints && (customHubUrl ?: "") != ""){ + 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("-", "") + (hubUID ? '/?access_token=' + state.accessToken : '')).bytes.encodeBase64() + } } private String getDashboardRegistrationUrl() { @@ -1980,7 +2015,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() { From 796f7a4f8250e4cbc92ad08c78f4ec1fb8b7e2c9 Mon Sep 17 00:00:00 2001 From: jp0550 Date: Thu, 12 Apr 2018 22:42:04 -0500 Subject: [PATCH 11/55] Fix non https local urls --- dashboard/js/app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dashboard/js/app.js b/dashboard/js/app.js index 988a6cdf..bceacc49 100644 --- a/dashboard/js/app.js +++ b/dashboard/js/app.js @@ -803,7 +803,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) { From 9e19adf66c6398ef4a504491003220ec25c0975c Mon Sep 17 00:00:00 2001 From: jp0550 Date: Fri, 13 Apr 2018 09:22:21 -0500 Subject: [PATCH 12/55] Add colors --- .../webcore-piston.src/webcore-piston.groovy | 6 +- smartapps/ady624/webcore.src/webcore.groovy | 156 +++++++++++++++++- 2 files changed, 158 insertions(+), 4 deletions(-) diff --git a/smartapps/ady624/webcore-piston.src/webcore-piston.groovy b/smartapps/ady624/webcore-piston.src/webcore-piston.groovy index dc340b10..93d09cc7 100644 --- a/smartapps/ady624/webcore-piston.src/webcore-piston.groovy +++ b/smartapps/ady624/webcore-piston.src/webcore-piston.groovy @@ -2176,7 +2176,7 @@ private long cmd_setColorTemperature(rtData, device, params) { } private getColor(colorValue) { - def color = (colorValue == 'Random') ? colorUtil?.RANDOM : colorUtil?.findByName(colorValue) + def color = (colorValue == 'Random') ? (colorUtil?.RANDOM ?: parent.getRandomColor()) : (colorUtil?.findByName(colorValue) ?: parent.getColorByName(colorValue)) if (color) { color = [ hex: color.rgb, @@ -7912,8 +7912,8 @@ 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 ?: parent.getRandomColor())?.rgb; setRandomValue("\$randomColor", result); return result + case "\$randomColorName": def result = getRandomValue("\$randomColorName") ?: (colorUtil?.RANDOM ?: parent.getRandomColor())?.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 diff --git a/smartapps/ady624/webcore.src/webcore.groovy b/smartapps/ady624/webcore.src/webcore.groovy index 8bafb970..0dcf838a 100644 --- a/smartapps/ady624/webcore.src/webcore.groovy +++ b/smartapps/ady624/webcore.src/webcore.groovy @@ -1085,7 +1085,7 @@ private api_intf_dashboard_piston_get() { comparisons: comparisons(), functions: functions(), colors: [ - standard: colorUtil?.ALL + standard: colorUtil?.ALL ?: getColors() ], ] } @@ -3060,4 +3060,158 @@ private Map virtualDevices(updateCache = false) { routine: [ n: 'Routine', t: 'enum', o: getRoutineOptions(updateCache), m: true], alarmSystemStatus: [ n: 'Smart Home Monitor status', t: 'enum', o: getAlarmSystemStatusOptions(), x: true], ] +} +public Map getColorByName(name){ + return getColors().find{ it.name == name } +} +public Map getRandomColor(){ + def random = (int)(Math.random() * getColors().size()) + return getColors()[random] +} + +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] + ] } \ No newline at end of file From e01ce0f2296d389694665c656c36d3b4bdb4b464 Mon Sep 17 00:00:00 2001 From: jp0550 Date: Sun, 15 Apr 2018 23:44:23 -0500 Subject: [PATCH 13/55] Fix for virtual flash, and looser restrictions on timing --- .../webcore-dashboard.groovy | 4 +-- .../webcore-piston.src/webcore-piston.groovy | 30 ++++++++++++------- .../webcore-storage.groovy | 16 +++++++--- smartapps/ady624/webcore.src/webcore.groovy | 22 ++++++++++---- 4 files changed, 48 insertions(+), 24 deletions(-) diff --git a/smartapps/ady624/webcore-dashboard.src/webcore-dashboard.groovy b/smartapps/ady624/webcore-dashboard.src/webcore-dashboard.groovy index c035c5a2..47cb6354 100644 --- a/smartapps/ady624/webcore-dashboard.src/webcore-dashboard.groovy +++ b/smartapps/ady624/webcore-dashboard.src/webcore-dashboard.groovy @@ -155,9 +155,7 @@ private void broadcastEvent(deviceId, eventName, eventValue, eventTime) { headers: ['ST' : state.instanceId], body: [d: deviceId, n: eventName, v: eventValue, t: eventTime] ] - - log.trace(params) - + httpPut(params){ resp ->resp.data log.info("broadcastEvent response :: ${resp.data}") diff --git a/smartapps/ady624/webcore-piston.src/webcore-piston.groovy b/smartapps/ady624/webcore-piston.src/webcore-piston.groovy index 93d09cc7..85a89c18 100644 --- a/smartapps/ady624/webcore-piston.src/webcore-piston.groovy +++ b/smartapps/ady624/webcore-piston.src/webcore-piston.groovy @@ -823,8 +823,9 @@ 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 && (30000 + rtData.timestamp - now() > 10000)) { //allocate 30 seconds total execution time with max of 20 for schedule loop + //we only keep doing stuff if we haven't passed the 20s execution time mark 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... @@ -832,7 +833,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() + 3000 }] } if (!event.schedule) break long threshold = now() > event.schedule.t ? now() : event.schedule.t @@ -1609,8 +1610,10 @@ private Boolean executeTask(rtData, devices, statement, task, async) { //ensure value type is successfuly passed through params.push p } - - def command = task.c == "pushMomentary" ? "push" : task.c + + //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[command] long delay = 0 @@ -1634,12 +1637,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 = 30000 + 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 < 3000) || (delay >= 5000) || 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) @@ -1670,6 +1674,10 @@ private long executeVirtualCommand(rtData, devices, command, params) } private executePhysicalCommand(rtData, device, command, params = [], delay = null, scheduleDevice = null, disableCommandOptimization = false) { + if(!!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 @@ -1716,7 +1724,7 @@ private executePhysicalCommand(rtData, device, command, params = [], delay = nul msg.m = "Skipped execution of physical command [${device.label}].$command($params) because it would make no change to the device." } else { if (params.size()) { - if (delay) { + if (delay) { //not supported device."$command"((params as Object[]) + [delay: delay]) msg.m = "Executed physical command [${device.label}].$command($params, [delay: $delay])" } else { @@ -1724,7 +1732,7 @@ private executePhysicalCommand(rtData, device, command, params = [], delay = nul msg.m = "Executed physical command [${device.label}].$command($params)" } } else { - if (delay) { + if (delay) { //not supported device."$command"([delay: delay]) msg.m = "Executed physical command [${device.label}].$command([delay: $delay])" } else { @@ -2644,7 +2652,7 @@ private long vcmd_internal_fade(Map rtData, device, String command, int startLev return duration + 100 } -private long vcmd_flash(rtData, device, params) { +private long vcmd_emulatedFlash(rtData, device, params) { long onDuration = cast(rtData, params[0], 'long') long offDuration = cast(rtData, params[1], 'long') int cycles = cast(rtData, params[2], 'integer') diff --git a/smartapps/ady624/webcore-storage.src/webcore-storage.groovy b/smartapps/ady624/webcore-storage.src/webcore-storage.groovy index 2f407fb3..ceea3139 100644 --- a/smartapps/ady624/webcore-storage.src/webcore-storage.groovy +++ b/smartapps/ady624/webcore-storage.src/webcore-storage.groovy @@ -158,16 +158,18 @@ 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{ transformCommand(it) }.collect{[n: transformCommand(it), 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){ - if(command.getName() == "push" && (command.getArguments()?.size() ?: 0) == 0){ - return "pushMomentary" +private def transformCommand(command, overrides){ + def override = overrides[command.getName()] + if(override && override.s == command.getArguments()?.toString()){ + return override.r; } return command.getName() } @@ -203,6 +205,12 @@ public String mem(showBytes = true) { return Math.round(100.00 * (bytes/ 100000.00)) + "%${showBytes ? " ($bytes bytes)" : ""}" } +public Map commandOverrides(){ + return [ + push : [c: "push", s: null , r: "pushMomentary"] + ] +} + /******************************************************************************/ /*** ***/ /*** SECURITY METHODS ***/ diff --git a/smartapps/ady624/webcore.src/webcore.groovy b/smartapps/ady624/webcore.src/webcore.groovy index 0dcf838a..a645d894 100644 --- a/smartapps/ady624/webcore.src/webcore.groovy +++ b/smartapps/ady624/webcore.src/webcore.groovy @@ -1757,10 +1757,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{ transformCommand(it) }.collect{[n: transformCommand(it), 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() @@ -1774,9 +1775,10 @@ public Map listAvailableDevices(raw = false, updateCache = false) { return result } -private def transformCommand(command){ - if(command.getName() == "push" && (command.getArguments()?.size() ?: 0) == 0){ - return "pushMomentary" +private def transformCommand(command, overrides){ + def override = overrides[command.getName()] + if(override && override.s == command.getArguments()?.toString()){ + return override.r; } return command.getName() } @@ -2067,7 +2069,8 @@ public Map getRunTimeData(semaphore = null, fetchWrappers = false) { semaphoreDelay: semaphoreDelay, commands: [ physical: commands(), - virtual: virtualCommands() + virtual: virtualCommands(), + overrides: commandOverrides() ], comparisons: comparisons(), coreVersion: version(), @@ -2602,6 +2605,12 @@ private static Map attributes() { ] } +public Map commandOverrides(){ + return [ + push : [c: "push", s: null , r: "pushMomentary"] + ] +} + private static Map commands() { return [ auto : [ n: "Set to Auto", a: "thermostatMode", v: "auto", ], @@ -2617,6 +2626,7 @@ private static Map commands() { fanAuto : [ n: "Set fan to Auto", a: "thermostatFanMode", v: "auto", ], fanCirculate : [ n: "Set fan to Circulate", a: "thermostatFanMode", v: "circulate", ], fanOn : [ n: "Set fan to On", a: "thermostatFanMode", v: "on", ], + flash : [ n: "Flash", ], getAllActivities : [ n: "Get all activities", ], getCurrentActivity : [ n: "Get current activity", ], heat : [ n: "Set to Heat", i: 'fire', a: "thermostatMode", v: "heat", ], @@ -2775,7 +2785,7 @@ private static Map virtualCommands() { fadeSaturation : [ n: "Fade saturation...", r: ["setSaturation"], i: "toggle-on", d: "Fade saturation{0} to {1}% in {2}{3}", p: [[n:"Starting saturation",t:"level",d:" from {v}%"],[n:"Final saturation",t:"level"],[n:"Duration",t:"duration"], [n:"Only if switch is...", t:"enum",o:["on","off"], d:" if already {v}"]], ], fadeHue : [ n: "Fade hue...", r: ["setHue"], i: "toggle-on", d: "Fade hue{0} to {1}° in {2}{3}", p: [[n:"Starting hue",t:"hue",d:" from {v}°"],[n:"Final hue",t:"hue"],[n:"Duration",t:"duration"], [n:"Only if switch is...", t:"enum",o:["on","off"], d:" if already {v}"]], ], fadeColorTemperature : [ n: "Fade color temperature...", r: ["setColorTemperature"], i: "toggle-on", d: "Fade color temperature{0} to {1}°K in {2}{3}", p: [[n:"Starting color temperature",t:"colorTemperature",d:" from {v}°K"],[n:"Final color temperature",t:"colorTemperature"],[n:"Duration",t:"duration"], [n:"Only if switch is...", t:"enum",o:["on","off"], d:" if already {v}"]], ], - 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}"]], ], + emulatedFlash : [ 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}"]], ], flashLevel : [ n: "Flash (level)...", r: ["setLevel"], i: "toggle-on", d: "Flash {0}% {1} / {2}% {3} for {4} times{5}", p: [[n:"Level 1", t:"level"],[n:"Duration 1",t:"duration"],[n:"Level 2", t:"level"],[n:"Duration 2",t:"duration"],[n:"Number of flashes",t:"integer"], [n:"Only if switch is...", t:"enum",o:["on","off"], d:" if already {v}"]], ], flashColor : [ n: "Flash (color)...", r: ["setColor"], i: "toggle-on", d: "Flash {0} {1} / {2} {3} for {4} times{5}", p: [[n:"Color 1", t:"color"],[n:"Duration 1",t:"duration"],[n:"Color 2", t:"color"],[n:"Duration 2",t:"duration"],[n:"Number of flashes",t:"integer"], [n:"Only if switch is...", t:"enum",o:["on","off"], d:" if already {v}"]], ], iftttMaker : [ n: "Send an IFTTT Maker event...", a: true, d: "Send the {0} IFTTT Maker event{1}{2}{3}", p: [[n:"Event", t:"text"], [n:"Value 1", t:"string", d:", passing value1 = '{v}'"], [n:"Value 2", t:"string", d:", passing value2 = '{v}'"], [n:"Value 3", t:"string", d:", passing value3 = '{v}'"]], ], From 9acafbd31c456e272cdac9f4244b5b3feb01751e Mon Sep 17 00:00:00 2001 From: jp0550 Date: Mon, 9 Apr 2018 16:34:52 -0500 Subject: [PATCH 14/55] Start porting hsm Remove hubUID restrictions and fix saveState error --- .../webcore-piston.src/webcore-piston.groovy | 32 ++++++--- smartapps/ady624/webcore.src/webcore.groovy | 65 ++++++++++++++----- 2 files changed, 71 insertions(+), 26 deletions(-) diff --git a/smartapps/ady624/webcore-piston.src/webcore-piston.groovy b/smartapps/ady624/webcore-piston.src/webcore-piston.groovy index 85a89c18..4d23db5d 100644 --- a/smartapps/ady624/webcore-piston.src/webcore-piston.groovy +++ b/smartapps/ady624/webcore-piston.src/webcore-piston.groovy @@ -2362,7 +2362,7 @@ 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] } if (status && status.size()) { - sendLocationEvent(name: 'alarmSystemStatus', value: status[0].id) + sendLocationEvent(name: 'hsmStatus', value: status[0].id) } else { error "Error setting SmartThings Home Monitor status. Status '$statusIdOrName' does not exist.", rtData } @@ -3343,7 +3343,14 @@ private long vcmd_writeToFuelStream(rtData, device, params) { ], requestContentType: "application/json" ] - if (asynchttp_v1) asynchttp_v1.put(null, requestParams) + if (asynchttp_v1) { + asynchttp_v1.put(null, requestParams) + } + else { + httpPut(requestParams) { + + } + } return 0 } @@ -4394,12 +4401,15 @@ private void subscribeAll(rtData) { def subscriptionId = null def attribute = null switch (operand.v) { + case 'alarmSystemStatus': + subscriptionId = "$deviceId${operand.v}" + attribute = "hsmStatus" + break; case 'time': case 'date': case 'datetime': case 'mode': case 'powerSource': - case 'alarmSystemStatus': subscriptionId = "$deviceId${operand.v}" attribute = operand.v break @@ -4739,8 +4749,8 @@ private Map getDeviceAttribute(rtData, deviceId, attributeName, subDeviceIndex = 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] + def v = rtData.hsmStatus + def n = rtData.virtualDevices['alarmSystemStatus']?.o[v] return [t: 'string', v: v, n: n] } return [t: 'string', v: location.getName().toString()] @@ -7657,7 +7667,13 @@ private log(message, rtData = null, shift = null, err = null, cmd = null, force } } if (hubUID) { - log."$cmd" "$prefix $message" + if(err){ + log."$cmd" "$prefix $message $err" + } + else { + log."$cmd" "$prefix $message" + } + } else { log."$cmd" "$prefix $message", err } @@ -7873,7 +7889,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], + "\$hsmStatus": [t: "string", d: true], "\$version": [t: "string", d: true] ].sort{it.key} } @@ -7926,7 +7942,7 @@ private getSystemVariableValue(rtData, name) { 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 "\$hsmStatus": switch (rtData.hsmStatus) { case 'disarmed': return 'Disarmed'; case 'armedHome': return 'Armed/Home'; case 'armedAway': return 'Armed/Away'; }; return null; } } diff --git a/smartapps/ady624/webcore.src/webcore.groovy b/smartapps/ady624/webcore.src/webcore.groovy index a645d894..9cd9d39c 100644 --- a/smartapps/ady624/webcore.src/webcore.groovy +++ b/smartapps/ady624/webcore.src/webcore.groovy @@ -892,6 +892,7 @@ private subscribeAll() { subscribe(location, "echoSistant", echoSistantHandler) subscribe(location, "HubUpdated", hubUpdatedHandler, [filterEvents: false]) subscribe(location, "summary", summaryHandler, [filterEvents: false]) + subscribe(location, "hsmStatus", hsmHandler, [filterEvents: false]) setPowerSource(getHub()?.isBatteryInUse() ? 'battery' : 'mains') } @@ -975,7 +976,7 @@ private api_get_base_result(deviceVersion = 0, updateCache = false) { id: hashId(location.id, 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: transformHsmStatus(atomicState["hsmStatus"]), name: location.name, temperatureScale: location.getTemperatureScale(), timeZone: tz ? [ @@ -989,6 +990,22 @@ private api_get_base_result(deviceVersion = 0, updateCache = false) { ] } +private String transformHsmStatus(status){ + switch(status){ + case "disarmed": + return "off" + break; + case "armedHome": + return "stay" + break; + case "armedAway": + return "away" + break; + default: + return "Unknown" + } +} + private api_intf_dashboard_load() { def result recoveryHandler() @@ -1051,7 +1068,6 @@ 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() result = [status: "ST_SUCCESS", id: hashId(piston.id)] } else { result = api_get_error_result("ERR_INVALID_TOKEN") @@ -1574,7 +1590,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." } @@ -1585,7 +1601,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" } @@ -1608,7 +1624,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' @@ -1633,7 +1649,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()) { @@ -1805,7 +1821,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() { @@ -1879,7 +1895,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() { @@ -1960,7 +1976,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], @@ -1976,7 +1993,15 @@ private registerInstance() { pd: pd, lpd: lpd.join(',') ] - ]) + ] + if (asynchttp_v1) + { + asynchttp_v1.put(instanceRegistrationHandler, params) + } + else { + params << [contentType: 'text/plain'] + httpPut(params) { res -> } + } } private initSunriseAndSunset() { @@ -2081,6 +2106,7 @@ public Map getRunTimeData(semaphore = null, fetchWrappers = false) { globalStore: state.store ?: [:], settings: state.settings ?: [:], lifx: state.lifx ?: [:], + hsmStatus: atomicState.hsmStatus, powerSource: state.powerSource ?: 'mains', region: state.endpoint.contains('graph-eu') ? 'eu' : 'us', instanceId: hashId(app.id), @@ -2180,11 +2206,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) { @@ -2274,7 +2300,9 @@ def NewIncidentHandler(evt) { //log.error "$evt.name >>> ${evt.jsonData}" } - +def hsmHandler(evt){ + atomicState["hsmStatus"] = evt.value +} def lifxHandler(response, cbkData) { if ((response.status == 200)) { @@ -2398,7 +2426,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 (hubUID) { log.error "$prefix$message $err" } else { log.error "$prefix$message", err } } else { log.debug "$prefix$message", err } @@ -3031,12 +3059,13 @@ private Map getLocationModeOptions(updateCache = false) { } private static Map getAlarmSystemStatusOptions() { return [ - off: "Disarmed", - stay: "Armed/Stay", - away: "Armed/Away" + disarmed: "Disarmed", + armedHome: "Armed/Home", + armedAway: "Armed/Away" ] } + private Map getRoutineOptions(updateCache = false) { def routines = location.helloHome?.getPhrases() def result = [:] @@ -3068,7 +3097,7 @@ 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: 'Home Security Monitor status', t: 'enum', o: getAlarmSystemStatusOptions(), x: true], ] } public Map getColorByName(name){ From 6d084abf8d7f6f9a7713f8869a0476b474cda00b Mon Sep 17 00:00:00 2001 From: jp0550 Date: Fri, 4 May 2018 16:41:30 -0500 Subject: [PATCH 15/55] Fix execute links --- dashboard/js/modules/dashboard.module.js | 2 +- dashboard/js/modules/piston.module.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/dashboard/js/modules/dashboard.module.js b/dashboard/js/modules/dashboard.module.js index eb5a77f8..e2d1a4af 100644 --- a/dashboard/js/modules/dashboard.module.js +++ b/dashboard/js/modules/dashboard.module.js @@ -34,7 +34,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:' + '?access_token=' + si.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 339fd653..cd46ef4f 100644 --- a/dashboard/js/modules/piston.module.js +++ b/dashboard/js/modules/piston.module.js @@ -205,7 +205,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 + '?access_token=' + si.accessToken; try { var showOptions = $scope.piston ? !!$scope.showOptions : false; if (!response || !response.data || !response.data.piston) { @@ -5220,4 +5220,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; From 504d40c6f2fab615047963321273cb0ff0cb9d51 Mon Sep 17 00:00:00 2001 From: jp0550 Date: Fri, 4 May 2018 16:52:13 -0500 Subject: [PATCH 16/55] Disable registration and fuel stream testing --- smartapps/ady624/webcore-piston.src/webcore-piston.groovy | 6 +++--- smartapps/ady624/webcore.src/webcore.groovy | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/smartapps/ady624/webcore-piston.src/webcore-piston.groovy b/smartapps/ady624/webcore-piston.src/webcore-piston.groovy index 4d23db5d..6d7b946e 100644 --- a/smartapps/ady624/webcore-piston.src/webcore-piston.groovy +++ b/smartapps/ady624/webcore-piston.src/webcore-piston.groovy @@ -3347,9 +3347,9 @@ private long vcmd_writeToFuelStream(rtData, device, params) { asynchttp_v1.put(null, requestParams) } else { - httpPut(requestParams) { + //httpPut(requestParams) { - } + //} } return 0 } @@ -7968,4 +7968,4 @@ private void setRandomValue(name, value) { private void resetRandomValues() { state.temp = state.temp ?: [:] state.temp.randoms = [:] -} \ No newline at end of file +} diff --git a/smartapps/ady624/webcore.src/webcore.groovy b/smartapps/ady624/webcore.src/webcore.groovy index 9cd9d39c..5f2f2a88 100644 --- a/smartapps/ady624/webcore.src/webcore.groovy +++ b/smartapps/ady624/webcore.src/webcore.groovy @@ -1999,8 +1999,8 @@ private registerInstance() { asynchttp_v1.put(instanceRegistrationHandler, params) } else { - params << [contentType: 'text/plain'] - httpPut(params) { res -> } + //params << [contentType: 'text/plain'] + //httpPut(params) { res -> } } } @@ -3253,4 +3253,4 @@ public List getColors(){ [name:"Yellow", rgb:"#FFFF00", h:60, s:100, l:50], [name:"Yellow Green", rgb:"#9ACD32", h:80, s:61, l:50] ] -} \ No newline at end of file +} From d13aa07dc1888a0a5db97cd46a6502095e8f3b28 Mon Sep 17 00:00:00 2001 From: jp0550 Date: Fri, 4 May 2018 22:10:15 -0500 Subject: [PATCH 17/55] Fix dashboard js error --- dashboard/js/modules/dashboard.module.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dashboard/js/modules/dashboard.module.js b/dashboard/js/modules/dashboard.module.js index e2d1a4af..62aa7589 100644 --- a/dashboard/js/modules/dashboard.module.js +++ b/dashboard/js/modules/dashboard.module.js @@ -34,7 +34,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:' + '?access_token=' + si.accessToken; + $scope.endpoint=data.endpoint + 'execute/:pistonId:' + '?access_token=' + data.accessToken; $scope.rawEndpoint=data.endpoint; $scope.rawAccessToken=data.accessToken; if (data.error) { From 680d659533411576edee2b9331570e24db3de725 Mon Sep 17 00:00:00 2001 From: jp0550 Date: Mon, 7 May 2018 17:18:59 -0500 Subject: [PATCH 18/55] Add back missing line to fix save state error --- smartapps/ady624/webcore.src/webcore.groovy | 1 + 1 file changed, 1 insertion(+) diff --git a/smartapps/ady624/webcore.src/webcore.groovy b/smartapps/ady624/webcore.src/webcore.groovy index 5f2f2a88..3303c913 100644 --- a/smartapps/ady624/webcore.src/webcore.groovy +++ b/smartapps/ady624/webcore.src/webcore.groovy @@ -1068,6 +1068,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() result = [status: "ST_SUCCESS", id: hashId(piston.id)] } else { result = api_get_error_result("ERR_INVALID_TOKEN") From 575b7f48c167858cc517c2899dc3f9309c5f77e0 Mon Sep 17 00:00:00 2001 From: jp0550 Date: Wed, 23 May 2018 12:39:00 -0500 Subject: [PATCH 19/55] Fix UTC offset issues in relation to restrictions and variables Also, disable piston execution logging ( slowing down events page ) and change local requests to use http methods instead of sendHubCommand for faster processing. --- .../webcore-piston.src/webcore-piston.groovy | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/smartapps/ady624/webcore-piston.src/webcore-piston.groovy b/smartapps/ady624/webcore-piston.src/webcore-piston.groovy index 6d7b946e..9c31dd71 100644 --- a/smartapps/ady624/webcore-piston.src/webcore-piston.groovy +++ b/smartapps/ady624/webcore-piston.src/webcore-piston.groovy @@ -887,12 +887,12 @@ def handleEvents(event) { if (rtData.currentEvent) { try { def desc = 'webCore piston \'' + app.label + '\' was executed' - sendLocationEvent(name: 'webCoRE', value: 'pistonExecuted', isStateChange: true, displayed: false, linkText: desc, descriptionText: desc, data: [ + /*sendLocationEvent(name: 'webCoRE', value: 'pistonExecuted', isStateChange: true, displayed: false, linkText: desc, descriptionText: desc, data: [ id: hashId(app.id), name: app.label, event: [date: rtData.currentEvent.date, delay: rtData.currentEvent.delay, duration: now() - rtData.currentEvent.date, device: "$rtData.event.device", name: rtData.currentEvent.name, value: rtData.currentEvent.value, physical: rtData.currentEvent.physical, index: rtData.currentEvent.index], state: [old: rtData.state.old, new: rtData.state.new] - ]) + ]) */ } catch (all) { } } @@ -1807,9 +1807,9 @@ private scheduleTimer(rtData, timer, long lastRun = 0) { //switch to local date/times - time = utcToLocalTime(time) - long rightNow = utcToLocalTime(now()) - lastRun = lastRun ? utcToLocalTime(lastRun) : rightNow + time = hubUID ? time : utcToLocalTime(time) + long rightNow = hubUID ? now() : utcToLocalTime(now()) + lastRun = lastRun ? (hubUID ? lastRun : utcToLocalTime(lastRun)) : rightNow long nextSchedule = lastRun if (lastRun > rightNow) { @@ -1925,7 +1925,7 @@ private scheduleTimer(rtData, timer, long lastRun = 0) { if (nextSchedule > lastRun) { //convert back to UTC - nextSchedule = localToUtcTime(nextSchedule) + nextSchedule = hubUID ? nextSchedule : localToUtcTime(nextSchedule) rtData.schedules.removeAll{ it.s == timer.$ } requestWakeUp(rtData, timer, [$: -1], nextSchedule) } @@ -3236,7 +3236,7 @@ private long vcmd_httpRequest(rtData, device, params) { data[variable] = getVariable(rtData, variable).v } } - if (internal) { + if (internal && !hubUID) { 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) @@ -3265,6 +3265,7 @@ private long vcmd_httpRequest(rtData, device, params) { requestContentType: (method != "GET") && (contentType == "JSON") ? "application/json" : "application/x-www-form-urlencoded", body: method != "GET" ? data : null ] + def func = "" switch(method) { case "GET": @@ -3960,7 +3961,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, hubUID ? now() : utcToLocalTime(), 5, 1) == 0 if (rtData.logging > 2) debug "Time restriction check ${pass ? 'passed' : 'failed'}", rtData if (!pass) res = false; } @@ -7402,7 +7403,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 + ( (!hubUID && location.timeZone) ? location.timeZone.getOffset(dateOrTimeOrString) : 0)) } return null } @@ -7968,4 +7970,4 @@ private void setRandomValue(name, value) { private void resetRandomValues() { state.temp = state.temp ?: [:] state.temp.randoms = [:] -} +} \ No newline at end of file From 5f1fb869fe9bd95b08be83d37afe455fb9c1ab21 Mon Sep 17 00:00:00 2001 From: jp0550 Date: Tue, 29 May 2018 14:08:40 -0500 Subject: [PATCH 20/55] Fix ifttt --- dashboard/js/modules/piston.module.js | 2 +- .../ady624/webcore-piston.src/webcore-piston.groovy | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/dashboard/js/modules/piston.module.js b/dashboard/js/modules/piston.module.js index cd46ef4f..9f3aed0d 100644 --- a/dashboard/js/modules/piston.module.js +++ b/dashboard/js/modules/piston.module.js @@ -787,7 +787,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 + '?access_token=' + si.accessToken; } $scope.toggleAdvancedOptions = function() { diff --git a/smartapps/ady624/webcore-piston.src/webcore-piston.groovy b/smartapps/ady624/webcore-piston.src/webcore-piston.groovy index 9c31dd71..d5d551dd 100644 --- a/smartapps/ady624/webcore-piston.src/webcore-piston.groovy +++ b/smartapps/ady624/webcore-piston.src/webcore-piston.groovy @@ -4425,7 +4425,7 @@ private void subscribeAll(rtData) { break case 'email': subscriptionId = "$deviceId${operand.v}${hashId(app.id)}" - attribute = "email.${hashId(app.id)}" + attribute = "email" break case 'ifttt': case 'askAlexa': @@ -4434,14 +4434,14 @@ 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}" + attribute = "${operand.v}" switch (operand.v) { case 'askAlexa': - attribute = "askAlexaMacro.${item}" + attribute = "askAlexaMacro" break; case 'echoSistant': - attribute = "echoSistantProfile.${item}" + attribute = "echoSistantProfile" break; } } From 99f8338c8cf41b00e412a8eaa80a34a00dc19f30 Mon Sep 17 00:00:00 2001 From: jp0550 Date: Tue, 26 Jun 2018 15:19:51 -0500 Subject: [PATCH 21/55] Performance improvements and timeout fixes for cloud --- .../webcore-piston.src/webcore-piston.groovy | 83 +++++++++++++++---- smartapps/ady624/webcore.src/webcore.groovy | 18 +++- 2 files changed, 86 insertions(+), 15 deletions(-) diff --git a/smartapps/ady624/webcore-piston.src/webcore-piston.groovy b/smartapps/ady624/webcore-piston.src/webcore-piston.groovy index d5d551dd..7044d464 100644 --- a/smartapps/ady624/webcore-piston.src/webcore-piston.groovy +++ b/smartapps/ady624/webcore-piston.src/webcore-piston.groovy @@ -685,6 +685,17 @@ private getTemporaryRunTimeData() { ] } +//atomic state performance is much worse in hubitat than in smartthings. Grab a cached version where possible +private getCachedAtomicState(){ + def atomStart = now() + + atomicState.loadState() + def atomState = atomicState.@backingMap + debug "Atomic state generated in ${now() - atomStart}ms", rtData + + return atomState +} + private getRunTimeData(rtData = null, semaphore = null, fetchWrappers = false) { def n = now() try { @@ -704,7 +715,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 = getCachedAtomicState() + + rtData.cache = atomState.cache ?: [:] rtData.newCache = [:] rtData.schedules = [] rtData.cancelations = [statements:[], conditions:[], all: false] @@ -716,21 +729,22 @@ private getRunTimeData(rtData = null, semaphore = null, fetchWrappers = false) { 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 + 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 } @@ -801,7 +815,7 @@ def handleEvents(event) { return; } checkVersion(rtData) - runIn(30.toInteger(), timeRecoveryHandler) + runIn(45.toInteger(), timeRecoveryHandler) if (rtData.semaphoreDelay) { warn "Piston waited at a semaphore for ${rtData.semaphoreDelay}ms", rtData } @@ -1128,7 +1142,7 @@ private processSchedules(rtData, scheduleJob = false) { 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.toInteger(), timeHandler, [data: next]) - runIn((t+30).toInteger(), timeRecoveryHandler, [data: next]) + runIn((t+45).toInteger(), timeRecoveryHandler, [data: next]) } else { rtData.stats.nextSchedule = 0 //remove the recovery @@ -1186,10 +1200,11 @@ 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 + //if (rtData.logging > 2) debug "Execute Statement ${statement.$}", rtData if (!rtData.fastForwardTo) { switch (statement.tep) { case 'c': @@ -3638,8 +3653,12 @@ private evaluateOperand(rtData, node, operand, index = null, trigger = false, ne break; case 'd': //devices def deviceIds = [] + //def systemDeviceIds = getAllDeviceIds() for (d in expandDeviceList(rtData, operand.d)) { - if (getDevice(rtData, d)) deviceIds.push(d) + //if (getDevice(rtData, d)) deviceIds.push(d) + //if(systemDeviceIds.any { (d == hashId(it.id)) || (d == it.label) }) { + deviceIds.push(d) + //} } /* for (d in rtData, operand.d) { @@ -4697,13 +4716,21 @@ private sanitizeVariableName(name) { name = name ? "$name".trim().replace(" ", "_") : null } +/* 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 start = now() + //rtData.allDevices = getAllDeviceIds().collect{ getDeviceById(it.id) }.flatten().collectEntries{ dev -> [(hashId(dev.id)): dev]} + //log.debug getAllDeviceIds() + rtData.allDevices = parent.listAvailableDevices(true) + if (rtData.logging > 2) debug "Grabbed parent devices in ${now() - start}ms", rtData + } + if (rtData.allDevices) { - def deviceMap = rtData.allDevices.find{ (idOrName == it.key) || (idOrName == it.value.getDisplayName()) } + def deviceMap = rtData.allDevices.find{ (idOrName == it.key) || (idOrName == it.value.getDisplayName()) } if (deviceMap) { rtData.updateDevices = true rtData.devices[deviceMap.key] = deviceMap.value @@ -4714,6 +4741,34 @@ private getDevice(rtData, idOrName) { } } return device +}*/ + +private getDevice(rtData, idOrName) { + def start = now() + 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 = [:] + } + + def deviceMap = rtData.allDevices.find{ (idOrName == it.key) || (idOrName == it.value.getDisplayName()) } + if(!deviceMap){ + def minDev = getAllDeviceIds().find { (idOrName == hashId(it.id)) || (idOrName == it.label) } + if(minDev){ + rtData.allDevices[hashId(minDev.id)] = getDeviceById(minDev.id) + deviceMap = rtData.allDevices.find { it.key == hashId(minDev.id) } + } + } + + if (deviceMap) { + rtData.updateDevices = true + rtData.devices[deviceMap.key] = deviceMap.value + device = deviceMap.value + } + } + //if (rtData.logging > 2) debug "Device grabbed in ${now() - start}ms", rtData + return device } private getDeviceAttributeValue(rtData, device, attributeName) { @@ -7786,9 +7841,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(']')) { diff --git a/smartapps/ady624/webcore.src/webcore.groovy b/smartapps/ady624/webcore.src/webcore.groovy index 3303c913..ab250c04 100644 --- a/smartapps/ady624/webcore.src/webcore.groovy +++ b/smartapps/ady624/webcore.src/webcore.groovy @@ -1112,9 +1112,25 @@ private api_intf_dashboard_piston_get() { } else { result = api_get_error_result("ERR_INVALID_TOKEN") } + //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) + + //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})" } From 693ff3f79190078d2a13b7ee55361cadd3bfe6aa Mon Sep 17 00:00:00 2001 From: jp0550 Date: Thu, 19 Jul 2018 14:24:03 -0500 Subject: [PATCH 22/55] Dashboard async, Custom Endpoint dynamic UI, Data saver remote only --- .../webcore-dashboard.groovy | 5 +-- .../webcore-piston.src/webcore-piston.groovy | 2 +- smartapps/ady624/webcore.src/webcore.groovy | 35 ++++++++++--------- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/smartapps/ady624/webcore-dashboard.src/webcore-dashboard.groovy b/smartapps/ady624/webcore-dashboard.src/webcore-dashboard.groovy index 47cb6354..c71fdb62 100644 --- a/smartapps/ady624/webcore-dashboard.src/webcore-dashboard.groovy +++ b/smartapps/ady624/webcore-dashboard.src/webcore-dashboard.groovy @@ -156,10 +156,7 @@ private void broadcastEvent(deviceId, eventName, eventValue, eventTime) { body: [d: deviceId, n: eventName, v: eventValue, t: eventTime] ] - httpPut(params){ - resp ->resp.data - log.info("broadcastEvent response :: ${resp.data}") - } + asynchttpPut((String)null, params) /* asynchttp_v1.put(null, [ diff --git a/smartapps/ady624/webcore-piston.src/webcore-piston.groovy b/smartapps/ady624/webcore-piston.src/webcore-piston.groovy index 7044d464..c1064b79 100644 --- a/smartapps/ady624/webcore-piston.src/webcore-piston.groovy +++ b/smartapps/ady624/webcore-piston.src/webcore-piston.groovy @@ -691,7 +691,7 @@ private getCachedAtomicState(){ atomicState.loadState() def atomState = atomicState.@backingMap - debug "Atomic state generated in ${now() - atomStart}ms", rtData + //debug "Atomic state generated in ${now() - atomStart}ms", rtData return atomState } diff --git a/smartapps/ady624/webcore.src/webcore.groovy b/smartapps/ady624/webcore.src/webcore.groovy index ab250c04..0832b0dd 100644 --- a/smartapps/ady624/webcore.src/webcore.groovy +++ b/smartapps/ady624/webcore.src/webcore.groovy @@ -386,11 +386,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", title: "Use custom endpoints?", default: false, required: true - input "customHubUrl", "string", title: "Custom hub url different from https://cloud.hubitat.com", default: null, required: false - input "customWebcoreInstanceUrl", "string", title: "Custom webcore instance url different from dashboard.webcore.co", default: null, required: false - paragraph "If you enter a custom url above you will have to use a different webcore instance from dashboard.webcore.co as they restrict their api to hubitat and smartthing's cloud" + input "customEndpoints", "bool", submitOnChange: true, title: "Use custom endpoints?", default: false, required: true + if(customEndpoints){ + input "customHubUrl", "string", title: "Custom hub url different from https://cloud.hubitat.com", default: null, required: false + input "customWebcoreInstanceUrl", "string", title: "Custom webcore instance url different from dashboard.webcore.co", default: null, required: false + paragraph "If you enter a custom url above you will have to use a different webcore instance from dashboard.webcore.co as they restrict their api to hubitat and smartthing's cloud" + } } } @@ -1112,22 +1114,23 @@ private api_intf_dashboard_piston_get() { } else { result = api_get_error_result("ERR_INVALID_TOKEN") } - //for accuracy, use the time as close as possible to the render result.now = now() def jsonData = groovy.json.JsonOutput.toJson(result) - //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) - } + if(!customEndpoints || (customHubUrl ?: "") == ""){ + //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})" From 663b81f168ea55a50e2844f4913fea489394ab8e Mon Sep 17 00:00:00 2001 From: jp0550 Date: Thu, 19 Jul 2018 14:37:28 -0500 Subject: [PATCH 23/55] Fix npe on routine phrases --- smartapps/ady624/webcore.src/webcore.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/smartapps/ady624/webcore.src/webcore.groovy b/smartapps/ady624/webcore.src/webcore.groovy index a85caffa..c7b59bbd 100644 --- a/smartapps/ady624/webcore.src/webcore.groovy +++ b/smartapps/ady624/webcore.src/webcore.groovy @@ -3091,7 +3091,7 @@ private static Map getAlarmSystemStatusOptions() { private Map getRoutineOptions(updateCache = false) { - def routines = location.helloHome?.getPhrases().sort{ it?.label ?: '' } + def routines = location.helloHome?.getPhrases()?.sort{ it?.label ?: '' } def result = [:] for(routine in routines) { if (routine && routine?.label) From d8cfdb6e05eb4e40c032e858780d55377e0ed0c0 Mon Sep 17 00:00:00 2001 From: jp0550 Date: Thu, 2 Aug 2018 11:25:15 -0500 Subject: [PATCH 24/55] Hsm Changes --- .../webcore-piston.src/webcore-piston.groovy | 30 ++++++++++--- smartapps/ady624/webcore.src/webcore.groovy | 43 +++++++++++++++---- 2 files changed, 57 insertions(+), 16 deletions(-) diff --git a/smartapps/ady624/webcore-piston.src/webcore-piston.groovy b/smartapps/ady624/webcore-piston.src/webcore-piston.groovy index 5dd84c7d..98d50073 100644 --- a/smartapps/ady624/webcore-piston.src/webcore-piston.groovy +++ b/smartapps/ady624/webcore-piston.src/webcore-piston.groovy @@ -690,11 +690,15 @@ private getTemporaryRunTimeData() { private getCachedAtomicState(){ def atomStart = now() - atomicState.loadState() - def atomState = atomicState.@backingMap + try{ + atomicState.loadState() + def atomState = atomicState.@backingMap + return atomState + } + catch(e){ + return atomicState + } //debug "Atomic state generated in ${now() - atomStart}ms", rtData - - return atomState } private getRunTimeData(rtData = null, semaphore = null, fetchWrappers = false) { @@ -943,6 +947,7 @@ private Boolean executeEvent(rtData, event) { device: srcEvent ? srcEvent.device : hashId((event.device?:location).id), 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 @@ -963,6 +968,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) @@ -972,6 +978,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) @@ -2378,7 +2385,7 @@ 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] } if (status && status.size()) { - sendLocationEvent(name: 'hsmStatus', value: status[0].id) + sendLocationEvent(name: 'hsmSetArm', value: status[0].id) } else { error "Error setting SmartThings Home Monitor status. Status '$statusIdOrName' does not exist.", rtData } @@ -3696,6 +3703,9 @@ 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 'powerSource': values = [[i: "${node?.$}:v", v:[t: 'enum', v:rtData.powerSource]]]; break; @@ -4437,6 +4447,10 @@ private void subscribeAll(rtData) { case 'alarmSystemStatus': subscriptionId = "$deviceId${operand.v}" attribute = "hsmStatus" + break; + case 'alarmSystemAlert': + subscriptionId = "$deviceId${operand.v}" + attribute = "hsmAlert" break; case 'time': case 'date': @@ -4818,7 +4832,7 @@ private Map getDeviceAttribute(rtData, deviceId, attributeName, subDeviceIndex = def mode = location.getCurrentMode(); return [t: 'string', v: hashId(mode.getId()), n: mode.getName()] case 'alarmSystemStatus': - def v = rtData.hsmStatus + def v = location.hsmStatus ?: rtData.hsmStatus def n = rtData.virtualDevices['alarmSystemStatus']?.o[v] return [t: 'string', v: v, n: n] } @@ -7916,6 +7930,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], @@ -8041,7 +8056,8 @@ private getSystemVariableValue(rtData, name) { 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 "\$hsmStatus": switch (rtData.hsmStatus) { case 'disarmed': return 'Disarmed'; case 'armedHome': return 'Armed/Home'; case 'armedAway': return 'Armed/Away'; }; return null; + //case "\$hsmStatus": switch (location.hsmStatus ?: rtData.hsmStatus) { case 'allDisarmed' : return 'All Disarmed'; case 'disarmed': return 'Disarmed'; case 'armedHome': return 'Armed/Home'; case 'armedAway': return 'Armed/Away'; }; return null; + case "\$hsmStatus": return location.hsmStatus ?: rtData.hsmStatus } } diff --git a/smartapps/ady624/webcore.src/webcore.groovy b/smartapps/ady624/webcore.src/webcore.groovy index c7b59bbd..50a05a4a 100644 --- a/smartapps/ady624/webcore.src/webcore.groovy +++ b/smartapps/ady624/webcore.src/webcore.groovy @@ -949,6 +949,15 @@ private api_get_error_result(error) { ] } +private getFirmwareVersion(){ + 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 @@ -974,12 +983,12 @@ private api_get_base_result(deviceVersion = 0, updateCache = false) { ] + (sendDevices ? [contacts: listAvailableContacts(false, updateCache), 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' ]}, + hubs: location.getHubs().collect{ [id: hashId(it.id, updateCache), name: it.name, firmware: getFirmwareVersion()[it.id], 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), mode: hashId(location.getCurrentMode().id, updateCache), modes: location.getModes().collect{ [id: hashId(it.id, updateCache), name: it.name ]}, - shm: transformHsmStatus(atomicState["hsmStatus"]), + shm: transformHsmStatus(state.hsmStatus), name: location.name, temperatureScale: location.getTemperatureScale(), timeZone: tz ? [ @@ -996,6 +1005,7 @@ private api_get_base_result(deviceVersion = 0, updateCache = false) { private String transformHsmStatus(status){ switch(status){ case "disarmed": + case "allDisarmed": return "off" break; case "armedHome": @@ -2127,7 +2137,7 @@ public Map getRunTimeData(semaphore = null, fetchWrappers = false) { globalStore: state.store ?: [:], settings: state.settings ?: [:], lifx: state.lifx ?: [:], - hsmStatus: atomicState.hsmStatus, + hsmStatus: state.hsmStatus, powerSource: state.powerSource ?: 'mains', region: state.endpoint.contains('graph-eu') ? 'eu' : 'us', instanceId: hashId(app.id), @@ -2322,7 +2332,7 @@ def NewIncidentHandler(evt) { } def hsmHandler(evt){ - atomicState["hsmStatus"] = evt.value + state.hsmStatus = evt.value } def lifxHandler(response, cbkData) { @@ -3082,10 +3092,24 @@ private Map getLocationModeOptions(updateCache = false) { return result } private static Map getAlarmSystemStatusOptions() { - return [ - disarmed: "Disarmed", - armedHome: "Armed/Home", - armedAway: "Armed/Away" + 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 getAlarmSystemAlertOptions() { + return [ + intrusion: "Intrusion", + smoke: "Smoke", + water: "Water", + rule: "Rule" ] } @@ -3121,7 +3145,8 @@ 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: 'Home Security Monitor status', t: 'enum', o: getAlarmSystemStatusOptions(), x: true], + alarmSystemStatus: [ n: 'Hubitat Safety Monitor status', t: 'enum', o: getAlarmSystemStatusOptions(), x: true], + alarmSystemAlert: [ n: 'Hubitat Safety Monitor alert',t: 'enum', o: getAlarmSystemAlertOptions(), m: true] ] } public Map getColorByName(name){ From e17d20a8611ff3e7e159db0e7305be97ee2d1306 Mon Sep 17 00:00:00 2001 From: jp0550 Date: Sun, 5 Aug 2018 17:31:41 -0500 Subject: [PATCH 25/55] Change version and add fallback for installed method --- dashboard/js/app.js | 2 +- dist/dashboard/js/webCoRE.min.js | 2 +- .../webcore-piston.src/webcore-piston.groovy | 14 +++++++++----- smartapps/ady624/webcore.src/webcore.groovy | 3 ++- 4 files changed, 13 insertions(+), 8 deletions(-) diff --git a/dashboard/js/app.js b/dashboard/js/app.js index 01e19e99..c7fd0b5f 100644 --- a/dashboard/js/app.js +++ b/dashboard/js/app.js @@ -2048,4 +2048,4 @@ if (!String.prototype.endsWith) { }; } -version = function() { return 'v0.3.105.20180628'; }; +version = function() { return 'v0.3.106.20180731'; }; diff --git a/dist/dashboard/js/webCoRE.min.js b/dist/dashboard/js/webCoRE.min.js index 9f9d5a95..435f1c54 100644 --- a/dist/dashboard/js/webCoRE.min.js +++ b/dist/dashboard/js/webCoRE.min.js @@ -9003,4 +9003,4 @@ return angular.module("ngMap",[]),function(){"use strict";var e,t=function(t,n,o * Copyright (c) 2015-2017, Jon Schlinkert. * Released under the MIT License. */ -function dashify(b,a){if(typeof b!=="string"){throw new TypeError("expected a string")}return b.trim().replace(/([a-z])([A-Z])/g,"$1-$2").replace(/\W/g,function(c){return/[À-ž]/.test(c)?c:"-"}).replace(/^-+|-+$/g,"").replace(/-{2,}/g,function(c){return a&&a.condense?"-":c}).toLowerCase()};var app=angular.module("webCoRE",["ng","ngRoute","ngSanitize","ngResource","ngDialog","ngAnimate","angular-svg-round-progressbar","angular-bootstrap-select","swipe","dndLists","ui.toggle","chart.js","smartArea","ui.bootstrap.contextMenu","ngFitText","googlechart","ngMap"]);var cdn="";var theme="";app.directive("head",["$rootScope","$compile",function(a,b){return{restrict:"E",link:function(e,f){var c='';f.append(b(c)(e));e.routeStyles={};a.$on("$routeChangeStart",function(k,g,h){if(h&&h.$$route&&h.$$route.css){if(!angular.isArray(h.$$route.css)){h.$$route.css=[h.$$route.css]}angular.forEach(h.$$route.css,function(l){delete e.routeStyles[l]})}if(g&&g.$$route&&g.$$route.css){if(!angular.isArray(g.$$route.css)){g.$$route.css=[g.$$route.css]}angular.forEach(g.$$route.css,function(l){e.routeStyles[l]=l})}})}}}]);app.directive("ngWheel",["$parse",function(a){return function(f,c,b){var e=a(b.ngWheel);c.bind("wheel",function(g){f.$apply(function(){e(f,{$event:g})})})}}]);app.directive("refresh",["$interval",function(e){var c=0;var a=null;var b=null;return{restrict:"A",link:function(g,h,f){h.on("$destroy",function(){if(b!=null){e.cancel(b)}});if(angular.isDefined(f.refresh)&&!isNaN(parseInt(f.refresh))){c=f.refresh}if(angular.isDefined(f.onRefresh)&&angular.isFunction(g[f.onRefresh])){a=g[f.onRefresh];b=e(function(){a(h[0])},c*1000);f.$observe("refresh",function(k){if(!angular.equals(k,c)){if(b!=null){e.cancel(b)}c=k;if(c>0){b=e(function(){a(h[0])},c*1000)}}})}}}}]);app.directive("textcomplete",["Textcomplete",function(a){return{restrict:"EA",scope:{members:"=",message:"=",callback:"&"},template:'',link:function(f,g,e){var b=f.members;var c=g.find("textarea");var h=new a(c,[{match:/(\b)(\w{2,})$/,search:function(k,l){l($.map(b,function(m){return m.toLowerCase().indexOf(k.toLowerCase())===0?m:null}))},index:2,replace:function(k){return"$1"+k+" "}}]);if(f.callback){f.$watch("message",function(l,k){f.callback()})}$(h).on({"textComplete:select":function(l,k){f.$apply(function(){f.message=k})},"textComplete:show":function(k){$(this).data("autocompleting",true)},"textComplete:hide":function(k){$(this).data("autocompleting",false)}})}}}]);app.directive("masonry",["$parse",function(a){return{restrict:"AC",link:function(g,h,e){g.items=[];var b=h[0];var c=angular.extend({itemSelector:"tile"},JSON.parse(e.masonry));var f=g.masonry=new Masonry(b,c);var k=0;g.update=function(){if(k){window.clearTimeout(k)}k=window.setTimeout(function(){k=0;f.reloadItems();f.layout();h.children(c.itemSelector).css("visibility","visible")},120)};g.update()}}}]).directive("masonryTile",function(){return{restrict:"AC",link:function(a,c){c.css("visibility","hidden");var b=c.parent("*[masonry]:first").scope(),e=b.update;imagesLoaded(c.get(0),e);c.ready(e)}}});app.directive("tileHeight",function(){var a={restrict:"A",link:function(g,h,b,c,e){var f=1;if(b.tileHeight){f=b.tileHeight}var k=function(){var l=h[0].parentElement.offsetWidth/Math.round(h[0].parentElement.offsetWidth/h[0].offsetWidth)*f;h.outerHeight(l)};g.$watch(b.tileHeight,function(l){f=1*l;k()});$(window).resize(k);k();g.$on("$destroy",function(){$(window).unbind("resize",k)})}};return a});app.directive("tileMeta",["$parse","$sce",function(b,a){var c={restrict:"A",scope:false,link:function(f,g,e){function h(){var l=b(e.tileMeta)(f);var k=b(e.tileIndex)(f)+1;var m=renderString(a,l["t"+k]).meta;if(!m||!m.type){m=renderString(a,l["f"+k]).meta}if(!m||!m.type){m=renderString(a,l["i"+k]).meta}f.$parent.meta=m}f.$watchCollection(e.tileMeta,h);f.$watch(e.tileIndex,h)}};return c}]);app.directive("help",["$compile",function(a){var b={restrict:"A",link:function(g,e,c){var h=c.help?c.help:e.text();var f=angular.element("');e.append(a(f)(g))}};return b}]);app.directive("script",function(){return{restrict:"E",scope:false,link:function(b,e,a){if(a.type==="text/javascript"){var c=e.text();var g=new Function(c);g()}}}});app.directive("devData",function(a){return function(e,c,b){var f=function(k,h,g){var l=a(g.devData)(k);if(l){for(attr in l){h.attr("data-"+attr,l[attr])}}};e.$watch(b.devData,function(){f(e,c,b)})}});app.directive("onSizeChanged",["$window",function(a){return{restrict:"A",scope:{onSizeChanged:"&"},link:function(f,c,b){var e=c[0];g(f,e);a.addEventListener("resize",h);function g(l,k){l.cachedElementWidth=k.offsetWidth;l.cachedElementHeight=k.offsetHeight}function h(){var k=f.cachedElementWidth!=e.offsetWidth||f.cachedElementHeight!=e.offsetHeight;if(k){var l=f.onSizeChanged();l()}}}}}]);app.directive("title",function(){return{restrict:"A",link:function(c,b,a){if(!mobileCheck()){$(b).hover(function(){$(b).tooltip({container:"body",html:true,placement:"bottom"});$(b).tooltip("show")},function(){$(b).tooltip("hide")});$(b).on("$destroy",function(){$(b).tooltip("hide")})}}}});app.directive("collapseControl",["dataService",function(a){return function(f,e,b){var g=(b.target||b.ariaControls||"").replace("#","");var c=a.isCollapsed(g);if(c&&"ariaExpanded" in b){e.attr("aria-expanded","false")}e.bind("click",function(h){var k=a.isCollapsed(g);a.setCollapsed(g,!k)})}}]);app.directive("collapseTarget",["dataService",function(a){return function(e,c,b){var h=b.id||"";var f=b.collapseClass||"in";var g=a.isCollapsed(h);if(g){c.removeClass(f)}else{c.addClass(f)}}}]);app.directive("taskedit",function(){return{restrict:"A",scope:false,link:function(b,c,a){var e=[];function f(k){if(e.length>0){for(var h=0;hf[e]?1:-1)});if(c){b.reverse()}return b}});app.filter("dashify",function(){return function(a){return dashify(a,{condense:true})}});app.filter("uniqueDashify",function(){var a={};return function(e,c){e=dashify(e,{condense:true});var f=e;var b=1;while(f in a&&a[f]!==c){f=e+"-"+b++}a[f]=c;return f}});var config=app.config(["$routeProvider","$locationProvider","$sceDelegateProvider","$rootScopeProvider","$animateProvider",function(c,a,g,f,b){f.digestTtl(10000);var e=".module.css";g.resourceUrlWhitelist(["self",cdn+"**"]);b.classNameFilter(/^(?:(?!no-ng-animate).)*$/);c.when("/",{templateUrl:cdn+theme+"html/modules/dashboard.module.html?v="+version(),controller:"dashboard",css:cdn+theme+"css/modules/dashboard"+e+"?v="+version()}).when("/register",{templateUrl:cdn+theme+"html/modules/register.module.html?v="+version(),controller:"register",css:cdn+theme+"css/modules/register"+e+"?v="+version()}).when("/init/:init",{redirectTo:function(h){app.initialInstanceUri=atou(h.init);return"/"}}).when("/piston/:pistonId",{templateUrl:cdn+theme+"html/modules/piston.module.html?v="+version(),controller:"piston",css:cdn+theme+"css/modules/piston"+e+"?v="+version(),reloadOnSearch:false}).when("/fuel",{templateUrl:cdn+theme+"html/modules/fuel.module.html?v="+version(),controller:"fuel",css:cdn+theme+"css/modules/fuel"+e+"?v="+version()}).when("/visors",{templateUrl:cdn+theme+"html/modules/visors.module.html?v="+version(),controller:"visors",css:cdn+theme+"css/modules/visors"+e+"?v="+version()}).when("/init/:instId1/:instId2",{redirectTo:function(h){app.initialInstanceUri=atou(h.instId1+"/"+h.instId2);return"/"}}).otherwise({redirectTo:"/"});a.html5Mode(true)}]);config.factory("dataService",["$http","$location","$rootScope","$window","$q",function(r,B,p,I,b){var J={};var o="";var q=null;var s={};var G=null;var w={};var a={};var l="N7zqL6a8Texs4wY5y&y2YPLzus+_dZ%s";var K=l;var g=null;var O=null;var N=null;var y=false;var A={};var C=1;var m=false;if(localforage){localforage.config({name:"webCoRE"});localforage.keys().then(function(Q){C=Q.length;if(C){localforage.iterate(function(T,S,R){A[S]=n(T);C--;if(!C&&!m){c()}})}else{c()}})}var h=function(Q){return JSON.parse(Q)};var P=function(Q,S,R){return Array(S-String(Q).length+1).join(R||"0")+Q};var D=function(Q){return P(Q.getFullYear(),4)+"-"+P(1+Q.getMonth(),2)+"-"+P(Q.getDate(),2)+" "+P(Q.getHours(),2)+":"+P(Q.getMinutes(),2)+":"+P(Q.getSeconds(),2)};var e=function(Q){return JSON.stringify(Q)};var F=function(S,Q){try{return utoa(I.sjcl.encrypt(Q?Q:K,angular.toJson(S),{ks:256}))}catch(R){return null}};J.encryptBackup=function(R,Q){return F(R,K+(Q?Q:""))};var n=function(R,Q){try{return angular.fromJson(I.sjcl.decrypt(Q?Q:K,atou(R)))}catch(S){return null}};var M=function(Q,S,R){localforage.setItem("core:"+Q,F(S,R));A["core:"+Q]=S;return};var E=function(Q,R){return A["core:"+Q]};var u=function(Q){q=Q;s[q.id]=q;M("locations",s);return q};var t=function(Q){if(!Q||!Q.uri){return null}if(Q.uri.indexOf("?access_token=")){var R=Q.uri.split("?access_token=");Q.uri=R[0];Q.accessToken=R[1]}return Q};var L=function(S){var R=(!G);if(!G||(G.id!=S.id)){G=S}var Q=a[G.id];if(!Q){Q={}}Q.token=S.token?S.token:Q.token;Q.uri=S.uri?S.uri.replace(":443",""):Q.uri;a[G.id]=t(Q);delete (G.token);delete (G.uri);if(S.contacts){G.contacts=S.contacts}G.contacts=G.contacts?G.contacts:(w[G.id]&&w[G.id].contacts?w[G.id].contacts:[]);if(S.devices){G.devices=S.devices;R=true}G.devices=G.devices?G.devices:(w[G.id]&&w[G.id].devices?w[G.id].devices:[]);if(!!G.pistons){for(i=0;iG.coreVersion){z("A newer SmartApp version ("+version()+") is available, please update and publish all the webCoRE SmartApps in the SmartThings IDE.",true)}else{z("A newer UI version ("+G.coreVersion+") is available, please hard reload this web page to get the newest version.",true)}}return G};var z=function(Q,R){if(g){g(Q,R)}};var v=function(Q){if(!Q){return""}return Q.replace(/([\uD83C-\uDBFF][\uDC00-\uDFFF])/g,function(R){return":"+encodeURIComponent(R)+":"})};var k=function(Q){if(!Q){return""}return Q.replace(/(\:%[0-9A-F]{2}%[0-9A-F]{2}%[0-9A-F]{2}%[0-9A-F]{2}\:)/g,function(R){return decodeURIComponent(R.substr(1,12))})};var f=function(Q){return(Q&&Q.accessToken?"access_token="+Q.accessToken+"&":"")};J.openWebSocket=function(T){if(T&&G){N=T;if(O){return O}var S=G.id;var Q=a[G.id];if(!Q){Q={}}var R=(Q&&Q.uri&&Q.uri.startsWith("https://graph-eu"))?"eu":"us";O=new WebSocket("wss://api-"+R+"-"+S[32]+".webcore.co:9297");O.onopen=function(U){O.send(G.id)};O.onclose=function(U){O=null;if(N){setTimeout(function(){J.openWebSocket(N)},5000)}};O.onmessage=function(U){if(N){try{N(U)}catch(V){}}};O.onerror=function(U){O=null;if(N){setTimeout(function(){J.openWebSocket(N)},5000)}};return O}else{N=null;O.close();O=null}};J.closeWebSocket=function(){J.openWebSocket(null)};J.ready=function(){return !!m};J.logout=function(){s={};w={};A={};return localforage.clear()};J.setStatusCallback=function(Q){g=Q};J.saveToStore=function(Q,R){return M(Q,R)};J.loadFromStore=function(Q){return E(Q)};J.deleteFromStore=function(Q){return localforage.removeItem("core:"+Q)};J.loadFromStore=function(Q){return E(Q)};J.deleteInstance=function(Q){if(Q){if(Q==G){G=null;M("instance",null,l)}delete (a[Q.id]);delete (w[Q.id]);M("instances",w);M("store",a)}};J.listLocations=function(){var Q=[];for(lid in s){Q.push(JSON.parse(JSON.stringify(s[lid])))}return Q};J.getLocation=function(Q){if(Q){for(lid in s){if(lid==Q){return JSON.parse(JSON.stringify(s[lid]))}}}else{return JSON.parse(JSON.stringify(q))}return null};J.listInstances=function(R){var Q=[];for(iid in w){if(!R||(w[iid].locationId==R)){Q.push(JSON.parse(JSON.stringify(w[iid])))}}return Q};J.getInstanceCount=function(R){var Q=0;for(iid in w){if(!R||(w[iid].locationId==R)){Q++}}return Q};J.getInstance=function(R,Q){if(G&&!R){return G}if(G&&(G.id==R)){return G}if(R){for(iid in w){if(iid==R){return JSON.parse(JSON.stringify(w[iid]))}}}else{try{return JSON.parse(JSON.stringify(G?G:(w?w[E("instance")]:null)))}catch(S){}}if(!!Q&&!!w){for(iid in w){return JSON.parse(JSON.stringify(w[iid]))}}return null};J.getPistonInstance=function(Q){for(iid in w){for(i in w[iid].pistons){if(w[iid].pistons[i].id==Q){return JSON.parse(JSON.stringify(w[iid]))}}}return null};J.loadInstance=function(U,R,Q,Y){var T=U?a[U.id]:null;var X=!U||!(U.devices instanceof Object)||!(Object.keys(U.devices).length)?0:(U.deviceVersion?U.deviceVersion:0);if(!T||!T.token){if((app.initialInstanceUri&&app.initialInstanceUri.length)||(R&&R.length)){R=app.initialInstanceUri?app.initialInstanceUri:R;if(!R.startsWith("https://")){if(R&&(R.indexOf("tat.comapi")>0)){var S=R.split("api");if(S[1].length>=33){var V=S[1].substr(0,32);var Z=S[1].substr(32);R="https://"+S[0]+"/api/"+V.substr(0,8)+"-"+V.substr(8,4)+"-"+V.substr(12,4)+"-"+V.substr(16,4)+"-"+V.substr(20,12)+"/apps/"+Z}}else{if(R&&!(R instanceof Object)&&(R.length>=69)){var ab=R.substr(0,R.length-64);if(!ab.endsWith(".com")){ab+=".api.smartthings.com"}R=R.substr(0,8)=="https://"?R:"https://"+ab+"/api/token/"+R.substr(-64,8)+"-"+R.substr(-56,4)+"-"+R.substr(-52,4)+"-"+R.substr(-48,4)+"-"+R.substr(-44,12)+"/smartapps/installations/"+R.substr(-32,8)+"-"+R.substr(-24,4)+"-"+R.substr(-20,4)+"-"+R.substr(-16,4)+"-"+R.substr(-12)+"/"}}}T=t({uri:R});for(id in a){if(a[id].uri==R){T=t(a[id]);if(w&&w[id]&&w[id].devices instanceof Object&&Object.keys(w[id].devices).length&&w[id].deviceVersion){X=w[id].deviceVersion}break}}}}delete (app.initialInstanceUri);if(!T){var aa=E("instance");if(aa){T=a[aa];if(w&&w[aa]&&w[aa].devices instanceof Object&&Object.keys(w[aa].devices).length&&w[aa].deviceVersion){X=w[aa].deviceVersion}}}if(!T){B.path("/register")}else{var W=document.getElementById("error");if(W){W.parentNode.removeChild(W)}}return r.jsonp((T?T.uri:"about:blank/")+"intf/dashboard/load?"+f(T)+"token="+(T&&T.token?T.token:"")+(Q?"&pin="+Q:"")+"&dashboard="+(Y?1:0)+"&dev="+X,{jsonpCallbackParam:"callback"}).then(function(ac){var ad=ac.data;if(ad.now){adjustTimeOffset(ad.now)}if(ad.error&&T){ad.uri=T.uri;ad.accessToken=T.accessToken}if(ad.location){u(ad.location)}if(ad.instance){ad.instance=L(ad.instance)}ad.endpoint=T.uri;ad.accessToken=T.accessToken;return ad},function(ac){z("There was a problem loading the dashboard data. The data shown below may be outdated; please log out if this problem persists.");return ac})};J.tap=function(Q){return r({method:"GET",url:"tap/"+Q})};J.getApiUri=function(){var Q=J.getInstance();si=a?a[Q.id]:null;return si?si.uri:null};J.refreshDashboard=function(){var R=J.getInstance();si=a&&R?a[R.id]:null;var Q=!R||!(R.devices instanceof Object)||!(Object.keys(R.devices).length)?0:(R.deviceVersion?R.deviceVersion:0);z("Loading dashboard...");return r.jsonp((si?si.uri:"about:blank/")+"intf/dashboard/refresh?"+f(si)+"token="+(si&&si.token?si.token:""),{jsonpCallbackParam:"callback"}).then(function(S){data=S.data;return data},function(S){return null})};J.getPiston=function(T){var S=J.getPistonInstance(T);if(!S){S=J.getInstance()}si=a&&S?a[S.id]:null;var R=!S||!(S.devices instanceof Object)||!(Object.keys(S.devices).length)?0:(S.deviceVersion?S.deviceVersion:0);var Q=E("db.version",l);z("Loading piston...");return r.jsonp((si?si.uri:"about:blank/")+"intf/dashboard/piston/get?"+f(si)+"id="+T+"&db="+Q+"&token="+(si&&si.token?si.token:"")+"&dev="+R,{jsonpCallbackParam:"callback"}).then(function(U){data=U.data;if(data.now){adjustTimeOffset(data.now)}if(data.dbVersion){M("db.version",data.dbVersion,l);M("db",data.db);z("Database updated to version "+data.dbVersion)}else{data.db=E("db");z()}if(data.location){u(data.location)}if(data.instance){data.instance=L(data.instance)}data.endpoint=si.uri;return data},function(U){return null})};J.backupPistons=function(R,Q){var S=J.getInstance(R);if(!S){S=J.getInstance()}si=a&&S?a[S.id]:null;return r.jsonp((si?si.uri:"about:blank/")+"intf/dashboard/piston/backup?"+f(si)+"ids="+Q+"&token="+(si&&si.token?si.token:""),{jsonpCallbackParam:"callback"}).then(function(T){data=T.data;if(data.now){adjustTimeOffset(data.now)}return data},function(T){return null})};J.getActivity=function(S,Q){var R=J.getPistonInstance(S);if(!R){R=J.getInstance()}si=a?a[R.id]:null;return r.jsonp((si?si.uri:"about:blank/")+"intf/dashboard/piston/activity?"+f(si)+"id="+S+"&log="+(Q?Q:0)+"&token="+(si&&si.token?si.token:""),{jsonpCallbackParam:"callback"}).then(function(T){return T.data})};J.generateBackupBin=function(R,S){var Q=J.getInstance();return r({method:"POST",url:"https://api.webcore.co/bins/"+(S?"":md5(Q.account.id)),data:R?(S?{d:F(R,l)}:{e:F(R,l+Q.account.id)}):{},transformResponse:function(T){try{T=JSON.parse(T);if(T&&T.bin){return T.bin}if(T&&T.uri){T=T.uri.split("/");if(T&&T.length){return T[T.length-1]}}}catch(U){}return null}})};J.saveToBin=function(S,R){z("Saving piston to backup bin...");var Q=J.getInstance();if(Q&&Q.account&&Q.account.id){R={e:F(R,l+Q.account.id)}}else{R={};S=null}return r({method:"PUT",url:"https://api.webcore.co/bins/"+md5(Q.account.id)+"/"+S,data:R,transformResponse:function(T){z("Backup bin updated");return true}})};J.loadFromBin=function(S,Q){z("Loading piston from backup bin...");var R=J.getInstance();if(!(R&&R.account&&R.account.id)){S=null}return r({method:"GET",url:"https://api.webcore.co/bins/"+md5(R.account.id)+"/"+S,transformResponse:function(T){if(S){try{T=JSON.parse(T);if(T&&T.e){return n(T.e,l+R.account.id)}if(T&&T.d){return n(T.d,l)}z()}catch(U){z("Sorry, an error occurred while importing the backup bin")}}return null}})};J.generateNewPistonName=function(){var Q=J.getInstance();si=a?a[Q.id]:null;return r.jsonp((si?si.uri:"about:blank/")+"intf/dashboard/piston/new?"+f(si)+"token="+(si&&si.token?si.token:""),{jsonpCallbackParam:"callback"}).then(function(R){return R.data})};J.createPiston=function(Q,R,T){var S=J.getInstance();si=a?a[S.id]:null;return r.jsonp((si?si.uri:"about:blank/")+"intf/dashboard/piston/create?"+f(si)+"author="+encodeURIComponent(R)+"&name="+encodeURIComponent(Q)+"&bin="+encodeURIComponent(T?T:"")+"&token="+(si&&si.token?si.token:""),{jsonpCallbackParam:"callback"}).then(function(U){return U.data})};var H=function(R,T,Q,S){if(QQ){var V=[].concat.apply([],S.split("").map(function(X,Y){return Y%Q?[]:S.slice(Y,Y+Q)},S));z("Preparing to save chunked piston...");return r.jsonp((si?si.uri:"about:blank/")+"intf/dashboard/piston/set.start?"+f(si)+"id="+U.id+"&chunks="+V.length.toString()+"&token="+(si&&si.token?si.token:""),{jsonpCallbackParam:"callback"}).then(function(X){if(X&&(X.status==200)&&X.data&&(X.data.status=="ST_READY")){return H(si,V,0,W)}})}else{z("Saving piston...");return r.jsonp((si?si.uri:"about:blank/")+"intf/dashboard/piston/set?"+f(si)+"id="+U.id+"&data="+encodeURIComponent(S)+"&bin="+encodeURIComponent(W)+"&token="+(si&&si.token?si.token:""),{jsonpCallbackParam:"callback"}).then(function(X){z();return X})}};J.setPistonBin=function(Q,R){var S=J.getPistonInstance(Q);if(!S){S=J.getInstance()}si=a?a[S.id]:null;z("Setting piston bin to "+R+"...");return r.jsonp((si?si.uri:"about:blank/")+"intf/dashboard/piston/set.bin?"+f(si)+"id="+Q+"&bin="+R+"&token="+(si&&si.token?si.token:""),{jsonpCallbackParam:"callback"}).then(function(T){z();return T.data})};J.clickPistonTile=function(Q,R){var S=J.getPistonInstance(Q);if(!S){S=J.getInstance()}si=a?a[S.id]:null;return r.jsonp((si?si.uri:"about:blank/")+"intf/dashboard/piston/tile?"+f(si)+"id="+Q+"&tile="+R+"&token="+(si&&si.token?si.token:""),{jsonpCallbackParam:"callback"}).then(function(T){z();return T.data})};J.setPistonCategory=function(Q,R){var S=J.getPistonInstance(Q);if(!S){S=J.getInstance()}si=a?a[S.id]:null;z("Setting piston category...");return r.jsonp((si?si.uri:"about:blank/")+"intf/dashboard/piston/set.category?"+f(si)+"id="+Q+"&category="+R+"&token="+(si&&si.token?si.token:""),{jsonpCallbackParam:"callback"}).then(function(T){z();return T.data})};J.setPistonLogging=function(Q,S){var R=J.getPistonInstance(Q);if(!R){R=J.getInstance()}si=a?a[R.id]:null;z("Setting piston logging level to "+S+"...");return r.jsonp((si?si.uri:"about:blank/")+"intf/dashboard/piston/logging?"+f(si)+"id="+Q+"&level="+S+"&token="+(si&&si.token?si.token:""),{jsonpCallbackParam:"callback"}).then(function(T){z();return T.data})};J.clearPistonLogs=function(Q){var R=J.getPistonInstance(Q);if(!R){R=J.getInstance()}si=a?a[R.id]:null;z("Clearing piston logs...");return r.jsonp((si?si.uri:"about:blank/")+"intf/dashboard/piston/clear.logs?"+f(si)+"id="+Q+"&token="+(si&&si.token?si.token:""),{jsonpCallbackParam:"callback"}).then(function(S){z();return S.data})};J.pausePiston=function(Q){var R=J.getPistonInstance(Q);if(!R){R=J.getInstance()}si=a?a[R.id]:null;z("Pausing piston...");return r.jsonp((si?si.uri:"about:blank/")+"intf/dashboard/piston/pause?"+f(si)+"id="+Q+"&token="+(si&&si.token?si.token:""),{jsonpCallbackParam:"callback"}).then(function(S){z();return S.data})};J.resumePiston=function(Q){var R=J.getPistonInstance(Q);if(!R){R=J.getInstance()}si=a?a[R.id]:null;z("Resuming piston...");return r.jsonp((si?si.uri:"about:blank/")+"intf/dashboard/piston/resume?"+f(si)+"id="+Q+"&token="+(si&&si.token?si.token:""),{jsonpCallbackParam:"callback"}).then(function(S){z();return S.data})};J.testPiston=function(Q){var R=J.getPistonInstance(Q);if(!R){R=J.getInstance()}si=a?a[R.id]:null;z("Testing piston...");return r.jsonp((si?si.uri:"about:blank/")+"intf/dashboard/piston/test?"+f(si)+"id="+Q+"&token="+(si&&si.token?si.token:""),{jsonpCallbackParam:"callback"}).then(function(S){z();return S.data})};J.createPresenceSensor=function(R,Q){var S=J.getPistonInstance();if(!S){S=J.getInstance()}si=a?a[S.id]:null;return r.jsonp((si?si.uri:"about:blank/")+"intf/dashboard/presence/create?"+f(si)+"name="+encodeURIComponent(R)+"&dni="+encodeURIComponent(Q?Q:"")+"&token="+(si&&si.token?si.token:""),{jsonpCallbackParam:"callback"}).then(function(T){return T.data})};J.deletePiston=function(Q){var R=J.getPistonInstance(Q);if(!R){R=J.getInstance()}si=a?a[R.id]:null;return r.jsonp((si?si.uri:"about:blank/")+"intf/dashboard/piston/delete?"+f(si)+"id="+Q+"&token="+(si&&si.token?si.token:""),{jsonpCallbackParam:"callback"}).then(function(S){return S.data})};J.setVariable=function(R,U,Q){var T=Q?J.getPistonInstance(Q):J.getInstance();si=a?a[T.id]:null;if(U&&U.t){switch(U.t){case"time":var V=new Date(U.v);U.v=V.getTime()-V.getTimezoneOffset()*60000;break;case"date":case"datetime":U.v=(new Date(U.v)).getTime();break}}var S=U?utoa(angular.toJson(U)):"";return r.jsonp((si?si.uri:"about:blank/")+"intf/dashboard/variable/set?"+f(si)+"name="+R+"&value="+encodeURIComponent(S)+(Q?"&id="+Q:"")+"&token="+(si&&si.token?si.token:""),{jsonpCallbackParam:"callback"}).then(function(W){return W.data})};J.setSettings=function(Q){var S=J.getInstance();si=a?a[S.id]:null;var R=Q?utoa(angular.toJson(Q)):"";return r.jsonp((si?si.uri:"about:blank/")+"intf/dashboard/settings/set?"+f(si)+"settings="+encodeURIComponent(R)+"&token="+(si&&si.token?si.token:""),{jsonpCallbackParam:"callback"}).then(function(T){return T.data})};J.evaluateExpression=function(R,U,Q){var T=J.getPistonInstance(R);if(!T){T=J.getInstance()}si=a?a[T.id]:null;var S=utoa(angular.toJson(U));return r.jsonp((si?si.uri:"about:blank/")+"intf/dashboard/piston/evaluate?"+f(si)+"id="+R+"&expression="+encodeURIComponent(S)+"&dataType="+(Q?encodeURIComponent(Q):"")+"&token="+(si&&si.token?si.token:""),{jsonpCallbackParam:"callback"}).then(function(V){return V.data})};J.registerDashboard=function(Q){return r.post("https://api.webcore.co/dashboard/register/"+Q).then(function(R){return R.data})};J.listFuelStreams=function(){var Q=J.getInstance();if(Q){var U=Q.id;var R=a[Q.id];if(!R){R={}}var T=(R&&R.uri&&R.uri.startsWith("https://graph-eu"))?"eu":"us";var S={method:"POST",url:"https://api-"+T+"-"+U[32]+".webcore.co:9287/fuelStreams/list",headers:{"Auth-Token":"|"+U},data:{i:U}};return r(S).then(function(V){return V.data})}};J.login=function(W,R){var T=G||J.getInstance(null,true);if(!T){T={id:"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"+[0,1,2,3,4,5,6,7,8,9,"a","b","c","d","e","f"][Math.floor(Math.random()*16)]}}if(T){var V=T.id;var Q=a[T.id];if(!Q){Q={}}var U=(Q&&Q.uri&&Q.uri.startsWith("https://graph-eu"))?"eu":"us";var S={method:"POST",url:"https://api-"+U+"-"+V[32]+".webcore.co:9287/user/login",headers:{"Auth-Token":"|"+V},data:{u:W,p:R}};return r(S).then(function(X){var Y=X.data;a=JSON.parse("{}");for(x in a){if(!w[x]||!w[x].account){w[x]={id:x,name:"Unknown",locationId:"?"};s["?"]={id:"?",name:"Unknown"}}}G=J.getInstance(null,true);M("store",a);M("instances",w);M("locations",s);if(G){M("instance",G.id)}B.path("/");if(Y&&Y.result){a=Y.store;return true}return false})}};J.listFuelStreamData=function(R){var Q=J.getInstance();if(Q){var V=Q.id;var S=a[Q.id];if(!S){S={}}var U=(S&&S.uri&&S.uri.startsWith("https://graph-eu"))?"eu":"us";var T={method:"POST",url:"https://api-"+U+"-"+V[32]+".webcore.co:9287/fuelStreams/get",headers:{"Auth-Token":"|"+V},data:{i:V,f:R}};return r(T).then(function(W){return W.data})}};J.registerHandler=function(){navigator.registerProtocolHandler("web+core","https://"+window.location.hostname+"/handler/%s","webCoRE")};J.determineDeviceType=function(Q){if(Q&&Q.cn){if(Q.cn.indexOf("Water Sensor")>=0){return"waterSensor"}if(Q.cn.indexOf("Contact Sensor")>=0){return"contactSensor"}if(Q.cn.indexOf("Thermostat")>=0){return"thermostat"}if(Q.cn.indexOf("Garage Door Control")>=0){return"garageDoor"}if(Q.cn.indexOf("Music Player")>=0){return"musicPlayer"}if(Q.cn.indexOf("Door Control")>=0){return"door"}if(Q.cn.indexOf("Presence Sensor")>=0){return"presenceSensor"}if(Q.cn.indexOf("Motion Sensor")>=0){return"motionSensor"}if(Q.cn.indexOf("Color Control")>=0){return"rgbBulb"}if(Q.cn.indexOf("Color Temperature")>=0){return"whiteBulb"}if(Q.cn.indexOf("Switch Level")>=0){var R=Q.n.toLowerCase();if(R.indexOf("light")>=0){return"whiteBulb"}if(R.indexOf("keen")>=0){return"vent"}if(R.indexOf("vent")>=0){return"vent"}return"dimmer"}if(Q.cn.indexOf("Lock")>=0){return"lock"}if((Q.cn.indexOf("Button")>=0)&&(Q.cn.indexOf("Button")>=0)){return"keypad"}if(Q.cn.indexOf("Button")>=0){return"button"}if(Q.cn.indexOf("Temperature Measurement")>0){return"temperatureSensor"}if((Q.cn.indexOf("Switch")>=0)&&(Q.cn.indexOf("Power Meter")>=0)){return"outlet"}if(Q.cn.indexOf("Switch")>=0){return"switch"}if(Q.cn.indexOf("Power Meter")>=0){return"powerMeter"}}return"unknownDevice"};J.getAllCollapsed=function(){return J.loadFromStore("collapsed")||[]};J.isCollapsed=function(Q){return J.getAllCollapsed().indexOf(Q)>=0};J.setCollapsed=function(T,R){var S=J.getAllCollapsed();var Q=S.indexOf(T);if(R&&Q<0){S.push(T)}else{if(!R&&Q>=0){S.splice(Q,1)}}J.saveToStore("collapsed",S)};var c=function(){a=E("store");if(!a){a={}}s=E("locations");if(!s){s={}}w=E("instances");if(!w){w={}}userId=0;m=true;window.ds=J;if(!!a.user){J.login(a.user.name,a.user.token).then(function(Q){console.log(Q)})}};return J}]);app.run(["$rootScope","$window","$location",function(a,b,c){a.getTime=function(e){if(e){return e.format("h:mmtt")}};a.$on("$viewContentLoaded",function(e){var f=c.path();if(!f.startsWith("/")){f="/"+f}if(f.startsWith("/init/")){f="/init"}if(f.startsWith("/piston/")){f="/piston"}b.ga("send","pageview",{page:f})});a.bytesToSize=function(e){var g=["bytes","kB","MB","GB","TB"];if(e==0){return"0 Byte"}var f=parseInt(Math.floor(Math.log(e)/Math.log(1024)));return(e/Math.pow(1024,f)).toFixed(f==0?0:2)+" "+g[f]}}]);Date.prototype.format=function(A,a){var u=["\x00","January","February","March","April","May","June","July","August","September","October","November","December"];var c=["\x01","Jan.","Feb.","Mar.","Apr.","May","June","July","Aug.","Sept.","Oct.","Nov.","Dec."];var b=["\x02","Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"];var g=["\x03","Sun","Mon","Tue","Wed","Thu","Fri","Sat"];function p(h,f){var m=h+"";f=f||2;while(m.length12?o-12:o==0?12:o;A=A.replace(/(^|[^\\])hh+/g,"$1"+p(B));A=A.replace(/(^|[^\\])h/g,"$1"+B);var v=a?this.getUTCMinutes():this.getMinutes();A=A.replace(/(^|[^\\])mm+/g,"$1"+p(v));A=A.replace(/(^|[^\\])m/g,"$1"+v);var r=a?this.getUTCSeconds():this.getSeconds();A=A.replace(/(^|[^\\])ss+/g,"$1"+p(r));A=A.replace(/(^|[^\\])s/g,"$1"+r);var C=a?this.getUTCMilliseconds():this.getMilliseconds();A=A.replace(/(^|[^\\])fff+/g,"$1"+p(C,3));C=Math.round(C/10);A=A.replace(/(^|[^\\])ff/g,"$1"+p(C));C=Math.round(C/10);A=A.replace(/(^|[^\\])f/g,"$1"+C);var e=o<12?"AM":"PM";A=A.replace(/(^|[^\\])TT+/g,"$1"+e);A=A.replace(/(^|[^\\])T/g,"$1"+e.charAt(0));var q=e.toLowerCase();A=A.replace(/(^|[^\\])tt+/g,"$1"+q);A=A.replace(/(^|[^\\])t/g,"$1"+q.charAt(0));var E=-this.getTimezoneOffset();var l=a||!E?"Z":E>0?"+":"-";if(!a){E=Math.abs(E);var F=Math.floor(E/60);var w=E%60;l+=p(F)+":"+p(w)}A=A.replace(/(^|[^\\])K/g,"$1"+l);var z=(a?this.getUTCDay():this.getDay())+1;A=A.replace(new RegExp(b[0],"g"),b[z]);A=A.replace(new RegExp(g[0],"g"),g[z]);A=A.replace(new RegExp(u[0],"g"),u[k]);A=A.replace(new RegExp(c[0],"g"),c[k]);A=A.replace(/\\(.)/g,"$1");return A};function formatTime(c){try{var a=(new Date(c)).getTime()+(window.timeOffset?window.timeOffset:0);var f=new Date(a);return f.format("h:mm TT")}catch(b){}}function currentTime(){return(new Date()).getTime()+(window.timeOffset?window.timeOffset:0)}function fixTime(b){if(b<86400000){var c=new Date();var a=c.getTime();b+=a-(a%86400000)+c.getTimezoneOffset()*60000}return b}function utcToString(a){return(new Date(fixTime(a))).toLocaleString()}function utcToTimeString(a){return(new Date(fixTime(a))).toLocaleTimeString()}function utcToDateString(a){return(new Date(fixTime(a))).toLocaleDateString()}function timeSince(f){if(!f){return"never"}switch(typeof f){case"number":break;case"string":f=+new Date(f);break;case"object":if(f.constructor===Date){f=f.getTime()}break;default:f=+new Date()}var e=[[60,"seconds",1],[120,"1 minute ago","1 minute from now"],[3600,"minutes",60],[7200,"1 hour ago","1 hour from now"],[86400,"hours",3600],[172800,"yesterday","tomorrow"],[604800,"days",86400],[1209600,"last week","next week"],[2419200,"weeks",604800],[4838400,"last month","next month"],[29030400,"months",2419200],[58060800,"last year","next year"],[2903040000,"years",29030400],[5806080000,"last century","next century"],[58060800000,"centuries",2903040000]];var h=(+new Date()+(window.timeOffset?window.timeOffset:0)-f)/1000,b="ago",g=1;if(h==0){return"Just now"}if(h<0){h=Math.abs(h);b="from now";g=2}var a=0,c;while(c=e[a++]){if(h-20){return"pending"}f=true;g=-g}var b="";if(g>86400){b=Math.floor(g/86400).toString()+"d ";g=g%86400}var e=Math.floor(g/3600);var a=Math.floor((g-e*3600)/60);var c=g%60;b+=(e>0?(e<10?"0":"")+e.toString()+":":"")+(a<10?"0":"")+a.toString()+":"+(c<10?"0":"")+c.toString();return b}function timeLeft(c,a){if(!c){return 0}c+=window.timeOffset?window.timeOffset:0;var b=Math.round((c-(new Date().getTime()))/1000);switch(a){case"h":return Math.floor(b/3600);break;case"m":return b>=3600?60:Math.floor(b/60);break;case"s":return b>=60?60:Math.floor(b%60);break}return b}function adjustTimeOffset(a){var b=a-(new Date()).getTime();if(isNaN(window.timeOffset)||(Math.abs(b)":u+=">";break;case"[":var k=f.indexOf("|",e);if(k>e){var r=f.substring(e+1,k);e=k+1;u+=g(r)}else{e++;u+=g()}break;case"]":if(l==undefined){return"["+u+"]"}var s=l.trim();while(/(\bsrc=\S+),/.test(s)){s=s.replace(/(\bsrc=\S+),/,"$1:webCoRE-comma:")}s=s.replace(/\s+/g,",").split(",");var o="";var m="";var n="";var t="";for(x in s){if(!s[x]){continue}switch(s[x]){case"b":case"u":case"i":case"s":case"pre":case"mono":case"blink":case"flash":case"left":case"center":case"condensed":case"right":case"full":o+="s-"+s[x]+" ";break;case"chart-gauge":h.type=s[x].replace("chart-","");break;case"img":case"image":h.type="image";break;case"vid":case"video":h.type="video";break;default:if(/^\d+(\.\d+)?(x|em)/.test(s[x])){t=s[x].replace("x","em")}else{if(s[x].startsWith("b-")){n=s[x].substr(2).replace(/[^#0-9a-z]/gi,"")}else{if(s[x].startsWith("bk-")||s[x].startsWith("bg-")){n=s[x].substr(3).replace(/[^#0-9a-z]/gi,"")}else{if(s[x].startsWith("back-")){n=s[x].substr(5).replace(/[^#0-9a-z]/gi,"")}else{if(s[x].indexOf("=")>0){var k=s[x].indexOf("=");h.options[s[x].substr(0,k)]=s[x].substr(k+1).replace(/:webCoRE-comma:/g,",")}else{m=s[x].replace(/[^#0-9a-z]/gi,"")}}}}}}}h.className=o;h.color=m;h.backColor=n;return""+u+"";default:u+=q}e++}return u};h.html=g(f).replace(/\:fa-([a-z0-9\-\s]*)\:/gi,function(k){return''}).replace(/\:fa5-([a-z0-9\-\s]*)\:/gi,function(k){return''}).replace(/\:fal-([a-z0-9\-\s]*)\:/gi,function(k){return''}).replace(/\:far-([a-z0-9\-\s]*)\:/gi,function(k){return''}).replace(/\:fas-([a-z0-9\-\s]*)\:/gi,function(k){return''}).replace(/\:fab-([a-z0-9\-\s]*)\:/gi,function(k){return''}).replace(/\:wu-([a-k]|v[1-4])-([a-z0-9_\-]+)\:/gi,function(l){var k=l[4];if(k=="v"){k+=l[5];var m=l.substr(7,l.length-8);return''}else{var m=l.substr(6,l.length-7);return''}}).replace(/(?![^<]*[>])#[a-z0-9]{6}/gi,function(k){return'    '+k}).replace(/\\[rn]/gi,"
");var c=document.createElement("DIV");c.innerHTML=h.html;h.text=c.textContent||c.innerText||"";var a=b.trustAsHtml(h.html);a.meta=h;return a}Object.defineProperty(Array.prototype,"unique",{enumerable:false,value:function(){if(!this){return[]}var e={},c=[];for(var f=0,b=this.length;fthis.length){a=this.length}return this.substring(a-b.length,a)===b}}version=function(){return"v0.3.105.20180628"};config.controller("dashboard",["$scope","$rootScope","dataService","$timeout","$interval","$location","$sce","$routeParams","ngDialog","$window",function(s,n,q,d,h,g,m,r,o,c){var b=null;var f=null;var j=null;s.initialized=false;s.loading=true;s.data=null;s.error="";s.designer={};s.locations=null;s.instances=null;s.requestId=0;s.dropDownMenu=false;s.endpoint="";s.rawEndpoint="";s.categories=[];s.pausedPistons=[];s.view="piston";s.isAppHosted=!!window.BridgeCommander;s.hostDeviceId="";s.sidebarCollapsed=q.isCollapsed("dashboardSidebar");s.completedInitialRender=false;s.init=function(w,z,y){if(s.$$destroyed){return}if(j){d.cancel(j)}j=null;s.requestId++;var A=0+s.requestId;s.loading=!s.initialized||!s.instance;q.setStatusCallback(s.setStatus);q.loadInstance(w,z,y,(s.view=="dashboard")).then(function(E){if(s.$$destroyed){return}if(A!=s.requestId){return}if(E){s.endpoint=E.endpoint+"execute/:pistonId:";s.rawEndpoint=E.endpoint;s.rawAccessToken=E.accessToken;if(E.error){switch(E.error){case"ERR_INVALID_TOKEN":s.dialogLogIn(E.name,E.uri,E.accessToken);break}}else{s.initialized=true;s.location=q.getLocation();s.instance=q.getInstance();s.currentInstanceId=s.instance.id;s.instanceCount=q.getInstanceCount();s.sidebarCollapsed=q.isCollapsed("dashboardSidebar");if(!s.devices){s.devices=s.listAvailableDevices()}if(!s.virtualDevices){s.virtualDevices=s.listAvailableVirtualDevices()}window.scope=s;window.dataService=q;s.loading=false;var C=s.getCategories();while(s.categories.length>C.length){s.categories.pop()}while(s.categories.length0){D=D.substr(0,C)+w+"="+(new Date()).getTime()}else{D+=(D.indexOf("?")>0?"&":"?")+w+"="+(new Date()).getTime()}var z=new Image();z.onload=function(){y.src=D};z.src=D;y.src=D};s.getGaugeChart=function(z,w,y){return{type:"Gauge",options:y.options,data:{cols:[{id:"gauge",label:y.text,type:"number"}],rows:[{c:[{v:s.renderString(z.meta.s["t"+(w+1)]).meta.text,f:z.meta.s["o"+(w+1)]?s.renderString(z.meta.s["o"+(w+1)]).meta.text:null}]}]}}};s.clock=function(){if(s.instance){for(pistonIndex in s.instance.pistons){var w=s.instance.pistons[pistonIndex];w.opacity=w.meta?s.getOpacity(w.meta.t):0}}};s.setStatus=function(w,y){if(y){s.permanentStatus=w;return}if(b){d.cancel(b)}b=null;s.status=w;if(s.status){b=d(function(){s.setStatus()},10000)}};s.clickPistonTile=function(w,z,y){if(w.originalEvent.ctrlKey||w.originalEvent.shiftKey){s.openPiston(z.id)}else{q.clickPistonTile(z.id,y).then(function(A){if(A&&(A.status=="ST_SUCCESS")&&!!(A["new"])&&!!z&&!!(z.meta)){z.meta.s=A}})}};s.copy=function(w){return angular.fromJson(angular.toJson(w))};s.getPlaces=function(){var w=(!!s.instance&&!!s.instance.settings&&(s.instance.settings.places instanceof Array))?s.copy(s.instance.settings.places):[];return w};s.getCategories=function(){var w=(!!s.instance&&!!s.instance.settings&&(s.instance.settings.categories instanceof Array))?s.copy(s.instance.settings.categories):[];if(!w.length){w=[{n:"Uncategorized",t:"d",i:0}]}return w};s.getCategory=function(y){y=parseInt(y);if(isNaN(y)){y=0}for(var w in s.categories){if(s.categories[w].i==y){return s.categories[w]}}for(var w in s.categories){if(s.categories[w].i==0){return s.categories[w]}}s.categories.push({n:"Uncategorized",t:"d",i:0});return s.categories[s.categories.length-1]};s.updateLocation=function(w){s.coords=w.coords};s.showSettings=function(){ga("send","event","settings","show");if(navigator.geolocation){navigator.geolocation.getCurrentPosition(s.updateLocation)}s.checkPresenceSensor();s.closeNavBar();s.settings=s.copy(s.instance.settings);s.settings.categories=s.getCategories();s.settings.places=s.getPlaces();s.view="settings"};s.addCategory=function(){var w=0;for(x in s.settings.categories){if(s.settings.categories[x].i>=w){w=s.settings.categories[x].i+1}}s.settings.categories.push({n:"New Category "+w,t:"d",i:w})};s.randomHash=function(w){var A="0123456789abcdef".split("");var z="";for(var y=0;yy.o){var w=y.i;y.i=y.o;y.o=w}if(y.i+100>=y.o){y.o=y.i+100}if(s.designer.$new){s.settings.places.push(y)}if(y.h){for(i in s.settings.places){s.settings.places[i].h=(s.settings.places[i]==y)}}s.closeDialog()};s.movePlace=function(z,A){var y=z?z.latLng:this.center;s.designer.position=[y.lat(),y.lng()];switch(A){case"i":s.designer.inner=this.radius;break;case"o":s.designer.outer=this.radius;break}if(s.designer.inner>s.designer.outer){var w=s.designer.inner;s.designer.inner=s.designer.outer;s.designer.outer=w}if(s.designer.inner<50){s.designer.inner=50}if(s.designer.inner+200>=s.designer.outer){s.designer.outer=s.designer.inner+200}};s.deletePlace=function(){for(var w=0;w=s.settings.categories)){return}var w=s.settings.categories[y];s.settings.categories[y]=s.settings.categories[y-1];s.settings.categories[y-1]=w};s.moveCategoryDown=function(y){if((y<0)||(y>=s.settings.categories-1)){return}var w=s.settings.categories[y];s.settings.categories[y]=s.settings.categories[y+1];s.settings.categories[y+1]=w};s.deleteCategory=function(w){s.settings.categories.splice(w,1)};s.hideSettings=function(){ga("send","event","settings","hide");s.view="piston"};s.messageHost=function(B,A,z){if(!window.BridgeCommander){return}var y=window.BridgeCommander.getPlatformName?window.BridgeCommander.getPlatformName():"unknown";switch(y){case"iOS":window.BridgeCommander.call(B,JSON.stringify(A)).then(function(C){if(z){z(C?JSON.parse(C):null)}});break;case"Android":if(window.BridgeCommander.hasOwnProperty(B)){var w=window.BridgeCommander[B](JSON.stringify(A));if(z){z(w?JSON.parse(w):null)}}break;default:window.BridgeCommander.subscribe(s.onAppRequest);window.BridgeCommander.call(B,JSON.stringify(A)).then(function(C){if(z){z(C)}});break}};s.checkPresenceSensor=function(){s.messageHost("getStatus",{i:s.instance.id},function(w){s.hostDeviceId=w instanceof Object?(w.dni?w.dni:""):"";s.presenceSensorId=w instanceof Object?!!w.s:!!w})};s.registerPresenceSensor=function(){s.designer.name="";window.designer=s.designer;s.designer.dialog=o.open({template:"dialog-register-presence-sensor",className:"ngdialog-theme-default ngdialog-large",closeByDocument:false,disableAnimation:true,scope:s})};s.doRegisterPresenceSensor=function(){var w=s.designer.name;s.closeDialog();if(!w){return}q.createPresenceSensor(w,s.hostDeviceId).then(function(z){var y=z.deviceId;if(y){s.messageHost("register",{e:s.rawEndpoint,a:s.rawAccessToken,i:s.instance.id,d:y},function(A){s.presenceSensorId=y})}})};s.unregisterPresenceSensor=function(){if(!s.presenceSensorId){return}q.destroyPresenceSensor(s.presenceSensorId).then(function(w){s.messageHost("unregister",{i:s.instance.id})})};s.updatePlaces=function(){s.messageHost("update",{i:s.instance.id,p:s.instance.settings.places})};s.saveSettings=function(){ga("send","event","settings","save");s.instance.settings=s.settings;q.setSettings(s.settings).then(function(w){s.instance.settings=s.settings;s.updatePlaces();s.hideSettings()})};s.showFuelStreams=function(){ga("send","event","fuel","show");s.initialized=false;s.loading=true;g.path("fuel")};s.showDashboard=function(){s.view="dashboard";ga("send","event","dashboard","show");q.openWebSocket(s.onWSUpdate);s.dropDownMenu=false;s.refreshing=true;q.refreshDashboard().then(function(z){for(deviceId in z){if(deviceId.startsWith(":")){var y=s.instance.devices[deviceId];if(y){var w=z[deviceId];for(attr in w){for(i in y.a){if(y.a[i].n==attr){y.a[i].v=w[attr];break}}}y.data=s.getDeviceData(y)}}}s.refreshing=false;s.setStatus()})};s.hideDashboard=function(){ga("send","event","dashboard","hide");q.closeWebSocket();s.view="piston";s.dropDownMenu=false};s.onWheel=function(w){s.dropDownMenu=w&&w.originalEvent&&(w.currentTarget.scrollTop==0)&&(w.originalEvent.deltaY<0);return true};s.onSwipe=function(w,y){s.dropDownMenu=(w.currentTarget.scrollTop==0)&&(y=="down");return true};s.range=function(w){return new Array(w)};s.listLocations=function(){return q.listLocations()};s.listInstances=function(w){return q.listInstances(w)};s.listAllInstances=function(){var y=[];var w=s.listLocations();for(l in w){var z=q.listInstances(w[l].id);for(i in z){y.push({id:z[i].id,name:w[l].name+" \\ "+z[i].name,pistons:z[i].pistons})}}return y};s.listAvailableDevices=function(){var w=[];for(deviceIndex in s.instance.devices){s.instance.devices[deviceIndex].id=deviceIndex;w.push(s.instance.devices[deviceIndex])}return w.sort(s.sortByName)};s.listAvailableVirtualDevices=function(){var w=[];for(deviceIndex in s.instance.virtualDevices){var y=s.instance.virtualDevices[deviceIndex];w.push(mergeObjects({id:deviceIndex},y))}return w.sort(s.sortByName)};s.sortByDisplay=function(y,w){return(y.d>w.d)?1:((w.d>y.d)?-1:0)};s.sortByName=function(y,w){return(y.n>w.n)?1:((w.n>y.n)?-1:0)};s.switchInstance=function(y){if(y!=s.instance.id){var w=q.getInstance(y);if(w){s.instance=null;if(j){d.cancel(j)}j=null;s.devices=null;s.init(w);s.closeNavBar()}}};s.$on("$destroy",function(){if(b){d.cancel(b)}if(f){h.cancel(f)}if(j){d.cancel(j)}});s.getDeviceData=function(w){var y={};for(a in w.a){y[w.a[a].n]=w.a[a].v}return y};s.getBatteryLevel=function(w){if(isNaN(w)){return 0}w=Math.floor(w/20);if(w<=0){return 0}if(w>=4){return 4}return w};s.renderString=function(w){return renderString(m,w)};s.onWSUpdate=function(w){if(w.isTrusted&&w.data){try{var z=JSON.parse(w.data);if(z.d&&z.n){var y=s.instance.devices[z.d];if(y){for(a in y.a){if(y.a[a].n==z.n){y.a[a].v=z.v;y.data=s.getDeviceData(y);break}}}}s.$apply()}catch(A){}}};s.getDeviceAttribute=function(z,y){for(a in z.a){var w=z.a[a];if(y==w.n){return w.v}}return""};s.openPiston=function(w){ga("send","event","piston","view",w);s.loading=true;s.initialized=false;g.path("piston/"+w)};s.newPiston=function(){s.loading=true;q.generateNewPistonName().then(function(w){s.loading=false;s.designer={};s.designer.author=q.loadFromStore("author.handle");s.designer.name=w.name;s.designer.page=0;s.designer.backup=!!q.loadFromStore("backup.auto");s.designer.disclaimer=!s.designer.backup;s.designer.items=[{type:"blank",name:"Create a blank piston",icon:"code",cssClass:"wide btn-default"},{type:"duplicate",name:"Create a duplicate piston",icon:"code",cssClass:"wide btn-info"},{type:"template",name:"Create a piston from a template",icon:"code",cssClass:"wide btn-success"},{type:"restore",name:"Restore a piston using a backup code",icon:"code",cssClass:"wide btn-warning"},{type:"import",name:"Import a piston from an external source",icon:"code",cssClass:"wide btn-danger"}];s.designer.dialog=o.open({template:"dialog-add-piston",className:"ngdialog-theme-default ngdialog-large",closeByDocument:false,disableAnimation:true,scope:s})})};s.backup=function(){s.designer={page:0,pistons:[]};s.designer.dialog=o.open({template:"dialog-backup-piston",className:"ngdialog-theme-default ngdialog-large",closeByDocument:false,disableAnimation:true,scope:s,onOpenCallback:function(){s.$$postDigest(function(){$("select").selectpicker("selectAll")})}})};s.backupPistons=function(){s.designer.progress=0;s.designer.page=1;s.designer.instances={};for(i in s.designer.pistons){var y=s.designer.pistons[i].substr(0,34);var w=s.designer.pistons[i].substr(34);s.designer.instances[y]=s.designer.instances[y]?s.designer.instances[y]:[];s.designer.instances[y].push({pid:w,requested:false})}s.designer.results=[];s.backupBatch()};s.backupBatch=function(){if(!s.designer||!s.designer.instances){return}var y="";var z=[];for(i in s.designer.instances){if(y!=""){break}var w=s.designer.instances[i];for(p in w){if(((y=="")||(y==i))&&(!w[p].requested)){y=i;w[p].requested=true;z.push(w[p].pid);if(z.length>=10){break}}}}if(z.length){q.backupPistons(y,z).then(function(A){if(A&&(A.pistons instanceof Array)){s.designer.results=s.designer.results.concat(A.pistons);s.designer.progress=s.designer.results.length}s.backupBatch()})}else{if(s.designer.pistons.length==s.designer.results.length){s.designer.page=2}else{s.designer.page=3}}};s.saveBackup=function(){var w=new Blob([q.encryptBackup(s.designer.results,s.designer.password)],{type:"text/plain"});var y=document.createElement("a");y.href=window.URL.createObjectURL(w);y.download="webCoRE."+(new Date()).toJSON()+".backup";y.click();s.closeDialog()};s.movePiston=function(){s.designer={pistons:[],instance:""};s.designer.dialog=o.open({template:"dialog-move-piston",className:"ngdialog-theme-default ngdialog-large",closeByDocument:false,disableAnimation:true,scope:s})};s.movePistons=function(){alert("Sorry, not ready yet")};s.createPiston=function(){var w=function(y){s.closeDialog();s.initialized=false;g.path("piston/"+y.id).search({description:s.designer.description,type:s.designer.type,piston:s.designer.piston,bin:s.designer.bin})};s.loading=true;q.saveToStore("backup.auto",!!s.designer.backup);q.saveToStore("author.handle",s.designer.author);if(s.designer.backup){q.generateBackupBin().then(function(y){var z=y.data;q.createPiston(s.designer.name,s.designer.author,z).then(w)})}else{q.createPiston(s.designer.name,s.designer.author).then(w)}};s.dialogLogIn=function(y,z,w){if(j){d.cancel(j)}j=null;s.loading=false;s.initialized=false;s.designer={};s.designer.sender=y;s.designer.uri=z;s.designer.accessToken=w;s.designer.dialog=o.open({template:"dialog-auth",className:"ngdialog-theme-default ngdialog-large",closeByDocument:false,disableAnimation:true,scope:s})};s.logOut=function(){q.logout().then(function(){s.loading=true;s.initialized=false;g.path("register")})};s.onAppRequest=function(w){s.setStatus(w)};s.initAds=function(){if(s.isAppHosted){return}window.adsbygoogle=(window.adsbygoogle||[]);window.adsbygoogle.push({google_ad_client:"ca-pub-4643048739403893",enable_page_level_ads:true})};s.authenticate=function(){s.closeDialog();s.init(null,s.designer.uri+(s.designer.accessToken?"?access_token="+s.designer.accessToken:""),window.md5("pin:"+s.designer.password));s.designer=null};s.dialogDeleteInstance=function(w){if(w){s.loading=false;s.initialized=false;s.designer={};s.designer.sender=w.locationName+" \\ "+w.name;s.designer.instance=w;s.designer.dialog=o.open({template:"dialog-del-instance",className:"ngdialog-theme-default ngdialog-large",closeByDocument:false,disableAnimation:true,scope:s})}};s.deleteInstance=function(){s.closeDialog();q.deleteInstance(s.designer.instance);s.designer=null;s.init()};s.setDesignerType=function(w){s.designer.type=w;s.nextPage()};s.closeDialog=function(){if(s.designer.dialog){s.designer.dialog.close();s.designer.dialog=null}};s.nextPage=function(){s.designer.page++};s.prevPage=function(){if(s.designer.page){s.designer.page--}};s.getOpacity=function(w){if(!w){return 0}w=currentTime()-w;if((w<0)||(w>60000)){return 0}return 1-w/60000};s.getLocationMode=function(){var y=s.location.mode;for(var w=0;w"+s.utcToString(w.date)+" - ";result+=w.message.replace(/\{\{(.*)\}\}/gi,function(y){return w.args[y.substr(2,y.length-4).trim()]});result+="";return m.trustAsHtml(result)};s.breakList=function(w){return w.replace(/,/g,"
")};var v=function(y){var z=y.getFullYear();var A=(1+y.getMonth()).toString();A=A.length>1?A:"0"+A;var w=y.getDate().toString();w=w.length>1?w:"0"+w;return A+"/"+w+"/"+z};s.getMonth=function(w){if(w){return["JAN","FEB","MAR","APR","MAY","JUN","JUL","AUG","SEP","OCT","NOV","DEC"][w.getMonth()]}};s.getDay=function(w){if(w){return("0"+w.getDate()).substr(-2)}};s.timeSince=timeSince;s.timeCounter=timeCounter;s.timeLeft=timeLeft;s.tap=function(w){q.tap(w).then(function(y){})};s.togglePiston=function(z,y){if((!z)&&(!s.viewerPiston||!s.viewerPiston.app)){return}var A=z?z.i:s.pistonId;if(A){d.cancel(tmrRefresh);var w=!(z?z.e:s.viewerPiston.app.enabled);if(z){z.e=w}else{s.viewerPiston.app.enabled=w}if(w){q.resumePiston(A).then(function(B){s.onRefresh(B)})}else{q.pausePiston(A).then(function(B){s.onRefresh(B)})}}if(y&&e.preventDefault){y.preventDefault()}if(y&&y.stopPropagation){y.stopPropagation()}};s.configurePiston=function(w){s.configuredPistonId=s.configuredPistonId==w.i?null:w.i};s.showPiston=function(w){document.body.scrollTop=0;s.viewerPiston=null;s.pistonId=w.i;s.refresh();window.onSwipeRight=s.hidePiston};s.hidePiston=function(){document.body.scrollTop=0;s.pistonId=null;window.onSwipeRight=null};s.prepareActions=function(z){var B=[];var w=[];var E=[];var F=(z.id<=0);var A=s.viewerPiston.tasks;var D=s.viewerPiston.app.actions;for(action in D){if(D[action].pid==z.id){if(D[action].t){var C=D[action].t;for(t in C){var y=0;for(task in A){if((A[task].type=="cmd")&&(A[task].ownerId==D[action].id)&&(A[task].taskId==C[t].i)){if((y==0)||(y>A[task].time)){y=A[task].time}}}C[t].time=y}}if(F){B.push(D[action])}else{if(D[action].rs==false){E.push(D[action])}else{w.push(D[action])}}}}var y=0;for(task in A){if((A[task].type=="evt")&&(A[task].ownerId==z.id)){if((y==0)||(y>A[task].time)){y=A[task].time}}}z.time=y;z.actions=B;z.trueActions=w;z.falseActions=E;z.$scope=s;if(z.children){for(child in z.children){s.prepareActions(z.children[child])}}};s.hadRecentActivity=function(w){return w&&w.le&&w.le.event&&w.le.event.date&&(timeLeft((new Date(w.le.event.date)).getTime())>-120)};s.toggleViewerOptions=function(){s.viewerPiston.showOptions=!s.viewerPiston.showOptions;s.closeNavBar()};s.getSecondaryStatementName=function(){var w=s.viewerPiston.app.mode;switch(w){case"Latching":return"BUT IF";case"Then-If":return"THEN IF";case"Else-If":return"ELSE IF";case"Or-If":return"OR IF";case"And-If":return"AND IF"}return"IF"};s.capturePiston=function(){var w=document.getElementById("viewerPanel");document.body.scrollTop=0;html2canvas(w).then(function(y){s.capturedImage=y.toDataURL("image/png");s.dialogCapture=o.open({template:"dialog-captured-image",className:"ngdialog-theme-default ngdialog-large",disableAnimation:true,scope:s,showClose:true})})};s.pausePiston=function(w){s.loading=true;q.pausePiston(w).then(function(y){s.init()})};s.resumePiston=function(w){s.loading=true;q.resumePiston(w).then(function(y){s.init()})};s.testPiston=function(w){q.testPiston(w)};s.determineDeviceType=function(w){return q.determineDeviceType(w)};s.initSocialMedia=function(){c.FB.XFBML.parse()};s.toggleSidebar=function(){s.sidebarCollapsed=!s.sidebarCollapsed};var u=navigator.userAgent||navigator.vendor||window.opera;if(u.match(/Android/i)){s.android=true}s.url=window.location.href;s.mobile=window.mobileCheck();s.tablet=(!s.mobile)&&(window.mobileOrTabletCheck());s.formatTime=formatTime;s.utcToString=utcToString;var k=setInterval(function(){if(q.ready()){clearInterval(k);s.init()}},1);if(navigator.geolocation){navigator.geolocation.getCurrentPosition(s.updateLocation)}}]);config.controller("register",["$scope","$rootScope","dataService","$timeout","$interval","$location","$sce","$routeParams","ngDialog","$window",function(k,g,i,c,e,d,f,j,h,b){var a=null;k.loading=false;k.code="";k.hasRegistered=i.listLocations().length>0;k.init=function(){};k.setStatus=function(m){if(a){c.cancel(a)}a=null;k.status=m;if(k.status){a=c(function(){k.setStatus()},10000)}};k.$on("$destroy",function(){if(a){c.cancel(a)}});k.register=function(){k.loading=true;i.registerDashboard(k.code).then(function(m){if(m&&(m.length>=80)&&(m.length<=180)){d.path("/init/"+m)}else{k.setStatus("Sorry, the registration code you provided did not work...")}k.loading=false})};k.cancel=function(){d.path("/")};k.init();var l=navigator.userAgent||navigator.vendor||window.opera;if(l.match(/Android/i)){k.android=true}k.url=window.location.href;k.mobile=window.mobileCheck();k.tablet=(!k.mobile)&&(window.mobileOrTabletCheck());k.formatTime=formatTime;k.utcToString=utcToString}]);config.controller("piston",["$scope","$rootScope","dataService","$timeout","$interval","$location","$sce","$routeParams","ngDialog","$window","$animate",function($scope,$rootScope,dataService,$timeout,$interval,$location,$sce,$routeParams,ngDialog,$window,$animate){var tmrReveal;var tmrStatus;var tmrActivity;var tmrClock;var statusAttribute="$status";$scope.lastLogEntry=0;$scope.error="";$scope.loading=true;$scope.initialized=false;$scope.mode="view";$scope.logging="0";$scope.data=null;$scope.error="";$scope.pistonId=$routeParams.pistonId;$scope.piston=null;$scope.designer={};$scope.showAdvancedOptions=false;$scope.dk="N7zqL6a8Texs4wY5y&y2YPLzus+_dZ%s";$scope.params=$location.search();$scope.insertIndexes={};$scope.warnings={};$scope.evalType="v";$scope.evalText="";$scope.evals=[];$scope.lastEval=0;$scope.category="0";$scope.categories=[];if($scope.params){$location.search({})}$scope.stack={undo:[],redo:[]};$scope.weekDays=["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"];$scope.yearMonths=["January","February","March","April","May","June","July","August","September","October","November","December"];$scope.render=function(cancelTimer){if(($scope.mode=="view")&&($scope.view.trace)){if(!tmrClock){tmrClock=$interval($scope.render,1000)}if($scope.trace){}}else{if(tmrClock){$timeout.cancel(tmrClock)}tmrClock=null}};$scope.setStatus=function(status){if(status){console.log(status)}if(tmrStatus){$timeout.cancel(tmrStatus)}tmrStatus=null;$scope.status=status;if($scope.status){tmrStatus=$timeout(function(){$scope.setStatus()},10000)}};$scope.version=function(){return $window.version()};$scope.encodeEmoji=function(value){if(!value){return""}return value.replace(/([\uD83C-\uDBFF][\uDC00-\uDFFF])/g,function(match){return encodeURIComponent(match)})};$scope.listAllPistons=function(){var result=[];var locations=dataService.listLocations();for(l in locations){var instances=dataService.listInstances(locations[l].id);for(i in instances){for(p in instances[i].pistons){result.push({v:instances[i].pistons[p].id,n:locations[l].name+" \\ "+instances[i].name+" \\ "+instances[i].pistons[p].name})}}}return result};$scope.listAvailableContacts=function(){var result=[];for(i in $scope.instance.contacts){var contact=$scope.instance.contacts[i];result.push({v:i,n:(contact.f+" "+contact.l).trim()+(contact.p?" (PUSH)":(contact.t?" ("+contact.t+")":"")),an:contact.an})}if(!result.length){result.push({v:"no one",n:"No available contacts"})}return result};$scope.getPistonName=function(pistonId){var locations=dataService.listLocations();for(l in locations){var instances=dataService.listInstances(locations[l].id);for(i in instances){for(p in instances[i].pistons){if(instances[i].pistons[p].id==pistonId){return locations[l].name+" \\ "+instances[i].name+" \\ "+instances[i].pistons[p].name}}}}return pistonId};$scope.getLifxSceneName=function(sceneId){if(!$scope.instance.lifx.scenes){return sceneId}var sceneName=$scope.instance.lifx.scenes[sceneId];if(!sceneName){return sceneId}return sceneName};$scope.getLifxSelectorName=function(selectorId){if(!$scope.instance.settings){return selectorId}var name=$scope.instance.lifx.lights?$scope.instance.lifx.lights[selectorId]:null;if(name){return name}name=$scope.instance.lifx.groups?$scope.instance.lifx.groups[selectorId]:null;if(name){return name}name=$scope.instance.lifx.locations?$scope.instance.lifx.locations[selectorId]:null;if(name){return name}name=$scope.instance.lifx.scenes?$scope.instance.lifx.scenes[selectorId]:null;if(name){return name}return selectorId};$scope.getModeName=function(modeId){for(modeIndex in $scope.location.modes){if($scope.location.modes[modeIndex].id==modeId){return $scope.location.modes[modeIndex].name}}return modeId};$scope.updateActivity=function(init){if($scope.$$destroyed){return}if($scope.mode!="view"){return}if(tmrActivity){$timeout.cancel(tmrActivity)}if(init){tmrActivity=$timeout($scope.updateActivity,10000);return}dataService.getActivity($scope.pistonId,$scope.lastLogEntry).then(function(response){if($scope.$$destroyed){return}if(response.error=="ERR_INVALID_ID"){$scope.home();return}if(response&&response.activity){if(response.activity.state){$scope.state=response.activity.state}if(response.activity.logs&&response.activity.logs.length){$scope.logs=response.activity.logs.concat($scope.logs)}if(response.activity.trace){$scope.trace=response.activity.trace}if(response.activity.localVars){$scope.localVars=response.activity.localVars}if(response.activity.memory){$scope.memory=response.activity.memory}if(response.activity.lastExecuted){$scope.lastExecuted=response.activity.lastExecuted}if(response.activity.nextSchedule){$scope.nextSchedule=response.activity.nextSchedule}if(response.activity.schedules){$scope.schedules=response.activity.schedules}if(response.activity.name){$scope.meta.name=response.activity.name}if($scope.logs&&$scope.logs.length){$scope.lastLogEntry=$scope.logs[0].t}if(response.activity.globalVars){$scope.updateGlobalVars(response.activity.globalVars)}}tmrActivity=$timeout($scope.updateActivity,3000)},function(error){tmrActivity=$timeout($scope.updateActivity,3000)})};$scope.updateGlobalVars=function(globalVars){$scope.globalVars=$scope.globalVars instanceof Object?$scope.globalVars:{};for(varName in globalVars){var varType=globalVars[varName].t;var varValue=globalVars[varName].v;var v=$scope.globalVars[varName];if(!v){$scope.globalVars[varName]={t:varType,v:varValue}}else{if(v.t!=varType){v.t=varType}if(v.v!=varValue){v.v=varValue}}}for(varName in $scope.globalVars){if(!globalVars[varName]){delete ($scope.globalVars[varName])}}};$scope.init=function(){if($scope.$$destroyed){return}dataService.setStatusCallback($scope.setStatus);$scope.loading=true;if($scope.piston){$scope.loading=true}dataService.getPiston($scope.pistonId).then(function(response){if($scope.$$destroyed){return}$scope.endpoint=data.endpoint+"execute/"+$scope.pistonId;try{var showOptions=$scope.piston?!!$scope.showOptions:false;if(!response||!response.data||!response.data.piston){$scope.error=$sce.trustAsHtml("Sorry, an error occurred while retrieving the piston data.");$scope.loading=false;return}$scope.piston=response.data.piston;$scope.validatePiston($scope.piston);$scope.meta=response.data.meta?response.data.meta:{};$scope.db=response.db;$scope.location=dataService.getLocation();$scope.instance=dataService.getInstance();$scope.view=dataService.loadFromStore("view")||{variables:false,elseIfs:false,restrictions:false,whens:false,advancedStatements:false};$scope.subscriptions=response.data.subscriptions?response.data.subscriptions:{};$scope.logs=response.data.logs?response.data.logs:[];$scope.lastLogEntry=($scope.logs&&$scope.logs.length)?$scope.logs[0].t:0;$scope.stats=response.data.stats?response.data.stats:{};$scope.state=response.data.state?response.data.state:"";$scope.trace=response.data.trace?response.data.trace:{};$scope.logging=""+(response.data.logging?response.data.logging:0);$scope.memory=response.data.memory?response.data.memory:0;$scope.lastExecuted=response.data.lastExecuted;$scope.nextSchedule=response.data.nextSchedule;$scope.schedules=response.data.schedules;$scope.categories=$scope.getCategories();$scope.category=$scope.meta.category?$scope.meta.category:"0";$scope.lifx={lights:!!$scope.instance.settings&&!!$scope.instance.lifx.lights?$scope.objectToArray($scope.instance.lifx.lights):[],groups:!!$scope.instance.settings&&!!$scope.instance.lifx.groups?$scope.objectToArray($scope.instance.lifx.groups):[],locations:!!$scope.instance.settings&&!!$scope.instance.lifx.locations?$scope.objectToArray($scope.instance.lifx.locations):[],scenes:!!$scope.instance.settings&&!!$scope.instance.lifx.scenes?$scope.objectToArray($scope.instance.lifx.scenes):[]};$scope.initChart();if($scope.instance&&$scope.instance.devices){$scope.anonymizeDevices($scope.instance.devices)}if($scope.instance&&$scope.instance.contacts){$scope.anonymizeContacts($scope.instance.contacts)}$scope.devices=$scope.listAvailableDevices();$scope.contacts=$scope.listAvailableContacts();$scope.virtualDevices=$scope.listAvailableVirtualDevices();window.scope=$scope;$scope.localVars=response.data.localVars;$scope.globalVars=$scope.instance.globalVars;$scope.systemVars=response.data.systemVars;$scope.systemVarNames=[];for(name in $scope.systemVars){$scope.systemVarNames.push(name)}$scope.meta.build=$scope.meta.build?1*$scope.meta.build:0;if($scope.piston&&($scope.meta.build==0)){$scope.piston.z=$scope.params&&$scope.params.description?$scope.params.description:"";$scope.mode="edit";if($scope.params&&$scope.params.type!="blank"){switch($scope.params.type){case"duplicate":if($scope.params.piston){$scope.loading=true;dataService.getPiston($scope.params.piston).then(function(response){$scope.loading=false;if(response&&response.data&&response.data.piston){$scope.piston.o=response.data.piston.o?response.data.piston.o:{};$scope.piston.r=response.data.piston.r?response.data.piston.r:[];$scope.piston.rn=!!response.data.piston.rn;$scope.piston.rop=response.data.piston.rop?response.data.piston.rop:"and";$scope.piston.s=response.data.piston.s?response.data.piston.s:[];$scope.piston.v=response.data.piston.v?response.data.piston.v:[]}$scope.initialized=true;$scope.loading=false});return}break;case"restore":if($scope.params.bin){$scope.loading=true;dataService.loadFromBin($scope.params.bin).then(function(response){var piston=response.data;$scope.loading=false;if(piston){$scope.piston.o=piston.o?piston.o:{};$scope.piston.r=piston.r?piston.r:[];$scope.piston.rn=!!piston.rn;$scope.piston.rop=piston.rop?piston.rop:"and";$scope.piston.s=piston.s?piston.s:[];$scope.piston.v=piston.v?piston.v:[];$scope.piston.z=piston.z?piston.z:""}$scope.initialized=true;$scope.loading=false;if(!!piston&&(piston.l instanceof Object)&&($scope.objectToArray(piston.l).length)){$scope.rebuildPiston(piston.l)}});return}break}}}if($scope.mode=="edit"){$scope.loadStack()}else{$scope.updateActivity(true)}$scope.piston.o=$scope.piston.o?$scope.piston.o:{cto:0,ced:0};$scope.piston.r=$scope.piston.r?$scope.piston.r:[];$scope.piston.s=$scope.piston.s?$scope.piston.s:[];$scope.piston.rop=$scope.piston.rop?$scope.piston.rop:"and";$scope.piston.rn=!!$scope.piston.rn;$scope.piston.v=$scope.piston.v?$scope.piston.v:[];$scope.piston.z=$scope.piston.z||"";$scope.initialized=true;$scope.loading=false;$scope.render()}catch(e){alert(e)}})};$scope.initChart=function(){$scope.chart={type:"bar",labels:[],series:["Event delay","Load time","Execution time","Update time"],data:[[],[],[],[]],onClick:function(points,evt){},datasetOverride:[{cubicInterpolationMode:"monotone",lineTension:0,yAxisID:"y-axis-1",fill:true,pointRadius:0,borderColor:"#88bbee",borderWidth:0,backgroundColor:"#99ccff"},{cubicInterpolationMode:"monotone",lineTension:0,yAxisID:"y-axis-1",fill:true,pointRadius:0,borderColor:"#eebb88",borderWidth:"0px",backgroundColor:"#ffcc99"},{cubicInterpolationMode:"monotone",lineTension:0,yAxisID:"y-axis-1",fill:true,pointRadius:0,borderColor:"#ee88bb",borderWidth:"0px",backgroundColor:"#ff99cc"},{cubicInterpolationMode:"monotone",lineTension:0,yAxisID:"y-axis-1",fill:true,pointRadius:0,borderColor:"#999",borderWidth:1,backgroundColor:"#ccff99"}],options:{legend:{display:true},multiTooltipTemplate:"<%=datasetLabel%> : <%= value %>ms",showLines:true,fill:true,scales:{xAxes:[{type:"time"}],yAxes:[{id:"y-axis-1",stacked:true,type:"linear",display:true,position:"left"}]},pan:{enabled:true,mode:"x"},zoom:{enabled:true,mode:"x"}}};if($scope.stats&&$scope.stats.timing){for(var i=0;i<$scope.stats.timing.length;i++){$scope.chart.labels.push(new Date($scope.stats.timing[i].t));$scope.chart.data[0].push($scope.stats.timing[i].d);$scope.chart.data[1].push($scope.stats.timing[i].l);$scope.chart.data[2].push($scope.stats.timing[i].e);$scope.chart.data[3].push($scope.stats.timing[i].u)}}};$scope.$on("$destroy",function(){if(tmrStatus){$timeout.cancel(tmrStatus)}if(tmrReveal){$timeout.cancel(tmrReveal)}if(tmrActivity){$timeout.cancel(tmrActivity)}if(tmrClock){$timeout.cancel(tmrClock)}});$scope.copy=function(object){return angular.fromJson(angular.toJson(object))};$scope.home=function(){$scope.initialized=false;$location.path("/")};$scope.toggleView=function(item){if(item){$scope.view[item]=!$scope.view[item]}dataService.saveToStore("view",$scope.view)};$scope.revealBin=function(){$scope.revealing=!$scope.revealing;if(tmrReveal){$timeout.cancel(tmrReveal)}if($scope.revealing){tmrReveal=$timeout(function(){$scope.revealing=false;tmrReveal=null},10000)}};$scope.getCategories=function(){var categories=(!!$scope.instance&&!!$scope.instance.settings&&($scope.instance.settings.categories instanceof Array))?$scope.copy($scope.instance.settings.categories):[];if(!categories.length){categories=[{n:"Uncategorized",t:"d",i:0}]}return categories};$scope.edit=function(){$scope.mode="edit";$scope.init();$("viewer")[0].scrollTop=0};$scope.cancel=function(){$scope.mode="view";$scope.init()};$scope.enableAutomaticBackup=function(){dataService.generateBackupBin().then(function(response){var binId=response.data;dataService.setPistonBin($scope.pistonId,binId).then(function(response){$scope.meta.bin=binId;$scope.save(true);$scope.loading=false})})};$scope.save=function(saveToBinOnly){$scope.loading=true;var piston=$scope.compilePiston({id:$scope.pistonId,o:$scope.piston.o,s:$scope.piston.s,v:$scope.piston.v,r:$scope.piston.r,rop:$scope.piston.rop,rn:$scope.piston.rn,z:$scope.piston.z,n:$scope.meta.name});var promise=dataService.setPiston(piston,$scope.meta.bin,saveToBinOnly);if(promise){promise.then(function(response){if(saveToBinOnly){return}$scope.loading=false;if(response&&response.data&&response.data.build){$scope.meta.active=response.data.active;$scope.meta.modified=response.data.modified;$scope.meta.build=response.data.build;$scope.saveStack(true);$scope.mode="view";$scope.init()}})}};$scope.pause=function(){$scope.loading=true;dataService.pausePiston($scope.pistonId).then(function(data){$scope.loading=false;if(data&&data.status&&(data.status=="ST_SUCCESS")){$scope.meta.active=data.active;$scope.subscriptions={};$scope.updateActivity()}})};$scope.setLoggingLevel=function(obj){$scope.loading=true;dataService.setPistonLogging($scope.pistonId,$scope.logging).then(function(data){$scope.loading=false})};$scope.setCategory=function(){$scope.loading=true;dataService.setPistonCategory($scope.pistonId,$scope.category).then(function(data){$scope.loading=false})};$scope.resume=function(){$scope.loading=true;dataService.resumePiston($scope.pistonId).then(function(data){$scope.loading=false;if(data&&data.status&&(data.status=="ST_SUCCESS")){$scope.meta.active=data.active;if(data.subscriptions){$scope.subscriptions=data.subscriptions}$scope.updateActivity()}})};$scope.del=function(){$scope.loading=true;dataService.deleteFromStore("stack"+$scope.pistonId);dataService.deletePiston($scope.pistonId).then(function(data){$scope.closeDialog();$location.path("/")})};$scope.padComment=function(comment,sz){if(!comment){comment=""}sz=sz-6-comment.replace(/\u200E/g,"").trim().length;while(sz>0){comment+=" ";sz--}return"/* "+comment+" */"};$scope.range=function(n){return new Array(n)};$scope.wiki=function(item){$scope.wikiUrl=$sce.trustAsUrl("https://wiki.webcore.co/"+item+"?content-only");$window.mydialog=ngDialog.open({template:"dialog-wiki",className:"ngdialog-theme-default ngdialog-large ngdialog-wiki",closeByDocument:true,disableAnimation:true,scope:$scope})};$scope.formatVariableValue=function(variable,name){if((variable.v==null)&&!!name&&$scope.localVars){variable=$scope.copy(variable);variable.v=$scope.localVars[name]}var t=(name=="$localNow")||(name=="$utc")?"long":variable.t;if((variable.v==="")||(variable.v===null)||((variable.v instanceof Array)&&!variable.v.length)){return"(not set)"}switch(t){case"time":return utcToTimeString(variable.v);case"datetime":return utcToString(variable.v);case"date":return utcToDateString(variable.v);case"contact":return $scope.renderContactNameList(variable.v);case"device":return $scope.renderDeviceNameList(variable.v)}if(variable.v instanceof Object){return angular.toJson(variable.v)}return variable.v};$scope.deleteDialog=function(){$scope.designer.dialog=ngDialog.open({template:"dialog-del-piston",className:"ngdialog-theme-default ngdialog-large",closeByDocument:false,disableAnimation:true,scope:$scope})};$scope.listDevicesWithAttributes=function(attributes){if(!attributes||!(attributes instanceof Array)||!attributes.length){return $scope.instance.devices}if(attributes.length===1&&attributes[0]===statusAttribute){return $scope.instance.devices}var isThreeAxis;attributes=attributes.filter(function(a){switch(a){case"orientation":case"axisX":case"axisY":case"axisZ":isThreeAxis=true;case statusAttribute:return false}return true}).concat(isThreeAxis?"threeAxis":[]);var result={};for(d in $scope.instance.devices){var device=$scope.instance.devices[d];var found=0;for(a in device.a){if(attributes.indexOf(device.a[a].n)>=0){found++;if(found==attributes.length){break}}}if(found==attributes.length){result[d]=device}}return result};$scope.rebuildPiston=function(legend){if(!legend){return}for(key in legend){var item=legend[key];item.id="";switch(item.t){case"device":item.i=$scope.listDevicesWithAttributes(item.a);break;case"contact":item.i=$scope.instance.contacts;break;case"mode":item.i=$scope.instance.virtualDevices.mode.o;for(i in item.i){if(item.i[i]==item.n){item.id=i;break}}break;case"routine":item.i=$scope.instance.virtualDevices.routine.o;break}}$scope.designer={legend:legend};$scope.designer.dialog=ngDialog.open({template:"dialog-rebuild-piston",className:"ngdialog-theme-default ngdialog-large",closeByDocument:false,disableAnimation:true,scope:$scope})};$scope.doRebuildPiston=function(){$scope.piston=$scope.compilePiston($scope.piston,false,$scope.designer.legend);$scope.closeDialog()};$scope.doValidatePiston=function(){$scope.validatePiston($scope.piston)};$scope.getExpressionConfig=function(){var attributes=[];for(attribute in $scope.db.attributes){attributes.push(": "+attribute+"]");if(attribute=="threeAxis"){attributes.push(": axisX]");attributes.push(": axisY]");attributes.push(": axisZ]");attributes.push(": orientation]")}}return{autocomplete:[{words:[]},{words:$scope.listAutoCompleteFunctions(),cssClass:"hl kwd"},{words:$scope.listAutoCompleteVariables(),cssClass:"hl var"},{words:$scope.listAutoCompleteDevices(),cssClass:"hl dev"},{words:attributes,cssClass:"hl dev"},{words:[/([0-9]+)(\.[0-9]+)?/g],cssClass:"hl num"}]}};$scope.removeFromArray=function(array,value){if(!(array instanceof Array)){return}var idx=array.indexOf(value);if(idx!==-1){array.splice(idx,1)}return array};$scope.deleteObject=function(obj,parent){var dialog=!obj;if(dialog){obj=$scope.designer.$obj;parent=$scope.designer.parent}if(!obj){return}if((parent instanceof Array)&&(obj)){$scope.autoSave();parent=$scope.removeFromArray(parent,obj);if(dialog){$scope.closeDialog()}}if(parent&&(parent.t=="action")&&(parent.k instanceof Array)&&(obj)){$scope.autoSave();parent.k=$scope.removeFromArray(parent.k,obj);if(dialog){$scope.closeDialog()}}};$scope.getIFTTTUri=function(eventName){var uri=dataService.getApiUri();if(!uri){return"An error has occurred retrieving the IFTTT Maker URL"}return uri+"ifttt/"+eventName};$scope.toggleAdvancedOptions=function(){$scope.designer.showAdvancedOptions=!$scope.designer.showAdvancedOptions};$scope.getClipboard=function(){var clipboard=dataService.loadFromStore("clipboard");if(!clipboard){clipboard=[]}return clipboard};$scope.getClipboardItems=function(itemType){var clipboard=$scope.getClipboard();var result=[];for(i in clipboard){if(clipboard[i].t.startsWith(itemType)){result.push(clipboard[i])}}return result};$scope.saveToClipboard=function(object,objectType){var clipboard=$scope.getClipboard();clipboard.push({s:(new Date()).getTime(),t:objectType,o:$scope.copy(object)});if(clipboard.length>MAX_STACK_SIZE){clipboard=clipboard.slice(-MAX_STACK_SIZE)}dataService.saveToStore("clipboard",clipboard)};$scope.deleteClipboardItem=function(item){var clipboard=$scope.getClipboard();$scope.removeFromArray($scope.designer.clipboard,item);for(i=0;i=0)?"1":"0";statement.tcp=$scope.designer.tcp;statement.tep=$scope.designer.tep;statement.tsp=$scope.designer.tsp;statement.z=$scope.designer.description;statement.r=statement.r?statement.r:[];statement.rop=$scope.designer.roperator;statement.rn=$scope.designer.rnot=="1";statement.di=$scope.designer.disabled=="1";switch(statement.t){case"action":statement.d=$scope.designer.devices;statement.k=statement.k?statement.k:[];break;case"do":statement.s=statement.s?statement.s:[];break;case"on":statement.c=statement.c?statement.c:[];statement.o="or";statement.n=false;statement.s=statement.s?statement.s:[];break;case"if":statement.o=$scope.designer.operator;statement.n=$scope.designer.not=="1";statement.c=statement.c?statement.c:[];statement.s=statement.s?statement.s:[];statement.ei=statement.ei?statement.ei:[];statement.e=statement.e?statement.e:[];break;case"switch":statement.lo=$scope.designer.operand.data;statement.cs=statement.cs||[];statement.e=statement.e?statement.e:[];statement.ctp=$scope.designer.ctp;break;case"for":statement.x=$scope.designer.x;statement.lo=$scope.designer.operand.data;statement.lo2=$scope.designer.operand2.data;statement.lo3=$scope.designer.operand3.data;statement.s=statement.s?statement.s:[];break;case"each":statement.x=$scope.designer.x;statement.lo=$scope.designer.operand.data;statement.s=statement.s?statement.s:[];break;case"while":statement.o=$scope.designer.operator;statement.n=$scope.designer.not=="1";statement.c=statement.c?statement.c:[];statement.s=statement.s?statement.s:[];break;case"every":statement.lo=$scope.designer.operand.data;statement.lo2=$scope.designer.operand2.data;if(statement.lo2.c instanceof Date){statement.lo2.c=statement.lo2.c.getHours()*60+statement.lo2.c.getMinutes()}statement.lo3=$scope.designer.operand3.data;statement.s=statement.s?statement.s:[];break;case"repeat":statement.o=$scope.designer.operator;statement.n=$scope.designer.not=="1";statement.c=statement.c?statement.c:[];statement.s=statement.s?statement.s:[];break;case"break":break;case"exit":statement.lo=$scope.designer.operand.data;break;default:statement.t=null}if(statement.t){statement.$$html=null;if($scope.designer.$new){if($scope.designer.parent instanceof Array){$scope.designer.parent.push(statement)}else{if(($scope.designer.parent.s)&&($scope.designer.parent.s instanceof Array)){$scope.designer.parent.s.push(statement)}else{$scope.designer.parent.s=[statement]}}}else{$scope.designer.$statement=statement}}$scope.doValidatePiston();$scope.closeDialog();if(nextDialog){switch(statement.t){case"action":$scope.addTask(statement);return;case"if":$scope.addCondition(statement.c,false,defaultType);return;case"on":$scope.addEvent(statement.c);return;case"while":$scope.addCondition(statement.c);return;case"do":case"for":case"each":case"repeat":case"every":$scope.addStatement(statement.s);return;case"switch":$scope.addCase(statement.cs);return}}};$scope.upgradeStatement=function(){$scope.updateStatement();var statement=$scope.designer.$statement;if(statement&&statement.c&&(statement.c instanceof Array)){statement.c=[{t:"group",n:false,o:"and",c:statement.c}]}};$scope.addCase=function(parent){return $scope.editCase(null,parent)};$scope.editCase=function(_case,parent){if($scope.mode!="edit"){return}var _new=_case?false:true;if(!_case){_case={};_case.t="s";_case.s=[];_case.ro={};_case.ro2={};_case.z=""}$scope.designer={config:$scope.getExpressionConfig()};$scope.designer.$obj=_case;$scope.designer.$case=_case;$scope.designer.$new=_new;$scope.designer.parent=parent;$scope.designer.type=_case.t;$scope.designer.operand={data:_case.ro,multiple:false};$scope.designer.operand2={data:_case.ro2,multiple:false};$scope.designer.autoDialogs=true;$scope.designer.description=_case.z;window.designer=$scope.designer;$scope.validateOperand($scope.designer.operand);$scope.validateOperand($scope.designer.operand2);$scope.designer.dialog=ngDialog.open({template:"dialog-edit-case",className:"ngdialog-theme-default ngdialog-large",closeByDocument:false,disableAnimation:true,scope:$scope})};$scope.updateCase=function(nextDialog){$scope.autoSave();var _case=$scope.designer.$case;_case.t=$scope.designer.type;_case.s=_case.s||[];_case.ro=$scope.designer.operand.data;_case.ro2=$scope.designer.operand2.data;_case.z=$scope.designer.description;if(_case.t){_case.$$html=null;_case.$$html2=null;if($scope.designer.$new){if($scope.designer.parent instanceof Array){$scope.designer.parent.push(_case)}else{if(($scope.designer.parent.cs)&&($scope.designer.parent.cs instanceof Array)){$scope.designer.parent.cs.push(_case)}else{$scope.designer.parent.cs=[_case]}}}else{$scope.designer.$case=_case}}$scope.doValidatePiston();$scope.closeDialog();if(nextDialog){$scope.addStatement(_case.s);return}};$scope.addEvent=function(parent){return $scope.editEvent(null,parent)};$scope.editEvent=function(event,parent){if($scope.mode!="edit"){return}var _new=!event;if(!event){event={};event.t="event";event.lo={t:"p",d:[],a:null,g:"any",v:null,c:"",x:null,e:""};event.z="";event.sm="auto"}$scope.designer={config:$scope.getExpressionConfig(),clipboard:_new?$scope.getClipboardItems("event"):[]};$scope.designer.$event=event;$scope.designer.$obj=event;$scope.designer.type=event.t;$scope.designer.$new=_new;$scope.designer.parent=parent;$scope.designer.comparison={event:true,type:"event",left:{data:event.lo?$scope.copy(event.lo):{},event:true}};$scope.validateComparison($scope.designer.comparison,true);$scope.designer.smode=event.sm;$scope.designer.description=event.z;window.designer=$scope.designer;$scope.designer.dialog=ngDialog.open({template:"dialog-edit-event",className:"ngdialog-theme-default ngdialog-large",closeByDocument:false,disableAnimation:true,scope:$scope})};$scope.updateEvent=function(nextDialog){$scope.autoSave();var event=$scope.designer.$new?{t:$scope.designer.type}:$scope.designer.$event;event.lo=$scope.fixOperand($scope.designer.comparison.left.data);event.sm=$scope.designer.smode;event.z=$scope.designer.description;if(event.t){event.$$html=null;if($scope.designer.$new){if($scope.designer.parent instanceof Array){$scope.designer.parent.push(event)}else{if(($scope.designer.parent.c)&&($scope.designer.parent.c instanceof Array)){$scope.designer.parent.c.push(event)}else{$scope.designer.parent.c=[event]}}}else{$scope.designer.$event=event}}$scope.doValidatePiston();$scope.closeDialog();if(event.t&&nextDialog){$scope.addEvent($scope.designer.parent);return}};$scope.addCondition=function(parent,newElseIf,defaultType,groupingMethod){return $scope.editCondition(null,parent,newElseIf,defaultType,groupingMethod?groupingMethod:(parent?parent.o:null))};$scope.editCondition=function(condition,parent,newElseIf,defaultType,groupingMethod){if($scope.mode!="edit"){return}var _new=!condition;var list=parent instanceof Array?parent:(parent instanceof Object?parent.c:null);var followedBy=(groupingMethod=="followed by")&&(list instanceof Array)&&(list.length>0)&&(list[0]!=condition);if(!condition){condition={};condition.t=defaultType;condition.d=[];condition.n=false;condition.o="and";condition.lo={t:"p",d:[],a:null,g:"any",v:null,c:"",x:null,e:""};condition.co=null;condition.ro={t:"c",d:[],a:null,g:"any",v:null,c:"",x:null,e:""};condition.ro2={t:"c",d:[],a:null,g:"any",v:null,c:"",x:null,e:""};condition.to={t:"c",d:[],a:null,g:"any",v:null,c:"",x:null,e:""};condition.to2={t:"c",d:[],a:null,g:"any",v:null,c:"",x:null,e:""};condition.z="";condition.sm="auto";condition.ts=[];condition.fs=[]}$scope.designer={config:$scope.getExpressionConfig(),clipboard:_new?$scope.getClipboardItems("condition"):[]};$scope.designer.$condition=condition;$scope.designer.followedBy=followedBy;$scope.designer.$obj=condition;$scope.designer.type=condition.t;$scope.designer.$new=!defaultType&&!!condition.t?false:true;$scope.designer.newElseIf=newElseIf;$scope.designer.page=$scope.designer.$new&&!defaultType?0:1;$scope.designer.parent=parent;$scope.designer.devices=condition.d;$scope.designer.not=condition.n?"1":"0";$scope.designer.operator=condition.o;$scope.designer.comparison={type:"condition",followedBy:followedBy,left:{data:condition.lo?$scope.copy(condition.lo):{},showSubDevices:true,showInteraction:true},operator:condition.co,right:{data:condition.ro?$scope.copy(condition.ro):{}},right2:{data:condition.ro2?$scope.copy(condition.ro2):{}},time:{data:condition.to?$scope.copy(condition.to):{t:"c",c:0},dataType:"duration"},time2:{data:condition.to2?$scope.copy(condition.to2):{t:"c",c:0},dataType:"duration"}};if(followedBy){$scope.designer.comparison.within={data:condition.wd?$scope.copy(condition.wd):{t:"c",c:1,vt:"m"},style:"success",dataType:"duration",hideMilliseconds:true};$scope.designer.comparison.withinOpt=(condition.wt?condition.wt:"l")}$scope.validateComparison($scope.designer.comparison,true);$scope.designer.smode=condition.sm;$scope.designer.description=condition.z;window.designer=$scope.designer;$scope.designer.items=[{type:"condition",name:"Condition",icon:"code",cssClass:"btn-info"},{type:"group",name:"Group",icon:"code-fork",cssClass:"btn-warning"}];$scope.designer.dialog=ngDialog.open({template:"dialog-edit-condition",className:"ngdialog-theme-default ngdialog-large",closeByDocument:false,disableAnimation:true,scope:$scope})};$scope.fixOperand=function(data){switch(data.vt){case"time":data.c=data.c instanceof Date?data.c.getHours()*60+data.c.getMinutes():data.c;break;case"date":case"datetime":data.c=data.c instanceof Date?data.c.getTime():(new Date(data.c)).getTime();break}return data};$scope.updateCondition=function(nextDialog){$scope.autoSave();var condition=$scope.designer.$new?{t:$scope.designer.type}:$scope.designer.$condition;switch(condition.t){case"condition":condition.lo=$scope.fixOperand($scope.designer.comparison.left.data);condition.co=$scope.designer.comparison.operator;condition.ro=$scope.fixOperand($scope.designer.comparison.right.data);condition.ro2=$scope.fixOperand($scope.designer.comparison.right2.data);condition.to=$scope.designer.comparison.time.data;condition.to2=$scope.designer.comparison.time2.data;if($scope.designer.followedBy){condition.wd=$scope.designer.comparison.within.data;condition.wt=$scope.designer.comparison.withinOpt}break;case"group":condition.c=condition.c?condition.c:[];condition.o=$scope.designer.operator;condition.n=$scope.designer.not=="1";if($scope.designer.followedBy){condition.wd=$scope.designer.comparison.within.data;condition.wt=$scope.designer.comparison.withinOpt}break}condition.sm=$scope.designer.smode;condition.ts=condition.ts?condition.ts:[];condition.fs=condition.fs?condition.fs:[];condition.z=$scope.designer.description;if(condition.t){condition.$$html=null;if($scope.designer.$new){if($scope.designer.newElseIf){var elseIf={o:"and",n:false,c:[],s:[]};elseIf.c.push(condition);$scope.designer.parent.push(elseIf)}else{if($scope.designer.parent instanceof Array){$scope.designer.parent.push(condition)}else{if(($scope.designer.parent.c)&&($scope.designer.parent.c instanceof Array)){$scope.designer.parent.c.push(condition)}else{$scope.designer.parent.c=[condition]}}}}else{$scope.designer.$condition=condition}}$scope.doValidatePiston();$scope.closeDialog();if(condition.t&&nextDialog){$scope.addCondition(condition.t=="group"?condition:$scope.designer.parent);return}};$scope.upgradeCondition=function(){$scope.updateCondition();var parent=$scope.designer.parent;if($scope.designer.$condition&&parent&&(parent instanceof Array)){var index=parent.indexOf($scope.designer.$condition);if(index>=0){var condition={};condition.t=$scope.designer.$condition.t;condition.n=$scope.designer.$condition.n;condition.o=$scope.designer.$condition.o;condition.c=$scope.designer.$condition.c;$scope.designer.$condition={};$scope.designer.$condition.t="group";$scope.designer.$condition.n=false;$scope.designer.$condition.o="and";$scope.designer.$condition.c=[condition];parent[index]=$scope.designer.$condition}}};$scope.editConditionGroup=function(group,parent,groupingMethod){if($scope.mode!="edit"){return}var followedBy=(groupingMethod=="followed by")&&(parent instanceof Array)&&(parent.length>0)&&(parent[0]!=group);$scope.designer={operator:group.o||"and",not:group.n?"1":"0",description:(group.t=="group"?group.z:group.zc)};$scope.designer.group=group;$scope.designer.followedBy=followedBy;$scope.designer.$obj=group;$scope.designer.parent=parent;if(followedBy){$scope.designer.within={data:group.wd?$scope.copy(group.wd):{t:"c",c:1,vt:"m"},style:"success",dataType:"duration",hideMilliseconds:true};$scope.designer.withinOpt=(group.wt?group.wt:"l");$scope.validateOperand($scope.designer.within)}window.designer=$scope.designer;$scope.designer.dialog=ngDialog.open({template:"dialog-edit-condition-group",className:"ngdialog-theme-default ngdialog-large",closeByDocument:false,disableAnimation:true,scope:$scope})};$scope.updateConditionGroup=function(){$scope.autoSave();var group=$scope.designer.group;group.n=$scope.designer.not=="1";group.o=$scope.designer.operator;if(group.t=="group"){group.z=$scope.designer.description;if($scope.designer.followedBy){group.wd=$scope.designer.within.data;group.wt=$scope.designer.withinOpt}}else{group.zc=$scope.designer.description}$scope.closeDialog()};$scope.addRestriction=function(parent){return $scope.editRestriction(null,parent)};$scope.editRestriction=function(restriction,parent){if($scope.mode!="edit"){return}var _new=!restriction;if(!restriction){restriction={};restriction.t=null;restriction.d=[];restriction.rn=false;restriction.rop="and";restriction.lo={t:"p",d:[],a:null,g:"any",v:null,c:"",x:null,e:""};restriction.co=null;restriction.ro={t:"c",d:[],a:null,g:"any",v:null,c:"",x:null,e:""};restriction.ro2={t:"c",d:[],a:null,g:"any",v:null,c:"",x:null,e:""};restriction.to={t:"c",d:[],a:null,g:"any",v:null,c:"",x:null,e:""};restriction.to2={t:"c",d:[],a:null,g:"any",v:null,c:"",x:null,e:""};restriction.z=""}$scope.designer={config:$scope.getExpressionConfig(),clipboard:_new?$scope.getClipboardItems("restriction"):[]};$scope.designer.$restriction=restriction;$scope.designer.$obj=restriction;$scope.designer.type=restriction.t;$scope.designer.$new=restriction.t?false:true;$scope.designer.page=$scope.designer.$new?0:1;$scope.designer.parent=parent;$scope.designer.devices=restriction.d;$scope.designer.not=restriction.rn?"1":"0";$scope.designer.operator=restriction.rop;$scope.designer.comparison={type:"restriction",left:{data:restriction.lo?$scope.copy(restriction.lo):{}},operator:restriction.co,right:{data:restriction.ro?$scope.copy(restriction.ro):{}},right2:{data:restriction.ro2?$scope.copy(restriction.ro2):{}},time:{data:restriction.to?$scope.copy(restriction.to):{t:"c",c:0},dataType:"duration"},time2:{data:restriction.to2?$scope.copy(restriction.to2):{t:"c",c:0},dataType:"duration"}};$scope.validateComparison($scope.designer.comparison,true);$scope.designer.smode=restriction.sm;$scope.designer.description=restriction.z;window.designer=$scope.designer;$scope.designer.items=[{type:"restriction",name:"Restriction",icon:"code",cssClass:"btn-info"},{type:"group",name:"Group",icon:"code-fork",cssClass:"btn-warning"}];$scope.designer.dialog=ngDialog.open({template:"dialog-edit-restriction",className:"ngdialog-theme-default ngdialog-large",closeByDocument:false,disableAnimation:true,scope:$scope})};$scope.updateRestriction=function(nextDialog){$scope.autoSave();var restriction=$scope.designer.$new?{t:$scope.designer.type}:$scope.designer.$restriction;switch(restriction.t){case"restriction":restriction.lo=$scope.fixOperand($scope.designer.comparison.left.data);restriction.co=$scope.designer.comparison.operator;restriction.ro=$scope.fixOperand($scope.designer.comparison.right.data);restriction.ro2=$scope.fixOperand($scope.designer.comparison.right2.data);restriction.to=$scope.designer.comparison.time.data;restriction.to2=$scope.designer.comparison.time2.data;break;case"group":restriction.r=restriction.r?restriction.r:[];restriction.rop=$scope.designer.operator;restriction.rn=$scope.designer.not=="1";break}restriction.z=$scope.designer.description;if(restriction.t){restriction.$$html=null;if($scope.designer.$new){if($scope.designer.parent instanceof Array){$scope.designer.parent.push(restriction)}else{if(($scope.designer.parent.r)&&($scope.designer.parent.r instanceof Array)){$scope.designer.parent.r.push(restriction)}else{$scope.designer.parent.r=[restriction]}}}else{$scope.designer.$restriction=restriction}}$scope.doValidatePiston();$scope.closeDialog();if(restriction.t&&nextDialog){$scope.addRestriction(restriction.t=="group"?restriction:$scope.designer.parent);return}};$scope.upgradeRestriction=function(){$scope.updateRestriction();var parent=$scope.designer.parent;if($scope.designer.$restriction&&parent&&(parent instanceof Array)){var index=parent.indexOf($scope.designer.$restriction);if(index>=0){var restriction={};restriction.t=$scope.designer.$restriction.t;restriction.rn=$scope.designer.$restriction.rn;restriction.rop=$scope.designer.$restriction.rop;restriction.c=$scope.designer.$restriction.c;$scope.designer.$restriction={};$scope.designer.$restriction.t="group";$scope.designer.$restriction.rn=false;$scope.designer.$restriction.rop="and";$scope.designer.$restriction.c=[restriction];parent[index]=$scope.designer.$restriction}}};$scope.editRestrictionGroup=function(group,parent){if($scope.mode!="edit"){return}$scope.designer={operator:group.rop||"and",not:group.rn?"1":"0",description:(group.t=="group"?group.z:group.zr)};$scope.designer.group=group;$scope.designer.$obj=group;$scope.designer.parent=parent;window.designer=$scope.designer;$scope.designer.dialog=ngDialog.open({template:"dialog-edit-restriction-group",className:"ngdialog-theme-default ngdialog-large",closeByDocument:false,disableAnimation:true,scope:$scope})};$scope.updateRestrictionGroup=function(){$scope.autoSave();var group=$scope.designer.group;group.rn=$scope.designer.not=="1";group.rop=$scope.designer.operator;if(group.t=="group"){group.z=$scope.designer.description}else{group.zr=$scope.designer.description}$scope.closeDialog()};$scope.addTask=function(parent){return $scope.editTask(null,parent)};$scope.editTask=function(task,parent){if($scope.mode!="edit"){return}if(!task){task={};task.c="";task.a="0";task.m="";task.z=""}var _new=task.c?false:true;$scope.designer={clipboard:_new?$scope.getClipboardItems("task"):[]};var insertIndex=_new?$scope.insertIndexes[parent.$$hashkey]:parent.k.indexOf(task);if(isNaN(insertIndex)){insertIndex=parent.k.length}$scope.designer.insertIndex=insertIndex;$scope.designer.$task=task;$scope.designer.$obj=task;$scope.designer.$new=_new;$scope.designer.page=0;$scope.designer.parent=parent;$scope.designer.command=task.c;$scope.designer.mode=task.m;$scope.designer.description=task.z;$scope.prepareParameters(task);window.designer=$scope.designer;window.scope=$scope;$scope.designer.commands=$scope.listAvailableCommands(parent.d);$("a-ckolor-wheel").remove();$scope.designer.dialog=ngDialog.open({template:"dialog-edit-task",className:"ngdialog-theme-default ngdialog-large",closeByDocument:false,disableAnimation:true,scope:$scope})};$scope.updateTask=function(nextDialog){$scope.autoSave();var task=$scope.designer.$new?{}:$scope.designer.$task;task.c=$scope.designer.command;task.a=$scope.designer.async;task.z=$scope.designer.description;task.m=$scope.designer.mode;if(task.c){task.$$html=null;task.p=[];for(parameterIndex in $scope.designer.parameters){var param=$scope.designer.parameters[parameterIndex].data;if(param.t=="c"){switch(param.vt){case"time":param.c=param.c instanceof Date?param.c.getHours()*60+param.c.getMinutes():param.c;break;case"date":case"datetime":param.c=param.c instanceof Date?param.c.getTime():(new Date(param.c)).getTime();break}}task.p.push(param)}if($scope.designer.$new){if(($scope.designer.parent)&&($scope.designer.parent.k instanceof Array)){$scope.designer.parent.k.push(task);$scope.insertIndexes[parent.$$hashkey]=$scope.designer.insertIndex+1}}else{$scope.designer.$task=task}}var tasks=$scope.designer.parent.k;if(tasks&&tasks.length){var currentIndex=tasks.indexOf(task);var insertIndex=$scope.designer.insertIndex;if(insertIndex>currentIndex){insertIndex--}if(insertIndex>=tasks.length){insertIndex=tasks.length-1}if($scope.designer.insertIndex!=currentIndex){tasks.splice(insertIndex,0,tasks.splice(currentIndex,1)[0])}}$scope.doValidatePiston();$scope.closeDialog();if(nextDialog){$scope.addTask($scope.designer.parent);return}};$scope.addVariable=function(){return $scope.editVariable(null)};$scope.editVariable=function(variable){if($scope.mode!="edit"){return}if(!variable){variable={};variable.t="dynamic";variable.n="";variable.v={data:{}};variable.a="d";variable.z=""}if(variable.v instanceof Array){variable.v={data:{}}}$scope.designer={};$scope.designer.$variable=variable;$scope.designer.$obj=variable;$scope.designer.$new=variable.n?false:true;$scope.designer.page=0;$scope.designer.parent=$scope.piston.v;$scope.designer.type=variable.t;$scope.designer.assignment=variable.a||"d";$scope.designer.name=variable.n;$scope.designer.operand={data:variable.v,multiple:false,dataType:variable.t,optional:true};$scope.designer.description=variable.z;window.designer=$scope.designer;window.scope=$scope;$scope.validateOperand($scope.designer.operand);$scope.designer.dialog=ngDialog.open({template:"dialog-edit-variable",className:"ngdialog-theme-default ngdialog-large",closeByDocument:false,disableAnimation:true,scope:$scope});$scope.refreshSelects()};$scope.updateVariable=function(nextDialog){$scope.autoSave();var variable=$scope.designer.$new?{}:$scope.designer.$variable;variable.t=$scope.designer.operand.dataType;variable.n=$scope.designer.name.trim().replace(/[^a-z0-9]|\s+|\r?\n|\r/gmi,"_");variable.z=$scope.designer.description;variable.a=$scope.designer.assignment;var value=$scope.fixOperand($scope.designer.operand.data);switch(value.t){case"":variable.v=null;break;default:variable.v=value;break}variable.$$html=null;if($scope.designer.$new){$scope.piston.v.push(variable)}else{$scope.designer.variable=variable}$scope.doValidatePiston();$scope.closeDialog();if(nextDialog){$scope.addVariable();return}};$scope.editLocalVariable=function(variable){if(!variable||!variable.n||!variable.t||!!variable.v){return}var value=$scope.localVars[variable.n];if((value instanceof Array)&&(value.length==0)){value=null}if(!variable){return}$scope.designer={};$scope.designer.$variableName=variable.n;$scope.designer.$variable=variable;$scope.designer.$obj=variable;$scope.designer.name=variable.n;$scope.designer.type=variable.t;$scope.designer.operand={data:{t:!value?"":(variable.t=="device"?"d":"c"),c:value,d:value},multiple:false,dataType:variable.t,optional:true,onlyAllowConstants:true,disableExpressions:true};window.designer=$scope.designer;window.scope=$scope;$scope.validateOperand($scope.designer.operand);$scope.designer.dialog=ngDialog.open({template:"dialog-edit-local-variable",className:"ngdialog-theme-default ngdialog-large",closeByDocument:false,disableAnimation:true,scope:$scope})};$scope.updateLocalVariable=function(nextDialog){var variable=$scope.designer.$variable;var value=variable.t=="device"?$scope.designer.operand.data.d:($scope.designer.operand.data.t=="c"?$scope.designer.operand.data.c:null);dataService.setVariable($scope.designer.$variableName,{t:variable.t,v:value},$scope.pistonId).then(function(data){if(data&&data.localVars&&data.id&&(data.id==$scope.pistonId)){$scope.localVars=data.localVars}});$scope.closeDialog()};$scope.addGlobalVariable=function(){return $scope.editGlobalVariable(null)};$scope.editGlobalVariable=function(variableName){if($scope.mode!="edit"){return}var variable=$scope.globalVars[variableName];if(!variable){variable={t:"dynamic",v:""}}$scope.designer={};$scope.designer.$variableName=variableName;$scope.designer.$variable=variable;$scope.designer.$obj=variable;$scope.designer.$new=variableName?false:true;$scope.designer.name=variableName?""+variableName:"@";$scope.designer.type=variable.t;$scope.designer.operand={data:{t:!variable.v?"":(variable.t=="device"?"d":"c"),c:variable.v,d:variable.v},multiple:false,dataType:variable.t,optional:true,onlyAllowConstants:true};window.designer=$scope.designer;window.scope=$scope;$scope.validateOperand($scope.designer.operand);$scope.designer.dialog=ngDialog.open({template:"dialog-edit-global-variable",className:"ngdialog-theme-default ngdialog-large",closeByDocument:false,disableAnimation:true,scope:$scope})};$scope.updateGlobalVariable=function(nextDialog){$scope.autoSave();var variable=$scope.designer.$new?{}:$scope.designer.$variable;variable.t=$scope.designer.operand.dataType;variable.n=$scope.designer.name.trim().replace(/[^@a-z0-9]|\s+|\r?\n|\r/gmi,"_");var value=$scope.designer.operand.data;switch(value.t){case"":variable.v=variable.t=="device"?[]:null;break;case"c":variable.v=value.c?value.c:"";break;case"d":variable.v=value.d?value.d:[];break;default:variable.v=null;break}delete (variable.$$html);dataService.setVariable($scope.designer.$variableName,variable).then(function(data){if(data&&data.globalVars){$scope.updateGlobalVars(data.globalVars)}});$scope.doValidatePiston();$scope.closeDialog();if(nextDialog){$scope.addGlobalVariable();return}};$scope.deleteGlobalVariable=function(){if((!$scope.designer)||(!$scope.designer.$variableName)){return}dataService.setVariable($scope.designer.$variableName,null).then(function(data){if(data&&data.globalVars){$scope.updateGlobalVars(data.globalVars)}});$scope.closeDialog()};$scope.validateGlobalVariableName=function(){if(!$scope.designer){return false}var name=$scope.designer.name;if(!name){return false}if(!name.startsWith("@")){name="@"+name}while(name.startsWith("@@@")){name=name.substr(1)}if($scope.designer.name!=name){$scope.designer.name=name}return name&&(name!="@")&&(name!="@@")&&(($scope.designer.$variableName==name)||!($scope.globalVars[name]))};$scope.getDeviceAttributeValue=function(device,attributeName){for(i in device.a){if(device.a[i].n==attributeName){var result={v:device.a[i].v,t:device.a[i].v};if(result.v==undefined){result.v=""}if((attributeName=="battery")&&(!isNaN(result.v))){result.t=result.t+"%";result.v=Math.floor(parseInt(result.v)/20);if(result.v>4){result.v=4}}if((attributeName=="temperature")&&(!isNaN(result.v))){result.v=Math.round(parseFloat(result.v)).toString()+"°";result.t=result.v}return result}}return{v:"",t:""}};$scope.renderDevice=function(device){var sSwitch=$scope.getDeviceAttributeValue(device,"switch");var sSwitch=sSwitch?'class="fa fa-toggle-off" switch="'+sSwitch+'"':"";var attributes=["temperature","battery","switch","motion","presence"];var result="
"+device.n+"
";for(a in attributes){var value=$scope.getDeviceAttributeValue(device,attributes[a]);result+="
'}return $sce.trustAsHtml(result)};$scope.drag=function(list,index){list.splice(index,1);$scope.autoSave();$scope.doValidatePiston()};$scope.copyVariable=function(list,index){var variable=list[index];for(var i=0;iparameterIndex)){p.data=$scope.copy(task.p[parameterIndex])}$scope.validateOperand(p);$scope.designer.parameters.push(p)}}else{$scope.designer.custom=!!$scope.designer.command;for(i in task.p){var param={dataType:task.p[i].vt,data:$scope.copy(task.p[i])};$scope.validateOperand(param);$scope.designer.parameters.push(param)}}if($scope.designer.command=="setVariable"){$scope.designer.parameters[0].linkedOperand=$scope.designer.parameters[1];$scope.validateOperand($scope.designer.parameters[0])}$scope.refreshSelects()};$scope.renameParameters=function(){if(!$scope.designer.custom){return}for(i in $scope.designer.parameters){$scope.designer.parameters[i].name="Parameter #"+(parseInt(i)+1).toString()+" ("+$scope.designer.parameters[i].dataType+")"}$scope.refreshSelects()};$scope.addParameter=function(dataType){if(!$scope.designer.custom){return}var param={dataType:dataType,name:"",data:{t:"c"}};$scope.validateOperand(param);$scope.designer.parameters.push(param);$scope.renameParameters()};$scope.deleteParameter=function(parameter){if(!$scope.designer.custom){return}var index=$scope.designer.parameters.indexOf(parameter);if(index>-1){$scope.designer.parameters.splice(index,1)}$scope.renameParameters()};$scope.getParameterInputType=function(parameter){switch(parameter.t){case"color":case"duration":case"enum":case"boolean":return parameter.t;case"number":return Math.abs(parameter.M-parameter.m)>360?"number":"range"}return"text"};$scope.getParameterMin=function(parameter){switch(parameter.t){case"level":case"saturation":case"hue":return 0;case"colorTemperature":return 1500}return null};$scope.getParameterMax=function(parameter){switch(parameter.t){case"level":case"saturation":return 100;case"hue":return 360;case"colorTemperature":return 10000}return null};$scope.getContactById=function(contactId){return $scope.instance.contacts[contactId]};$scope.getRoutineById=function(routineId){if($scope.instance.virtualDevices.routine&&$scope.instance.virtualDevices.routine.o){return $scope.instance.virtualDevices.routine.o[routineId]}return null};$scope.getLocationModeById=function(locationModeId){if($scope.instance.virtualDevices.mode&&$scope.instance.virtualDevices.mode.o){return $scope.instance.virtualDevices.mode.o[locationModeId]}return null};$scope.getDeviceById=function(deviceId){if(deviceId==$scope.location.id){return{id:deviceId,n:$scope.location.name,an:"Location"}}return $scope.instance.devices[deviceId]};$scope.getDeviceByName=function(deviceName){for(deviceIndex in $scope.instance.devices){if($scope.instance.devices[deviceIndex].n==deviceName){return mergeObjects({id:deviceIndex},$scope.instance.devices[deviceIndex])}}return null};$scope.getVirtualDeviceById=function(deviceId){if(deviceId==$scope.location.id){return{id:deviceId,name:$scope.location.name}}return $scope.instance.virtualDevices[deviceId]};$scope.getCapabilityById=function(capabilityId){return $scope.db.capabilities[capabilityId]};$scope.getCapabilityByName=function(capabilityName){for(capabilityIndex in $scope.db.capabilities){if($scope.db.capabilities[capabilityIndex].n==capabilityName){return mergeObjects({id:capabilityIndex},$scope.db.capabilities[capabilityIndex])}}return null};$scope.getCommandById=function(commandId){return $scope.db.commands.physical[commandId]||$scope.db.commands.virtual[commandId]};$scope.getCommandByName=function(commandName){for(commandIndex in $scope.db.commands.physical){if($scope.db.commands.physical[commandIndex].n==commandName){return mergeObjects({id:commandIndex},$scope.db.commands.physical[commandIndex])}}for(commandIndex in $scope.db.commands.virtual){if($scope.db.commands.virtual[commandIndex].n==commandName){return mergeObjects({id:commandIndex},$scope.db.commands.virtual[commandIndex])}}return null};$scope.getAttributeById=function(attributeId){return $scope.db.attributes[attributeId]};$scope.getAttributeByName=function(attributeName){for(attributeIndex in $scope.db.attributes){if($scope.db.attributes[attributeIndex].n==attributeName){return $scope.db.attributes[attributeIndex]}}return null};$scope.getDeviceAttributeById=function(device,attributeId){if(!device){return null}for(i in device.a){if(device.a[i].n==attributeId){return device.a[i]}}return null};$scope.buildName=function(name,noQuotes,pedantic,itemPrefix,grouping){if((name==null)||(name==undefined)){return""}if(name instanceof Array){return $scope.buildNameList(name,grouping?grouping:"or","","",false,noQuotes,pedantic,itemPrefix)}if(pedantic||(name.length==34)){for(deviceId in $scope.instance.virtualDevices){var device=$scope.instance.virtualDevices[deviceId];if(device.o){for(id in device.o){noQuotes=noQuotes||!pedantic;if(name==id){return(!noQuotes?"'":"")+device.o[id]+(!noQuotes?"'":"")}}}}}return(!noQuotes?"'":"")+(itemPrefix?itemPrefix:"")+name+(!noQuotes?"'":"")};$scope.buildNameList=function(list,suffix,tag,className,possessive,noQuotes,pedantic,itemPrefix){var cnt=1;var result="";for(i in list){var an="";var it=list[i];if(it instanceof Object){tag=it.t?it.t:tag;an=it.a?it.a:"[unknown]";it=it.n}var item=$scope.buildName(it,noQuotes,pedantic,itemPrefix);result+=""+item+""+(possessive?"'"+(item.substr(-1)=="s"?"":"s"):"")+(cnt"+(cnt==list.length-1?(list.length>2?", ":" ")+suffix+" ":", ")+"":"");cnt++}return result.trim()};$scope.buildLocationModeNameList=function(modes){var modeNames=[];if(modes instanceof Array){for(modeIndex in modes){modeNames.push($scope.getModeName(modes[modeIndex]))}if(modeNames.length){return $scope.buildNameList(modeNames,"or","lit","",false,true)}}return""};$scope.buildDeviceNameList=function(devices){var deviceNames=[];if(devices instanceof Array){for(deviceIndex in devices){var device=$scope.getDeviceById(devices[deviceIndex]);if(device){deviceNames.push({n:device.n,a:device.an,t:"dev"})}else{deviceNames.push({n:"{"+devices[deviceIndex]+"}",t:"var"})}}if(deviceNames.length){return $scope.buildNameList(deviceNames,"and","dev","",false,true)}}return"Location"};$scope.buildContactNameList=function(contacts){var contactNames=[];if(contacts instanceof Array){for(contactIndex in contacts){var contact=$scope.getContactById(contacts[contactIndex]);if(contact){contactNames.push({n:(contact.f+" "+contact.l).trim()+" ("+contact.t+"/"+(contact.p?"PUSH":"SMS")+")",a:contact.an,t:"cnt"})}else{contactNames.push({n:"{"+contacts[contactIndex]+"}",a:"Unknown Contact",t:"var"})}}if(contactNames.length){return $scope.buildNameList(contactNames,"and","cnt","",false,true)}}return"(empty)"};$scope.formatHour=function(hour){return(!location.timeZone||location.timeZone.id.startsWith("America"))?((hour%12?hour%12:"12")+(hour<12?"am":"pm")):("00"+hour).substr(-2)};$scope.renderDeviceNameList=function(devices){return $sce.trustAsHtml($scope.buildDeviceNameList(devices))};$scope.renderContactNameList=function(contacts){return $sce.trustAsHtml($scope.buildContactNameList(contacts))};$scope.hasCommand=function(device,commandName){if(!device||!device.c){return false}return $scope.hasName(device.c,commandName)};$scope.hasName=function(arrayOfObjects,name){if(!arrayOfObjects||!arrayOfObjects.length){return false}for(obj in arrayOfObjects){if(arrayOfObjects[obj]&&(arrayOfObjects[obj].n===name)){return true}}return false};$scope.hasId=function(arrayOfObjects,id){if(!arrayOfObjects||!arrayOfObjects.length){return false}for(obj in arrayOfObjects){if(arrayOfObjects[obj]&&(arrayOfObjects[obj].id===id)){return true}}return false};$scope.listAvailableCommands=function(devices){var commands={};var deviceCount=devices?devices.length:0;for(deviceIndex in devices){var deviceId=devices[deviceIndex]||"";var cmds=[];var all=false;if(deviceId.startsWith(":")){var device=$scope.getDeviceById(devices[deviceIndex]);if(device){cmds=device.c}}else{all=true;cmds=$scope.db.commands.physical}for(commandIndex in cmds){var commandName=all?commandIndex:cmds[commandIndex].n;if(commands[commandName]){commands[commandName]+=1}else{commands[commandName]=1}}}var result={common:[],partial:[],virtual:[]};for(commandName in commands){var command=$scope.db.commands.physical[commandName];if(!command){command={n:commandName+"(..)",cm:true}}if(commands[commandName]==deviceCount){result.common.push(mergeObjects({id:commandName},command))}else{result.partial.push(mergeObjects({id:commandName},command))}}for(commandName in $scope.db.commands.virtual){var command=$scope.db.commands.virtual[commandName];if(command.r){var count=0;for(deviceIndex in devices){var deviceId=devices[deviceIndex]||"";var ok=false;if(deviceId.startsWith(":")){var device=$scope.getDeviceById(devices[deviceIndex]);ok=!!device;if(ok){for(req in command.r){if(!$scope.hasCommand(device,command.r[req])){ok=false;break}}}}else{ok=true}if(ok){count++}}if(count>0){if(count==deviceCount){if(!$scope.hasId(result.common,commandName)){result.common.push(mergeObjects({id:commandName,em:true},command))}}else{if(!$scope.hasId(result.partial,commandName)){result.partial.push(mergeObjects({id:commandName,em:true},command))}}}}else{result.virtual.push(mergeObjects({id:commandName},command))}}result.common.sort($scope.sortByName);result.partial.sort($scope.sortByName);result.virtual.sort($scope.sortByName);return result};$scope.listAvailableDevices=function(){var result=[];for(deviceIndex in $scope.instance.devices){var device=$scope.instance.devices[deviceIndex];var tokens="";for(i in device.a){tokens+=":"+device.a[i].n+" "}result.push(mergeObjects({id:deviceIndex,tokens:tokens+device.n},device))}return result.sort($scope.sortByName)};$scope.listAvailableVirtualDevices=function(){var result=[];for(deviceIndex in $scope.instance.virtualDevices){var device=$scope.instance.virtualDevices[deviceIndex];result.push(mergeObjects({id:deviceIndex},device))}return result.sort($scope.sortByName)};$scope.escapeRegExp=function(str){return str};$scope.listAutoCompleteFunctions=function(){var result=[];for(functionIndex in $scope.db.functions){result.push(($scope.db.functions[functionIndex].d?$scope.db.functions[functionIndex].d:functionIndex)+"(")}return result.sort()};$scope.listAutoCompleteDevices=function(){var result=[];for(deviceIndex in $scope.instance.devices){var device=$scope.instance.devices[deviceIndex];result.push($scope.escapeRegExp("["+device.n+" :"))}return result.sort()};$scope.listAutoCompleteVariables=function(){var result=[];for(varIndex in $scope.piston.v){var v=$scope.piston.v[varIndex];result.push($scope.escapeRegExp(v.n))}if($scope.systemVars){for(varName in $scope.systemVars){result.push($scope.escapeRegExp(varName))}}if($scope.globalVars){for(varName in $scope.globalVars){result.push($scope.escapeRegExp(varName))}}return result.sort()};$scope.getVariableByName=function(name){if($scope.systemVars&&$scope.systemVars[name]){return $scope.systemVars[name]}if($scope.globalVars&&$scope.globalVars[name]){return $scope.globalVars[name]}for(varIndex in $scope.piston.v){if($scope.piston.v[varIndex].n==name){return $scope.piston.v[varIndex]}}return null};$scope.getVariableValue=function(name,dt){if($scope.localVars){var variable=$scope.localVars[name];if(variable!=undefined){if(dt=="datetime"){return utcToString(variable)}return""+variable}}return"(not set)"};$scope.autoAddVariable=function(name){if(!name){return false}name=name?name.trim():"";var v=$scope.getVariableByName(name);$scope.piston.v.push({t:"dynamic",n:name})};$scope.hasAttribute=function(device,attributeName){for(a in device.a){if(device.a[a].n==attributeName){return true}}return false};$scope.listAvailableAttributeNames=function(devices,restrictAttribute){var result=[];var list=$scope.listAvailableAttributes(devices,restrictAttribute);for(i in list){if(list[i].n!=statusAttribute){result.push({n:list[i].n,v:list[i].id})}}return result};$scope.listAvailableAttributes=function(devices,restrictAttribute){var result=[];var device=null;if(devices&&devices.length){var attributes={};var deviceCount=devices.length;var hasThreeAxis=false;for(deviceIndex in devices){device=$scope.getDeviceById(devices[deviceIndex]);if(device){for(attributeIndex in device.a){var attribute=device.a[attributeIndex];if(!restrictAttribute||(attribute.n==restrictAttribute)){if(attributes[attribute.n]){attributes[attribute.n]+=1}else{attributes[attribute.n]=1}}}}else{for(attributeName in $scope.db.attributes){if(!restrictAttribute||(attributeName==restrictAttribute)){if(attributes[attributeName]){attributes[attributeName]+=1}else{attributes[attributeName]=1}}}}}for(attributeId in attributes){if(attributes[attributeId]==deviceCount){var attribute=$scope.getAttributeById(attributeId);if(attribute){result.push(mergeObjects({id:attributeId},attribute));if(attributeId=="threeAxis"){hasThreeAxis=true}}else{for(a in device.a){if(device.a[a].n==attributeId){attribute=device.a[a];break}}if(attribute){var obj=mergeObjects({id:attributeId,c:true},attribute);obj.n="⌂ "+obj.n;obj.t=(obj.t||"string").toLowerCase().replace("number","decimal");result.push(obj)}}}}if(hasThreeAxis){result.push({id:"axisX",n:"X axis",t:"decimal"});result.push({id:"axisY",n:"Y axis",t:"decimal"});result.push({id:"axisZ",n:"Z axis",t:"decimal"});result.push({id:"orientation",n:"orientation",t:"string"})}result.push({id:statusAttribute,n:"⌂ "+statusAttribute,t:"string"});result.sort($scope.sortByName)}return result};$scope.sortByDisplay=function(a,b){return(a.d>b.d)?1:((b.d>a.d)?-1:0)};$scope.sortByName=function(a,b){a=a.n.toLowerCase();b=b.n.toLowerCase();return(a>b)?1:((b>a)?-1:0)};$scope.getStackData=function(){var data=angular.toJson($scope.compilePiston($scope.piston));return{hash:$scope.md5(data),timestamp:(new Date()).getTime(),data:angular.fromJson(data)}};$scope.autoSave=function(stack){var clearRedo=stack?false:true;stack=stack?stack:$scope.stack.undo;pushToStack=true;var obj=$scope.getStackData();if(stack&&stack.length){if(obj.hash==stack[stack.length-1].hash){pushToStack=false}}if(pushToStack){stack.push(obj);if(stack.length>MAX_STACK_SIZE){stack=stack.slice(-MAX_STACK_SIZE)}}if(clearRedo){$scope.stack.redo=[]}};$scope.objectToArray=function(object){var result=[];for(property in object){result.push({v:property,n:object[property]})}return result};$scope.saveStack=function(justSaved){$scope.stack.current=$scope.getStackData();if(justSaved){$scope.stack.current.timestamp=0}$scope.stack.build=$scope.meta.build;dataService.saveToStore("stack"+$scope.pistonId,$scope.stack)};$scope.loadStack=function(){$scope.stack=dataService.loadFromStore("stack"+$scope.pistonId);$scope.stack=$scope.stack instanceof Object?$scope.stack:{};$scope.stack.undo=($scope.stack.undo instanceof Array?$scope.stack.undo:[]);$scope.stack.redo=($scope.stack.redo instanceof Array?$scope.stack.redo:[]);if($scope.stack.current instanceof Object&&$scope.stack.current.data&&($scope.stack.build==$scope.meta.build)&&($scope.meta.modified<$scope.stack.current.timestamp)){$scope.setStatus();$scope.dialogChooseVersion()}else{}};$scope.dialogChooseVersion=function(){$scope.designer.dialog=ngDialog.open({template:"dialog-choose-version",className:"ngdialog-theme-default ngdialog-large",closeByDocument:false,disableAnimation:true,scope:$scope})};$scope.getLocalVariable=function(name){for(i in $scope.piston.v){if($scope.piston.v[i].n==name){return $scope.piston.v[i]}}return null};$scope.getLocalVariableType=function(name){for(i in $scope.piston.v){if($scope.piston.v[i].n==name){return $scope.piston.v[i].t}}return""};$scope.chooseVersion=function(keepLocal){if(keepLocal){$scope.piston=$scope.stack.current.data;$scope.validatePiston($scope.piston)}else{$scope.autoSave()}$scope.closeDialog()};$scope.undo=function(){if($scope.stack&&$scope.stack.undo&&$scope.stack.undo.length){$scope.autoSave($scope.stack.redo);$scope.stack.current=$scope.stack.undo.pop();$scope.piston=$scope.stack.current.data;$scope.validatePiston($scope.piston);$scope.saveStack()}};$scope.redo=function(){if($scope.stack&&$scope.stack.redo&&$scope.stack.redo.length){$scope.autoSave($scope.stack.undo);$scope.stack.current=$scope.stack.redo.pop();$scope.piston=$scope.stack.current.data;$scope.validatePiston($scope.piston);$scope.saveStack()}};$scope.localTimeToDate=function(time){time=time?time:0;var today=new Date();today.setHours(Math.floor(time/60));today.setMinutes(time%60);today.setSeconds(0);today.setMilliseconds(0);return today};$scope.validateOperand=function(operand,reinit,managed){if(!!$scope.designer.comparison&&!managed){$scope.validateComparison($scope.designer.comparison,reinit);return}operand=operand||{};if(!operand.initialized||reinit){operand.data=operand.data||{};operand.data.a=operand.data.a||"";operand.data.c=(operand.data.c==undefined)||(operand.data.c==null)?"":operand.data.c;operand.data.v=operand.data.v||"";operand.data.e=operand.data.e||"";operand.data.x=operand.data.x||"";operand.data.d=operand.data.d||[];operand.data.g=operand.data.g||(operand.multiple?"any":"avg");operand.data.f=operand.data.f||"l";operand.options=operand.options||[]}if(true||!operand.initialized||reinit){var dataType=(operand.dataType||"string").toLowerCase();if(dataType=="variables"){operand.multiple=true;dataType="variable"}if(dataType=="devices"){operand.multiple=true;dataType="device"}if(dataType=="pistons"){operand.multiple=true;dataType="piston"}if(dataType=="routines"){operand.multiple=true;dataType="routine"}if(dataType=="attributes"){operand.multiple=true;dataType="attribute"}if(dataType=="modes"){operand.multiple=true;dataType="mode"}if(dataType=="alarmsystemstatus"){dataType="alarmSystemStatus"}if(dataType=="alarmsystemstatuses"){operand.multiple=true;dataType="alarmSystemStatus"}if(dataType=="modes"){operand.multiple=true;dataType="mode"}if(dataType=="enums"){operand.multiple=true;dataType="enum"}if(dataType=="lifxScenes"){operand.multiple=true;dataType="lifxScene"}if(dataType=="lifxscene"){dataType="lifxScene"}if(dataType=="lifxselector"){dataType="lifxSelector"}if(dataType=="contacts"){operand.multiple=true;dataType="contact"}if(dataType=="number"){dataType="decimal"}if(dataType=="bool"){dataType="boolean"}if((dataType=="enum")&&!operand.options&&!operand.options.length){dataType="string"}switch(operand.data.vt){case"time":if(!(operand.data.c instanceof Date)){if(operand.data.c!=undefined){operand.data.c=$scope.localTimeToDate(operand.data.c)}}break;case"date":case"datetime":if(!(operand.data.c instanceof Date)){operand.data.c=new Date(operand.data.c);if(operand.data.c=="Invalid Date"){operand.data.c=new Date()}}break}var disableExpressions=(!!operand.disableExpressions)||(dataType=="piston")||(dataType=="routine")||(dataType=="askAlexaMacro")||(dataType=="attribute");operand.onlyAllowConstants=operand.onlyAllowConstants||disableExpressions;var strict=!!operand.strict;if(operand.onlyAllowConstants||(dataType=="contact")){operand.allowArgument=false;operand.allowDevices=(dataType=="device");operand.allowPhysical=false;operand.allowVirtual=false;operand.allowConstant=(dataType!="device");operand.allowVariable=false;operand.allowExpression=!disableExpressions}else{operand.allowDevices=dataType=="device";operand.allowPhysical=(dataType!="datetime")&&(dataType!="date")&&(dataType!="time")&&(dataType!="device")&&(dataType!="variable")&&(!strict||(dataType!="boolean"))&&(dataType!="duration");operand.allowPreset=(!operand.event)&&(dataType=="datetime")||(dataType=="time")||(dataType=="color");operand.allowVirtual=(dataType!="datetime")&&(dataType!="date")&&(dataType!="time")&&(dataType!="device")&&(dataType!="variable")&&(dataType!="decimal")&&(dataType!="integer")&&(dataType!="number")&&(dataType!="boolean")&&(dataType!="enum")&&(dataType!="color")&&(dataType!="duration");operand.allowVariable=(dataType!="device"||((dataType=="device")&&operand.multiple))&&(!strict||(dataType!="boolean"));operand.allowConstant=(!operand.event)&&(dataType!="device")&&(dataType!="variable");operand.allowArgument=(!operand.event)&&(dataType!="device")&&(dataType!="variable");operand.allowExpression=(!operand.event)&&(dataType!="variable")&&(!strict||(dataType!="boolean"))}if(operand.data.t==null){var t="";if(!operand.optional){t=dataType=="variable"?"x":(!!operand.allowPreset?"s":"c");if(($scope.designer.$condition)||($scope.designer.$restriction)){t="p"}}operand.data.t=t}if(((operand.data.t=="p")&&(!operand.allowPhysical))||((operand.data.t=="v")&&(!operand.allowVirtual))){operand.data.t=(!!operand.allowPreset)?"s":"c"}if(!operand.config){operand.config=$scope.copy($scope.getExpressionConfig());operand.config.autocomplete[5].words=[/([0-9]+)(\.[0-9]+)?/g]}operand.restrictAttribute=null;operand.restrictType=null;switch(dataType){case"color":operand.restrictAttribute="color";break;case"device":operand.restrictType="device";break;case"integer":operand.restrictType="integer,integer[]";break;case"decimal":operand.restrictType="integer,integer[],decimal,decimal[]";break;case"time":operand.restrictType="datetime,datetime[],time,time[]";break;case"date":case"datetime":operand.restrictType="datetime,datetime[],date,date[]";break}operand.dataType=dataType;operand.durationUnit=operand.durationUnit||operand.data.vt||"s";operand.data.vt=dataType=="duration"?operand.durationUnit:dataType}switch(dataType){case"enum":break;case"bool":case"boolean":operand.options=["false","true"];break;case"mode":case"powerSource":case"alarmSystemStatus":case"routine":operand.options=$scope.objectToArray($scope.instance.virtualDevices[dataType].o);break;case"attribute":operand.attrs=operand.attrs?operand.attrs:$scope.listAvailableAttributeNames($scope.designer.parent.d);operand.options=operand.attrs;break;case"piston":operand.options=$scope.listAllPistons();break;case"contact":operand.options=$scope.contacts;break;case"lifxScene":operand.options=$scope.objectToArray($scope.instance.lifx.scenes).sort($scope.sortByName);break;case"lifxSelector":operand.options=[];break;case"integer":case"decimal":case"duration":if((operand.data.t=="c")&&(isNaN(operand.data.c)||(operand.data.c==""))){operand.data.c=0}default:operand.options=null}if((!operand.multiple)&&(operand.options)&&(operand.options.length)&&(operand.data.t=="c")){if(operand.options[0] instanceof Object){var found=false;for(i in operand.options){if(operand.options[i].v==operand.data.c){found=true;break}}if(!found){operand.data.c=operand.options[0].v}}else{if(operand.options.indexOf(operand.data.c)<0){operand.data.c=operand.options[0]}}}operand.initialized=true;operand.allowAggregation=true;operand.allowAll=true;operand.attributes=(operand.data.t=="p")?$scope.listAvailableAttributes(operand.data.d,operand.restrictAttribute):[];operand.valid=false;operand.selectedMultiple=(operand.data.t=="p")&&operand.data.d&&(operand.data.d.length>1)&&((operand.data.g=="all")||(operand.data.g=="any"));operand.error=null;operand.momentary=false;operand.selectedDataType="string";operand.selectedOptions=[];switch(operand.data.t){case"p":var attribute=$scope.db.attributes[operand.data.a];if(!attribute){if(operand.data.d.length){var device=$scope.getDeviceById(operand.data.d[0]);if(device){for(a in device.a){if(device.a[a].n==operand.data.a){attribute=device.a[a];break}}}}}operand.count=0;if(attribute){operand.momentary=attribute.m||(!!attribute.s&&(operand.data.i instanceof Array)&&operand.data.i.length);operand.interactive=!!attribute.p;if(operand.interactive&&!operand.data.p){operand.data.p="a"}if(!!attribute.s){operand.subDeviceName=attribute.sd;var countAttributes=attribute.s.split(",");for(deviceIndex in operand.data.d){var dev=$scope.getDeviceById(operand.data.d[deviceIndex]);var c=0;if(dev){for(i in countAttributes){var attr=$scope.getDeviceAttributeById(dev,countAttributes[i]);if((attr)&&(!isNaN(attr.v))){c=parseInt(attr.v);if(c>operand.count){operand.count=c}}}}}if(operand.count==0){operand.count=32}}else{operand.subDeviceName=""}if(operand.count){if((operand.data.i==null)||(operand.data.i==undefined)){operand.data.i=[]}$scope.refreshSelects()}operand.selectedDataType=attribute.t.toLowerCase();operand.selectedOptions=attribute.o;if(operand.momentary){operand.allowAll=false;operand.allowAggregation=false}}else{operand.selectedDataType="string"}if(operand.data.d&&(operand.data.d.length>1)){if(!operand.data.g){operand.error="Invalid aggregation method"}if(!(["any","all","least","most"].indexOf(operand.data.g)>=0)){operand.selectedDataType="decimal"}}break;case"v":var virtualDevice=$scope.instance.virtualDevices[operand.data.v];operand.selectedDataType=(!!virtualDevice&&!!virtualDevice.t)?virtualDevice.t:"string";if(virtualDevice){operand.momentary=virtualDevice.m;for(o in virtualDevice.o){operand.selectedOptions.push({v:o,n:virtualDevice.o[o]})}}break;case"x":if(operand.data.x instanceof Array){if(operand.data.x.length){operand.selectedDataType="dynamic"}else{operand.error="Invalid list of variables"}}else{var variable=$scope.getVariableByName(operand.data.x);if(variable){operand.selectedDataType=variable.t;if(operand.selectedDataType=="boolean"){operand.selectedOptions=["false","true"]}}else{operand.error="Invalid variable"}}break;case"u":operand.selectedDataType="dynamic";break;case"c":var expression=$scope.parseString(operand.data.c,operand.data.vt);operand.error=expression.err;operand.expressionVar=expression.errVar;operand.data.exp=expression;if(!operand.options){if(!operand.optional&&!operand.data.c&&(operand.requirePositiveNumber)){operand.error="Empty value";operand.expressionVar=""}}else{if((operand.data.c==null)||(operand.data.c==undefined)){operand.error="Invalid selection";operand.expressionVar=""}}operand.selectedDataType=operand.dataType;break;case"e":var expression=$scope.parseExpression(operand.data.e,false,operand.data.vt);operand.error=expression.err;operand.expressionVar=expression.errVar;if(expression.err){var loc=(expression.loc?expression.loc:"0:"+(expression.str.length-1).toString()).split(":");var start=parseInt(loc[0]);var end=loc.length==2?parseInt(loc[1]):start;operand.config.autocomplete[0]={words:[new RegExp(".(?=.{"+(expression.str.length-start-1)+"}$).{"+(end-start)+"}")],cssClass:"hl err",title:expression.err}}else{operand.config.autocomplete[0]={words:[],cssClass:"hl err"}}operand.data.exp=expression;$scope.delayEvaluation(operand);operand.selectedDataType="dynamic";break}if((!operand.error)&&(operand.dataType=="duration")&&(!operand.durationUnit)){operand.error="Invalid duration unit"}operand.valid=(!operand.error)&&(((operand.data.t=="")&&(operand.optional))||((operand.data.t=="d")&&!!operand.data.d&&!!operand.data.d.length)||((operand.data.t=="p")&&!!operand.data.d&&!!operand.data.d.length&&!!operand.data.a)||((operand.data.t=="v")&&!!operand.data.v)||((operand.data.t=="x")&&!!operand.data.x&&!!operand.data.x.length)||((operand.data.t=="s")&&!!operand.data.s)||((operand.data.t=="u")&&!!operand.data.u)||((operand.data.t=="c")&&!((operand.data.c=="Invalid Date")&&(operand.data.c instanceof Object))&&!((dataType=="duration")&&(isNaN(operand.data.c)||(operand.requirePositiveNumber&&(operand.data.c<1)))))||((operand.data.t=="e")&&!!operand.data.e&&!!operand.data.e.length));switch(operand.dataType){case"integer":operand.inputType="number";try{operand.data.c=parseInt(operand.data.c)}catch(all){operand.data.c=0}break;case"duration":case"decimal":operand.inputType="number";try{operand.data.c=parseFloat(operand.data.c)}catch(all){operand.data.c=0}break;default:operand.inputType=operand.dataType}if(operand.linkedOperand){operand.linkedOperand.dataType=operand.selectedDataType;operand.linkedOperand.options=operand.selectedOptions;$scope.validateOperand(operand.linkedOperand,true);$scope.refreshSelects()}};$scope.refreshSelects=function(type){if(type){$scope.$$postDigest(function(){$("select["+type+"]").selectpicker("refresh");$timeout(function(){$("select["+type+"]").selectpicker("refresh")},0,false)})}else{$scope.$$postDigest(function(){$("select[selectpicker]").selectpicker("refresh");$timeout(function(){$("select[selectpicker]").selectpicker("refresh")},0,false)})}};$scope.getOrdinalSuffix=function(value){if(isNaN(value)){return""}value=parseInt(value);var value100=value%100;var value10=value%10;if(((value100>3)&&(value100<21))||(value10==0)||(value10>3)){return"th"}switch(value10){case 1:return"st";case 2:return"nd";case 3:return"rd"}return"th"};$scope.getOrdinal=function(value){if(isNaN(value)){return""}value=parseInt(value);switch(value){case -3:return"third-last";case -2:return"second-last";case -1:return"last"}return value+$scope.getOrdinalSuffix(value)};$scope.listODM=function(){var result=$scope.designer.odm;var sz=(!$scope.designer.operand.data.odw||($scope.designer.operand.data.odw=="d"))?31:5;if(!result||(result.length!=(sz+3))){result=[];for(i=1;i<=sz;i++){result.push({v:i,n:i+$scope.getOrdinalSuffix(i)})}result.push({v:-1,n:"last"});result.push({v:-2,n:"second-last"});result.push({v:-3,n:"third-last"});$scope.designer.odm=result}return result};$scope.listODW=function(){var result=$scope.designer.odw;var sz=($scope.designer.operand.data.odm>5)?0:7;if(!result||(result.length!=(sz+1))){result=[];result.push({v:"d",n:"day"});if(sz){for(i in $scope.weekDays){result.push({v:i.toString(),n:$scope.weekDays[i]})}}$scope.designer.odw=result}return result};$scope.validateComparison=function(comparison,reinit){$scope.validateOperand(comparison.left,reinit,true);if((comparison.left.selectedDataType!=comparison.dataType)||(comparison.left.selectedMultiple!=comparison.selectedMultiple)||(comparison.left.momentary!=comparison.momentary)||(comparison.left.data.t=="v")||(comparison.selectedInteractive!=comparison.left.data.p)){comparison.dataType=comparison.left.selectedDataType;comparison.selectedMultiple=comparison.left.selectedMultiple;comparison.selectedInteractive=comparison.left.data.p;comparison.momentary=comparison.left.momentary;var disableTimedConditions=(comparison.left.data.t!="p")||((comparison.left.data.g!="any")&&(comparison.left.data.g!="all"));var disableConditions=(comparison.left.interactive&&((comparison.left.data.p=="p")||(comparison.left.data.p=="s")));var disableTimedTriggers=disableConditions;var disableTriggers=(comparison.type=="restriction");var optionList=[];var options=[];if(!comparison.dataType){comparison.dataType="dynamic"}switch(comparison.dataType){case"color":case"hexcolor":case"object":case"vector3":case"enum":dt="s";break;case"image":dt="f";break;case"dynamic":dt="";break;case"time":case"date":case"datetime":dt="t";break;default:dt=comparison.dataType.substr(0,1)}dt=(comparison.momentary&&(dt!="e")?(comparison.left.data.t=="v"?"v":"m"):((dt=="n"?"d":dt)));if(!disableConditions){for(conditionId in $scope.db.comparisons.conditions){var condition=$scope.db.comparisons.conditions[conditionId];if(((!dt&&(condition.g!="m"))||(condition.g.indexOf(dt)>=0))&&(!disableTimedConditions||!condition.t)){options.push({id:conditionId,d:(comparison.selectedMultiple?(condition.dd?condition.dd:condition.d):condition.d),c:"Conditions"})}}optionList=optionList.concat(options.sort($scope.sortByDisplay))}if(!disableTriggers){options=[];for(triggerId in $scope.db.comparisons.triggers){var trigger=$scope.db.comparisons.triggers[triggerId];if((trigger.g.indexOf(dt)>=0)&&(!disableTimedTriggers||!trigger.t)){options.push({id:triggerId,d:(comparison.selectedMultiple?(trigger.dd?trigger.dd:trigger.d):trigger.d),c:"Triggers"})}}optionList=optionList.concat(options.sort($scope.sortByDisplay))}comparison.options=optionList;if(comparison.options.length==1){comparison.operator=comparison.options[0].id}}var comp=$scope.db.comparisons.conditions[comparison.operator]||$scope.db.comparisons.triggers[comparison.operator];comparison.operatorValid=!!comp;comparison.parameterCount=comp&&comp.p?comp.p:0;comparison.multiple=comp&&comp.m?true:false;comparison.valid=comparison.left.valid&&comparison.operatorValid;comparison.timed=comp?comp.t:0;if((comparison.parameterCount>0)||(comparison.dataType=="email")){comparison.right.multiple=comparison.multiple;comparison.right.disableAggregation=comparison.multiple;comparison.right.dataType=(comparison.dataType=="email"?"string":comparison.left.selectedDataType);if(angular.toJson(comparison.right.options)!=angular.toJson(comparison.left.selectedOptions)){if((comparison.right.data.t=="c")&&comparison.right.options&&comparison.right.options.length&&(!comparison.left.selectedOptions||!comparison.left.selectedOptions.left)){comparison.right.data.c=""}comparison.right.options=comparison.left.selectedOptions}$scope.validateOperand(comparison.right,reinit,true);comparison.valid=comparison.valid&&comparison.right.valid}if((comparison.parameterCount>1)||(comparison.dataType=="email")){comparison.right2.multiple=comparison.multiple;comparison.right2.disableAggregation=comparison.multiple;comparison.right2.dataType=(comparison.dataType=="email"?"string":comparison.left.selectedDataType);if(angular.toJson(comparison.right2.options)!=angular.toJson(comparison.left.selectedOptions)){comparison.right2.options=comparison.left.selectedOptions}$scope.validateOperand(comparison.right2,reinit,true);comparison.valid=comparison.valid&&comparison.right2.valid}var usingTime=(comparison.timed>0);var usingTime2=false;if(comparison.left.selectedDataType=="time"){usingTime=usingTime||(comparison.right.data.t!="c");usingTime2=(comparison.right2.data.t!="c")}if(usingTime){comparison.time.requirePositiveNumber=!!comparison.timed;$scope.validateOperand(comparison.time,reinit,true);comparison.valid=comparison.valid&&comparison.time.valid}if(usingTime2){comparison.time2.requirePositiveNumber=false;comparison.time2.dataType="duration";$scope.validateOperand(comparison.time2,reinit,true);comparison.valid=comparison.valid&&comparison.time2.valid}if(comparison.followedBy){comparison.within.requirePositiveNumber=false;comparison.within.dataType="duration";$scope.validateOperand(comparison.within,reinit,true);comparison.valid=comparison.valid&&comparison.within.valid}};$scope.detectDataType=function(value){switch(typeof value){case"string":if(!isNaN(parseFloat(value))){return"number"}return"string";case"number":return"number";default:return"string"}};$scope.renderOperand=function(operand,noQuotes,pedantic,noNegatives,grouping){var result="";if(operand){switch(operand.t){case"d":if(operand.d){result=$scope.buildDeviceNameList(operand.d)}break;case"p":if(operand.d&&operand.a){result=$scope.renderDeviceList(operand.d,operand.a,operand.g,true)+" "+operand.a+""}break;case"v":var device=$scope.getVirtualDeviceById(operand.v);result=""+(device?device.n:"(invalid virtual device)")+"";break;case"s":if(operand.s){result=""+operand.s+""}break;case"x":if(operand.x){result="{"+operand.x+($scope.getLocalVariableType(operand.x).endsWith("]")?"["+operand.xi+"]":"")+"}"}break;case"c":var m="num";noQuotes=noQuotes||!isNaN(operand.c);switch(operand.vt){case"time":var date=$scope.localTimeToDate(operand.c);result=""+date.toLocaleTimeString({hour:"2-digit",minute:"2-digit"})+"";break;case"date":result=""+utcToDateString(operand.c)+"";break;case"datetime":result=""+utcToString(operand.c)+"";break;case"email":result=""+operand.c+"";break;case"piston":result=""+$scope.getPistonName(operand.c)+"";break;case"lifxScene":result=""+$scope.getLifxSceneName(operand.c)+"";break;case"lifxSelector":result=""+$scope.getLifxSelectorName(operand.c)+"";break;case"phone":result=""+operand.c+"";break;case"uri":result=""+operand.c+"";break;case"contact":result=$scope.renderContactNameList(operand.c);break;default:if(!noQuotes){if((operand.vt=="boolean")||(operand.vt=="enum")){noQuotes=true}m="lit"}var c=operand.c;if(noNegatives&&!isNaN(c)&&parseInt(c)<0){c=-parseInt(c)}result=""+scope.buildName(c,noQuotes,pedantic,null,grouping)+""}break;case"u":result=result+"{$args."+operand.u+"}";break;case"e":if(operand.e){result="{"+operand.e+"}"}break}}result=result?result:'(empty)';return(result instanceof Object)?result:$sce.trustAsHtml(result)};$scope.renderForOperands=function(statement){var result;result=""+(statement.x?statement.x:"$index")+" = "+$scope.renderOperand(statement.lo)+" to "+$scope.renderOperand(statement.lo2)+" step "+$scope.renderOperand(statement.lo3);return $sce.trustAsHtml(result?result:"(invalid operands)")};$scope.renderForEachOperands=function(statement){var result;result=""+(statement.x?statement.x:"$device")+" in "+$scope.renderOperand(statement.lo);return $sce.trustAsHtml(result?result:"(invalid operands)")};$scope.renderTimeOperand=function(to){if(!to){return""}var isConstant=(to.t=="c");var constantValue=isConstant&&!isNaN(to.c)?parseInt(to.c):0;if(isConstant&&(constantValue==0)){return""}return $scope.renderOperand(to,false,false,true)+" "+$scope.getDurationUnitName(to.vt,(constantValue!=1))+" "+(constantValue<0?"to":"past")+" "};$scope.renderComparison=function(l,o,r,r2,to,to2){var comparison=$scope.db.comparisons.triggers[o];var trigger=!!comparison;if(!comparison){comparison=$scope.db.comparisons.conditions[o]}if(!comparison){return"[ERROR: Invalid comparison]"}var pedantic=l.t=="v";var plural=l&&(l.t=="p")&&l.d&&(l.d.length>1)&&(l.g=="all");var noQuotes=false;var unit="";var a=null;switch(l.t){case"v":switch(l.v){case"locationMode":case"shmState":noQuotes=true;break}break;case"p":a=$scope.getAttributeById(l.a);if(!!a&&!!a.u){unit=a.u}if(unit=="°?"){unit="°"+($scope.location.temperatureScale?$scope.location.temperatureScale:"")}break}var indexes="";if(!!a&&!!a.s&&(l.i instanceof Array)&&l.i.length){indexes=" "+$scope.buildNameList(l.i,"or",null,null,false,true,false,"#")+""}if(!!a&&!!a.p){switch(l.p){case"p":indexes+=" physically";break;case"s":indexes+=" programmatically";break}}var offset1="";var offset2="";if((l.t=="v")&&(l.v=="time")){if(r&&to&&(r.t!="c")){offset1=$scope.renderTimeOperand(to)}if(r2&&to2&&(r2.t!="c")){offset2=$scope.renderTimeOperand(to2)}}var result=$scope.renderOperand(l)+indexes+" "+(plural?(comparison.dd?comparison.dd:comparison.d):comparison.d)+""+(comparison.p>0?" "+offset1+$scope.renderOperand(r,noQuotes,pedantic)+(unit?""+unit+" ":""):"")+(comparison.p>1?" "+(comparison.d.indexOf("between")?"and":"through")+" "+offset2+$scope.renderOperand(r2,noQuotes,pedantic)+(unit?""+unit+" ":""):"");switch(comparison.t){case 1:result+=" "+(trigger?"for":"in the last")+" "+$scope.renderOperand(to)+" "+$scope.getDurationUnitName(to.vt,!((to.t=="c")&&(!isNaN(to.c))&&(parseInt(to.c)==1)))+"";break;case 2:result+=" for "+(to.f=="g"?"at least":"less than")+" "+$scope.renderOperand(to)+" "+$scope.getDurationUnitName(to.vt,!((to.t=="c")&&(!isNaN(to.c))&&(parseInt(to.c)==1)))+"";break}if((l.t=="v")&&(["time","date","datetime"].indexOf(l.v)>=0)){var odw=(l.odw instanceof Array)&&l.odw.length?l.odw:null;var odm=(l.odm instanceof Array)&&l.odm.length?l.odm:null;var owm=!odm&&(l.owm instanceof Array)&&l.owm.length?l.owm:null;var omy=(l.omy instanceof Array)&&l.omy.length?l.omy:null;if(!!odw||!!odm||!!owm||!!omy){var rCount=0;result+=", but only";var odwString="";if(odw){for(i in odw){if((i>0)&&(odw.length>2)){odwString+=", "}if((i>0)&&(i==odw.length-1)){odwString+=" or "}odwString+=""+$scope.weekDays[odw[i]]+"s"}rCount++}if(owm){result+=(rCount?",":"")+" on the ";for(i in owm){if((i>0)&&(owm.length>2)){result+=", "}if((i>0)&&(i==owm.length-1)){result+=" or "}result+=""+$scope.getOrdinal(owm[i])+""}result+=" "+(odwString?odwString:"week"+(owm.length>1?"s":"")+"")+(omy?"":" of the month");rCount++}else{if(odwString){result+=(rCount>1?",":"")+" on "+odwString}}if(odm){result+=(rCount?",":"")+" on the ";for(i in odm){if((i>0)&&(odm.length>2)){result+=", "}if((i>0)&&(i==odm.length-1)){result+=" or "}result+=""+$scope.getOrdinal(odm[i])+""}result+=" day"+(odm.length>1?"s":"")+(omy?"":" of the month");rCount++}if(omy){result+=" "+(owm||odm?"of":"in")+" ";for(i in omy){if((i>0)&&(omy.length>2)){result+=", "}if((i>0)&&(i==omy.length-1)){result+=" or "}result+=""+$scope.yearMonths[omy[i]-1]+""}rCount++}}}return $sce.trustAsHtml(result)};$scope.renderGroupingMethod=function(collection,item){var result=collection.o;if((collection.c instanceof Array)&&(result=="followed by")){var idx=collection.c.indexOf(item)+1;if(idx"+$scope.getDurationUnitName(it.wd.vt,!((it.wd.t=="c")&&(!isNaN(it.wd.c))&&(parseInt(it.wd.c)==1)))+" by"}}return $sce.trustAsHtml(result)};$scope.renderGroupWithin=function(collection,group){var list=collection instanceof Array?collection:collection.c;var result="";if((list instanceof Array)&&(!!list.length)&&(list[0]!=group)){result=" within "+$scope.renderOperand(group.wd)+" "+$scope.getDurationUnitName(group.wd.vt,!((group.wd.t=="c")&&(!isNaN(group.wd.c))&&(parseInt(group.wd.c)==1)))+""+(group.wt=="s"?" (strict)":"")}return $sce.trustAsHtml(result)};$scope.getWeekDayName=function(day){if(isNaN(day)){return"day"}return $scope.weekDays[parseInt(day)]};$scope.getMonthDayName=function(day){switch(day){case -1:return"last";case -2:return"second-last";case -3:return"third-last"}return day+$scope.getOrdinalSuffix(day)};$scope.getMonthName=function(month){return $scope.yearMonths[month]};$scope.getDurationUnitName=function(unit,plural){var suffix=plural?"s":"";switch(unit){case"ms":return"millisecond"+suffix;case"s":return"second"+suffix;case"m":return"minute"+suffix;case"h":return"hour"+suffix;case"d":return"day"+suffix;case"w":return"week"+suffix;case"n":return"month"+suffix;case"y":return"year"+suffix}return""};$scope.renderTimer=function(timer){var result="";var interval=timer.lo;var unit=$scope.getDurationUnitName(interval.vt);var unit2=unit;var level=0;switch(interval.vt){case"ms":level=1;break;case"s":level=2;break;case"m":level=3;break;case"h":level=4;break;case"d":level=5;break;case"w":level=6;unit=$scope.getWeekDayName(interval.odw);break;case"n":level=7;unit=$scope.getMonthDayName(interval.odm)+" "+$scope.getWeekDayName(interval.odw)+" of the month";break;case"y":level=8;unit=$scope.getMonthDayName(interval.odm)+" "+$scope.getWeekDayName(interval.odw)+" of "+$scope.getMonthName(interval.omy);break}switch(interval.t){case"c":if(!isNaN(interval.c)){var c=parseInt(interval.c);switch(c){case 1:result=unit;break;case 2:result="other "+unit;break;default:result=""+c+" "+unit2+"s";switch(interval.vt){case"n":result+=", on the "+$scope.getMonthDayName(interval.odm)+" "+$scope.getWeekDayName(interval.odw)+" of the month";break;case"y":result+=", on the "+$scope.getMonthDayName(interval.odm)+" "+$scope.getWeekDayName(interval.odw)+" of "+$scope.getMonthName(interval.omy);break}}break}default:result=$scope.renderOperand(interval)+" "+unit+"s"}if(level==4){var m=("00"+timer.lo.om).substr(-2);result+=", at :"+m+" past the hour"}if(level>=5){result+=", at ";if(timer.lo2.t!="c"){switch(timer.lo3.t){case"c":var offset=isNaN(timer.lo3.c)?0:parseInt(timer.lo3.c);if(offset==0){result+=$scope.renderOperand(timer.lo2)}else{if(offset<0){result+=""+(-offset).toString()+" "+$scope.getDurationUnitName(timer.lo3.vt,(offset<-1))+" before "+$scope.renderOperand(timer.lo2)}else{result+=""+offset.toString()+" "+$scope.getDurationUnitName(timer.lo3.vt,(offset>1))+" after "+$scope.renderOperand(timer.lo2)}}break;default:result+=$scope.renderOperand(timer.lo2)+" ± "+$scope.renderOperand(timer.lo3)}}else{result+=$scope.renderOperand(timer.lo2)}}var om=(level<=2)&&(interval.om instanceof Array)&&interval.om.length?interval.om:null;var oh=(level<=3)&&(interval.oh instanceof Array)&&interval.oh.length?interval.oh:null;var odw=(level<=5)&&(interval.odw instanceof Array)&&interval.odw.length?interval.odw:null;var odm=(level<=6)&&(interval.odm instanceof Array)&&interval.odm.length?interval.odm:null;var owm=(level<=6)&&!odm&&(interval.owm instanceof Array)&&interval.owm.length?interval.owm:null;var omy=(level<=7)&&(interval.omy instanceof Array)&&interval.omy.length?interval.omy:null;if(!!om||!!oh||!!odw||!!odm||!!owm||!!omy){var rCount=0;result+=", but only";if(om){result+=" at ";for(i in om){if((i>0)&&(om.length>2)){result+=", "}if((i>0)&&(i==om.length-1)){result+=" or "}result+=":"+("00"+om[i]).substr(-2)+""}result+=" minutes past the hour";rCount++}if(oh){result+=(rCount?",":"")+" during the ";for(i in oh){if((i>0)&&(oh.length>2)){result+=", "}if((i>0)&&(i==oh.length-1)){result+=" or "}result+=""+$scope.formatHour(oh[i])+""}result+=" hour"+(oh.length>1?"s":"")+"";rCount++}var odwString="";if(odw){for(i in odw){if((i>0)&&(odw.length>2)){odwString+=", "}if((i>0)&&(i==odw.length-1)){odwString+=" or "}odwString+=""+$scope.weekDays[odw[i]]+"s"}rCount++}if(owm){result+=(rCount?",":"")+" on the ";for(i in owm){if((i>0)&&(owm.length>2)){result+=", "}if((i>0)&&(i==owm.length-1)){result+=" or "}result+=""+$scope.getOrdinal(owm[i])+""}result+=" "+(odwString?odwString:"week"+(owm.length>1?"s":"")+"")+(omy?"":" of the month");rCount++}else{if(odwString){result+=(rCount>1?",":"")+" on "+odwString}}if(odm){result+=(rCount?",":"")+" on the ";for(i in odm){if((i>0)&&(odm.length>2)){result+=", "}if((i>0)&&(i==odm.length-1)){result+=" or "}result+=""+$scope.getOrdinal(odm[i])+""}result+=" day"+(odm.length>1?"s":"")+(omy?"":" of the month");rCount++}if(omy){result+=" "+(owm||odm?"of":"in")+" ";for(i in omy){if((i>0)&&(omy.length>2)){result+=", "}if((i>0)&&(i==omy.length-1)){result+=" or "}result+=""+$scope.yearMonths[omy[i]-1]+""}rCount++}}return $sce.trustAsHtml(result)};$scope.renderString=function(value){return renderString($sce,value)};$scope.renderTask=function(task){var command=$scope.getCommandById(task.c);var display;if(!command){display=task.c+"(";for(i in task.p){display+=(parseInt(i)?", ":"")+$scope.renderOperand(task.p[i],null,null,null,"and")}display+=")"}else{var displayFormat=command.d;if(task.c==="httpRequest"){var method=task.p[1].c;var useQueryString=method==="GET"||method==="DELETE"||method==="HEAD";var requestBodyType=task.p[2].c;if(useQueryString){displayFormat+="[? with query {3}]"}else{if(requestBodyType==="CUSTOM"){displayFormat+="[? with {4}][? as type {5}]"}else{displayFormat+="[? with {2}][? encoded {3}]"}}}display=!displayFormat?command.n:displayFormat.replace(/(?:\[\?(.*?))?\{(\d)\}(?:\s*\])?/g,function(match,prefix,text){var idx=parseInt(text);if((idx<0)||(!task.p)||(idx>=task.p.length)){return" (?) "}var value="";if(command.p[idx].t=="duration"){var unit=$scope.getDurationUnitName(task.p[idx].vt,true);value=$scope.renderOperand(task.p[idx],true)+" "+unit}else{if((task.p[idx].t=="c")&&(!!command.p[idx].d)&&(task.p[idx].c=="false")){value=""}else{value=$scope.renderOperand(task.p[idx],true,null,null,"and")}}if(!value){value=""}if(!!value&&!!command.p[idx].d){value=(!!task.p[idx]&&!!task.p[idx].t)?command.p[idx].d.replace("{v}",value):""}return(value?(prefix||""):"")+value}).replace(/(\{T\})/g,"°"+$scope.location.temperatureScale);var icon=command.i;if(icon){display=' '+display}}if(task.m){display+=" (only while "+$scope.buildLocationModeNameList(task.m)+")"}display+=";";return $sce.trustAsHtml(display)};$scope.renderDeviceList=function(devices,attribute,aggregation,trailing){var result="";var deviceNames=[];suffix=(aggregation=="any"?"or":"and");var prefix="";if(devices instanceof Array){if(devices.length>1){switch(aggregation){case"any":prefix="Any of ";break;case"all":prefix="All of ";break;case"count":prefix="Count of ";break;case"avg":prefix="Average of ";break;case"median":prefix="Median of ";break;case"least":prefix="Least occurring value of ";break;case"most":prefix="Most occurring value of ";break;case"stdev":prefix="Standard deviation of ";break;case"min":prefix="Minimum of ";break;case"max":prefix="Maximum of ";break;case"variance":prefix="Variance of ";break}if(!trailing){prefix=prefix.toLowerCase()}}for(deviceIndex in devices){var device=$scope.getDeviceById(devices[deviceIndex]);if(device){deviceNames.push({n:device.n,a:device.an,t:"dev"})}else{deviceNames.push({n:"{"+devices[deviceIndex]+"}",t:"var"})}}if(deviceNames.length){result=prefix+$scope.buildNameList(deviceNames,suffix,"dev","",!!attribute,true)}}return $sce.trustAsHtml(result)};$scope.validatePiston=function(piston){var idx=0;var level=0;var warnings={};var addWarning=function(object,warning){if(!object){return}object.w=object.w?object.w:[];object.w.push(warning)};var traverseObject=function(object,parentObject,dataType,parentLevel){var level=parentLevel+1;if(object instanceof Array){for(i in object){object[i]=traverseObject(object[i],parentObject,dataType,level)}return object}if(object instanceof Object){for(property in object){object[property]=traverseObject(object[property],object,object.vt?object.vt:object.t,level)}if(!!object.t){delete (object.w);switch(object.t){case"every":if(level>3){addWarning(object,"Timers are designed to be top-level statements and should not be used inside other statements. If you need a conditional timer, please look into using a while loop instead.")}break;case"on":if(level>3){addWarning(object,"On event statements are designed to be top-level statements and should not be used inside other statements.")}break}}}return object};piston=traverseObject(piston,"piston",null,0);$scope.warnings=warnings;return piston};$scope.compilePiston=function(piston,anonymize,legend){var legend=legend?legend:{};var idx=0;var warnings={};var anonymizeValue=function(key,data){if(!anonymize){return(!!legend[key]&&!!legend[key].id)?legend[key].id:key}if(!key){return""}var safeKey;if(legend[key]){var item=legend[key];safeKey=item.key;if(data&&data.a&&(data.a instanceof Array)&&item.value&&item.value.a&&(item.value.a instanceof Array)){for(a in data.a){if(item.value.a.indexOf(data.a[a])<0){item.value.a.push(data.a[a])}}}}else{safeKey=":"+("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"+idx).substr(-32)+":";idx++;legend[key]={key:safeKey,value:data}}return safeKey};var addWarning=function(object,warning){if(!object){return}object.w=object.w?object.w:[];object.w.push(warning)};var traverseObject=function(object,parentObject,dataType){if(object instanceof Array){for(i in object){object[i]=traverseObject(object[i],parentObject,dataType)}return object}if(object instanceof Object){if(object.exp){delete (object.exp)}if(anonymize){switch(object.vt){case"phone":var phones=object.c?object.c.split(/,;\*\|/):[];var safePhones=[];for(p in phones){safePhones.push(anonymizeValue(phones[p],{t:"phone"}))}object.c=safePhones.join(",");object.e="";break;case"contact":case"contacts":var contacts=object.c?(object.c instanceof Array?object.c:object.c.split(/,;\*|/)):[];var safeContacts=[];for(c in contacts){safeContacts.push(anonymizeValue(contacts[c],{t:"contact"}))}object.c=(object.c instanceof Array)?safeContacts:safeContacts[0];object.e="";break;case"email":object.c=anonymizeValue(object.c,{t:"email"});object.e="";break;case"uri":object.c=anonymizeValue(object.c,{t:"uri"});object.e="";break}}delete (object.w);for(property in object){var v=object[property];if((v===false)||(v===null)||(v==="")){delete (object[property])}else{object[property]=traverseObject(object[property],object,object.vt?object.vt:object.t)}}if(!anonymize&&!!object&&!!object.t&&!!object.vt&&((object.t=="c")||(object.t=="e"))){switch(object.t){case"c":object.exp=$scope.parseString(object.c,object.vt);break;case"e":object.exp=$scope.parseExpression(object.e,false,object.vt);break}}return object}var value=object?object.toString():"";if(value.startsWith(":")&&value.endsWith(":")){if(anonymize){var device=$scope.getDeviceById(object);if(device){object=anonymizeValue(object,{t:"device",n:device.an,a:!!parentObject&&!!parentObject.a&&(parentObject.a.length>1)?[parentObject.a]:[]});return object}var locationMode=$scope.getLocationModeById(object);if(locationMode){switch(locationMode){case"Home":case"Night":case"Sleep":case"Away":case"Vacation":break;default:locationMode="Custom Mode"}object=anonymizeValue(object,{t:"mode",n:locationMode});return object}var routine=$scope.getRoutineById(object);if(routine){object=anonymizeValue(object,{t:"routine"});return object}var contact=$scope.getContactById(object);if(contact){object=anonymizeValue(object,{t:"contact"});return object}}else{object=anonymizeValue(object,{t:"unknown"});return object}}return object};piston=traverseObject($scope.copy(piston),"piston");piston.l={};for(l in legend){piston.l[legend[l].key]=legend[l].value}$scope.warnings=warnings;return piston};$scope.determineDeviceType=function(device){return dataService.determineDeviceType(device)};$scope.anonymizeDevices=function(devices){var cache={};for(i in devices){var device=devices[i];var name=dataService.determineDeviceType(device).replace(/([A-Z])/g," $1").replace(/^./,function(str){return str.toUpperCase()}).replace("Rgb ","RGB ");var idx=cache[name]?cache[name]+1:1;cache[name]=idx;devices[i].an=name+" "+idx}return devices};$scope.anonymizeContacts=function(contacts){var cache={};for(i in contacts){var contact=contacts[i];var name="John Doe";var idx=cache[name]?cache[name]+1:1;cache[name]=idx;contacts[i].an=name+" "+idx}return contacts};$scope.breakList=function(list){return list.replace(/,/g,"
")};var formatDate=function(date){var year=date.getFullYear();var month=(1+date.getMonth()).toString();month=month.length>1?month:"0"+month;var day=date.getDate().toString();day=day.length>1?day:"0"+day;return month+"/"+day+"/"+year};$scope.getMonth=function(date){if(date){return["JAN","FEB","MAR","APR","MAY","JUN","JUL","AUG","SEP","OCT","NOV","DEC"][date.getMonth()]}};$scope.getDay=function(date){if(date){return("0"+date.getDate()).substr(-2)}};$scope.timeSince=timeSince;$scope.timeCounter=timeCounter;$scope.timeLeft=timeLeft;$scope.currentTime=currentTime;$scope.tap=function(tapId){dataService.tap(tapId).then(function(response){})};$scope.test=function(){dataService.testPiston($scope.pistonId)};$scope.togglePiston=function(piston,$event){if((!piston)&&(!$scope.viewerPiston||!$scope.viewerPiston.app)){return}var pistonId=piston?piston.i:$scope.pistonId;if(pistonId){$timeout.cancel(tmrRefresh);var enabled=!(piston?piston.e:$scope.viewerPiston.app.enabled);if(piston){piston.e=enabled}else{$scope.viewerPiston.app.enabled=enabled}if(enabled){dataService.resumePiston(pistonId).then(function(response){$scope.onRefresh(response)})}else{dataService.pausePiston(pistonId).then(function(response){$scope.onRefresh(response)})}}if($event&&e.preventDefault){$event.preventDefault()}if($event&&$event.stopPropagation){$event.stopPropagation()}};$scope.configurePiston=function(piston){$scope.configuredPistonId=$scope.configuredPistonId==piston.i?null:piston.i};$scope.showPiston=function(piston){document.body.scrollTop=0;$scope.viewerPiston=null;$scope.pistonId=piston.i;$scope.refresh();window.onSwipeRight=$scope.hidePiston};$scope.hidePiston=function(){document.body.scrollTop=0;$scope.pistonId=null;window.onSwipeRight=null};$scope.prepareActions=function(condition){var actions=[];var trueActions=[];var falseActions=[];var mainGroup=(condition.id<=0);var tasks=$scope.viewerPiston.tasks;var acts=$scope.viewerPiston.app.actions;for(action in acts){if(acts[action].pid==condition.id){if(acts[action].t){var actionTasks=acts[action].t;for(t in actionTasks){var time=0;for(task in tasks){if((tasks[task].type=="cmd")&&(tasks[task].ownerId==acts[action].id)&&(tasks[task].taskId==actionTasks[t].i)){if((time==0)||(time>tasks[task].time)){time=tasks[task].time}}}actionTasks[t].time=time}}if(mainGroup){actions.push(acts[action])}else{if(acts[action].rs==false){falseActions.push(acts[action])}else{trueActions.push(acts[action])}}}}var time=0;for(task in tasks){if((tasks[task].type=="evt")&&(tasks[task].ownerId==condition.id)){if((time==0)||(time>tasks[task].time)){time=tasks[task].time}}}condition.time=time;condition.actions=actions;condition.trueActions=trueActions;condition.falseActions=falseActions;condition.$scope=$scope;if(condition.children){for(child in condition.children){$scope.prepareActions(condition.children[child])}}};$scope.hadRecentActivity=function(piston){return piston&&piston.le&&piston.le.event&&piston.le.event.date&&(timeLeft((new Date(piston.le.event.date)).getTime())>-120)};$scope.toggleViewerOptions=function(){$scope.viewerPiston.showOptions=!$scope.viewerPiston.showOptions};$scope.getSecondaryStatementName=function(){var mode=$scope.viewerPiston.app.mode;switch(mode){case"Latching":return"BUT IF";case"Then-If":return"THEN IF";case"Else-If":return"ELSE IF";case"Or-If":return"OR IF";case"And-If":return"AND IF"}return"IF"};$scope.serializeObject=function(object){return angular.toJson(object)};$scope.anonymizeObject=function(object,returnAsString){var data=$scope.serializeObject(object);var matches=data.match(/(:[a-f0-9]{32}:)/g);if(matches){matches=matches.unique()}for(i in matches){data=data.replace(new RegExp(matches[i],"g"),("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"+i).substr(-32))}return(returnAsString?data:angular.fromJson(data))};$scope.objectToBlob=function(object,contentType){contentType=contentType||"";var sliceSize=1024;var data=utoa($scope.serializeObject(object));data+="|"+data.length.toString();var bytesLength=data.length;var slicesCount=Math.ceil(bytesLength/sliceSize);var byteArrays=new Array(slicesCount);for(var sliceIndex=0;sliceIndexstartIndex){var value=str.slice(startIndex,i-1).trim();var parsedValue=parseFloat(value.trim());if(!isNaN(parsedValue)&&(numExp.test(value.trim()))){arr.push({t:(value.indexOf(".")>=0?"decimal":"integer"),v:parsedValue,l:location(startIndex,i-2)});return true}if(typeof value=="string"){if(["true","false"].indexOf(value)>=0){arr.push({t:"boolean",v:value,l:location(startIndex,i-2)})}else{if(["null"].indexOf(value)>=0){arr.push({t:"dynamic",v:null,l:location(startIndex,i-2)})}else{arr.push({t:"variable",x:value,l:location(startIndex,i-2)})}}return true}arr.push({t:"operand",v:str.slice(startIndex,i-1),l:location(startIndex,i-2)});return true}return false}function addConstant(allowEmpty){if(i-(allowEmpty?0:1)>startIndex){var value=str.slice(startIndex,i-1).replace(/\\[\[\]\{\}\'\"0-9abcdefghijklmopqsuvwxyz]/gi,function(match){return match[1]});var parsedValue=parseFloat(value.trim());if((dataType!="phone")&&!isNaN(parsedValue)&&(numExp.test(value.trim()))){arr.push({t:(value.indexOf(".")>=0?"decimal":"integer"),v:parsedValue,l:location(startIndex,i-2)});return true}arr.push({t:(["true","false"].indexOf(value)>=0?"boolean":"string"),v:(value=="null"?null:value),l:location(startIndex,i-2)})}}function addDevice(){if(i-1>startIndex){var value=str.slice(startIndex,i-1);var pos=value.lastIndexOf(":");var deviceName=value;var attribute="";if(pos>0){var deviceName=value.substr(0,pos).trim().replace(/\\[\[\]\{\}\'\"0-9abcdefghijklmopqsuvwxyz]/gi,function(match){return match[1]});attribute=value.substr(pos+1).trim()}var device=$scope.getDeviceByName(deviceName);if(device&&device.id){var a=attribute.toLowerCase();attribute="";virtualAttribute="";switch(a){case"orientation":case"axisx":case"axisy":case"axisz":virtualAttribute=a.replace("axisx","axisX").replace("axisy","axisY").replace("axisz","axisZ");a="threeaxis"}if(a==statusAttribute){attribute=statusAttribute}else{for(attributeIndex in device.a){var attr=device.a[attributeIndex];if(a==attr.n.toLowerCase()){attribute=virtualAttribute?virtualAttribute:attr.n}}}if(!!a&&!attribute){attribute="?"}arr.push({t:"device",id:device.id,a:attribute,l:location(startIndex-1,i-1)})}else{arr.push({t:"device",x:deviceName,a:attribute,l:location(startIndex-1,i-1)})}}}function addFunction(){var value=str.slice(startIndex,i-1).toLowerCase().trim();if($scope.db.functions[value]){func++;var params=main();var items=[];var item=null;for(p in params){if(!item){item={t:"expression",i:[]}}if((params[p].t=="operator")&&(params[p].o==",")){items.push(item);item={t:"expression",i:[]}}else{item.i.push(params[p])}}if(item){items.push(item)}arr.push({t:"function",n:value,i:items,l:location(startIndex,i-1)});func--}else{addOperand();arr.push({t:"expression",i:main(),l:location(startIndex,i-1)});startIndex=i}}var compositeVariable=isCompositeVariable();while(i":case"?":case":":var c2=(i=","<=","<>","<<",">>"].indexOf(c+c2)>=0){i++;c+=c2}arr.push({t:"operator",o:c,l:location(i-1,i-1)});startIndex=i;compositeVariable=isCompositeVariable()}else{if(c=="\\"){i++;c=c2}}continue;case'"':case"“":case"”":if(exp&&!dv&&!sq){dq=!dq;odq=!odq;(dq?addOperand():addConstant(true));startIndex=i;compositeVariable=isCompositeVariable()}continue;case"'":case"‘":case"’":if(exp&&!dq&&!dv){sq=!sq;osq=!osq;(sq?addOperand():addConstant(true));startIndex=i;compositeVariable=isCompositeVariable()}continue;case"(":if(exp&&!dv&&!dq&&!sq){parenthesis++;addFunction();startIndex=i;compositeVariable=isCompositeVariable()}continue;case")":if(exp&&!dv&&!dq&&!sq){parenthesis--;addOperand();startIndex=i;return arr}continue;case"[":if(!compositeVariable&&exp&&!dq&&!sq&&!dv){dv=true;addOperand();startIndex=i;compositeVariable=isCompositeVariable()}continue;case"]":if(!compositeVariable&&exp&&dv&&!dq&&!sq){addDevice();dv=false;startIndex=i;compositeVariable=isCompositeVariable()}continue;case"{":if(exp==initExp){exp++;addConstant();startIndex=i;arr.push({t:"expression",i:main(),l:location(startIndex-1,i-1)});startIndex=i;compositeVariable=isCompositeVariable()}else{exp++;startIndex=i;arr.push({t:"expression",i:main(),l:location(startIndex-1,i-1)});startIndex=i;compositeVariable=isCompositeVariable()}continue;case"}":addOperand();exp--;return arr;continue}}i++;exp?addOperand():addConstant();return arr}var items=main();var result={t:"expression",i:items,str:str};if(exp!=initExp){result.err="Invalid expression closure termination"}else{if(osq){result.err="Invalid single quote termination"}else{if(odq){result.err="Invalid double quote termination"}else{if(parenthesis){result.err="Invalid parenthesis closure termination"}}}}result.ok=!result.err;if(result.ok){result.ok=$scope.validateExpression(result)}return result};$scope.validateExpression=function(expression){var error="";var errVar="";var errorLoc="";function getSubstring(location,separator,partNo){if(!location){return""}location=location.toString().split(":");var start=parseInt(location[0]);var end=(location.length==2)?parseInt(location[1]):start;var s=expression.str.substr(start,end-start+1);if((s.substr(0,1)=="[")&&(s.substr(-1,1)=="]")){s=s.substr(1,s.length-2)}if(separator){s=s.split(separator);if(partNo>=s.length){return""}return s[partNo].trim()}return s.trim()}function validateItem(item){var ok=true;var err="";var loc="";if(item.i){for(subitem in item.i){ok=ok&&validateItem(item.i[subitem])}}else{switch(item.t){case"device":if(!item.x&&!item.id){ok=false;err="Invalid device "+getSubstring(item.l,":",0);loc=item.l;break}if(!item.id&&item.x&&!(($scope.systemVars&&scope.systemVars[item.x])||($scope.globalVars&&$scope.systemVars[item.x])||$scope.getVariableByName(item.x))){ok=false;err="Invalid device variable "+getSubstring(item.l,":",0);loc=item.l;break}if(item.a=="?"){ok=false;err="Invalid attribute "+getSubstring(item.l,":",1);loc=item.l;break}break;case"variable":if(item.x.startsWith("$args.")&&(item.x.length>6)){break}if(item.x.startsWith("$args[")&&(item.x.length>6)){break}if(item.x.startsWith("$json.")&&(item.x.length>6)){break}if(item.x.startsWith("$json[")&&(item.x.length>6)){break}if(item.x.startsWith("$places.")&&(item.x.length>8)){break}if(item.x.startsWith("$places[")&&(item.x.length>8)){break}if(item.x.startsWith("$response.")&&(item.x.length>10)){break}if(item.x.startsWith("$response[")&&(item.x.length>10)){break}if(item.x.startsWith("$nfl.")&&(item.x.length>5)){break}if(item.x.startsWith("$weather.")&&(item.x.length>9)){break}if(item.x.startsWith("$incidents.")&&(item.x.length>11)){break}if(item.x.startsWith("$incidents[")&&(item.x.length>11)){break}if($scope.systemVars&&$scope.systemVars[item.x]){break}if($scope.globalVars&&$scope.globalVars[item.x]){break}if(!$scope.getVariableByName(item.x)){if(item.x.indexOf("[")>=0){var v=$scope.getVariableByName(item.x.split("[")[0]);if(v&&v.t.endsWith("]")){break}}ok=false;errVar=getSubstring(item.l);err="Variable "+errVar+" not found";loc=item.l;break}break}}item.ok=ok;if(err){item.err=err;if(!error){error=err;errorLoc=loc}}return ok}validateItem(expression);if(error){expression.err=error;expression.errVar=errVar;expression.loc=errorLoc}return expression.ok};$scope.hexToHsl=function(hex){var rgb=/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);if(!rgb){return{h:0,s:0,l:0}}var r=0+parseInt(rgb[1],16)/255;var g=0+parseInt(rgb[2],16)/255;var b=0+parseInt(rgb[3],16)/255;var max=Math.max(r,g,b),min=Math.min(r,g,b);var h,s,l=(max+min)/2;if(max==min){h=s=0}else{var d=max-min;s=(l>0.5)?d/(2-max-min):d/(max+min);switch(max){case r:h=(g-b)/d+(g=0)||($scope.evalText=="")){var delta=(event.originalEvent.keyCode==38?-1:(event.originalEvent.keyCode==40?1:0));if(delta==0){return}var i=$scope.lastEval+delta;if(i>=$scope.evals.length){i=0}if(i<0){i=$scope.evals.length-1}if((i>=0)&&(i<$scope.evals.length)){$scope.evalText=$scope.evals[i].text}$scope.lastEval=i}};$scope.onEvalKeyPress=function(event){if(event.originalEvent.keyCode!=13){return}$scope.lastEval=-1;var text=$scope.evalText;switch(text){case"/clear":$scope.evalText="";$scope.evals=[];return}var eval={type:($scope.evalType=="e"?"expression":"value"),text:text,eval:""};if($scope.evalType=="e"){eval.eval=$scope.evaluateExpression(text,null,eval,true)}else{eval.eval=$scope.evaluateValue(text,null,eval,true)}$scope.evals.push(eval);$scope.evalText="";$scope.$$postDigest(function(){var d=$("console > content");d.scrollTop(d.prop("scrollHeight"))})};$scope.delayEvaluation=function(operand){if(!operand){return}operand.eval="...";var expression=operand.data.exp;var dataType=operand.data.vt;switch(dataType){case"s":case"m":case"h":case"d":case"w":case"n":case"y":dataType="ms"}$timeout.cancel(operand.tmrDelayEvaluation);operand.tmrDelayEvaluation=$timeout(function(){if($scope.designer&&$scope.designer.dialog){operand.eval="(evaluating)";evaluateExpression(expression,dataType,operand)}},2500)};$scope.evaluateValue=function(value,dataType,output,showType){return $scope.evaluateExpression($scope.parseExpression(value,true),dataType,output,showType)};$scope.evaluateExpression=function(expression,dataType,output,showType){var useConsole=!(output instanceof Object);if(!(expression instanceof Object)){expression=$scope.parseExpression(expression)}if(!(expression instanceof Object)){return"Evaluation error: unknown error."}if(expression.err){return"Evaluation error: "+expression.err}dataService.evaluateExpression($scope.pistonId,expression,dataType).then(function(response){var result="";if(!response||(response.status!="ST_SUCCESS")){result="Evaluation error: Received a "+(response?response.status:"(unknown)")+" result."}else{result=(!!useConsole||!!showType?"("+response.value.t+") ":"")+response.value.v}if(useConsole){console.log(result)}else{output.eval=$scope.renderString(result)}});return"(evaluating)"};window.evaluateValue=$scope.evaluateValue;window.evaluateExpression=$scope.evaluateExpression;var userAgent=navigator.userAgent||navigator.vendor||window.opera;if(userAgent.match(/Android/i)){$scope.android=true}$scope.url=window.location.href;$scope.mobile=window.mobileCheck();$scope.tablet=(!$scope.mobile)&&(window.mobileOrTabletCheck());$scope.formatTime=window.formatTime;$scope.utcToString=utcToString;$scope.utcToTimeString=utcToTimeString;$scope.utcToDateString=utcToDateString;$scope.formatLogTime=function(timestamp,offset){return utcToString(timestamp)+"+"+offset};$scope.md5=window.md5;var tmrInit=setInterval(function(){if(dataService.ready()){clearInterval(tmrInit);$scope.init()}},1)}]);function test(g,b,f){scope.evaluateExpression(scope.parseExpression(g,b,f))}var MAX_STACK_SIZE=10;config.controller("fuel",["$scope","$rootScope","dataService","$timeout","$interval","$location","$sce","$routeParams","ngDialog","$window",function(q,m,o,c,f,e,l,p,n,b){var a=null;var d=null;var g=null;q.initialized=false;q.loading=true;q.canisters=[];q.fuelStreams=[];q.selectedCanister="";q.error="";q.designer={};q.locations=null;q.instances=null;q.requestId=0;q.activePistons=0;q.pausedPistons=0;q.dropDownMenu=false;q.view="piston";q.init=function(s,u,t){if(q.$$destroyed){return}o.setStatusCallback(q.setStatus);o.listFuelStreams().then(function(x){if(q.$$destroyed){return}q.loading=false;q.initialized=true;if(!x||!x.fuelStreams||!(x.fuelStreams instanceof Array)){return}q.fuelStreams=x.fuelStreams;var v=[];for(i in q.fuelStreams){var w=q.fuelStreams[i];v.push(w.c)}q.canisters=v.unique().sort()});q.initChart()};q.selectCanister=function(s){q.selectedCanister=s};q.selectFuelStream=function(s){for(i in q.fuelStreams){q.fuelStreams[i].selected=(q.fuelStreams[i]==s)}q.prepareFuelStream(s);return};q.prepareFuelStream=function(s){if(s.selected&&!s.data){q.loading=true;o.listFuelStreamData(s.i).then(function(t){if(t&&t.points&&(t.points instanceof Array)){s.data=t.points;q.populateChart()}q.loading=false})}else{q.populateChart()}};q.populateChart=function(){var B=0;var D=[];var A=[];var z=null;var x=0;var C=!!q.chart.options.isStacked;function w(t){return !isNaN(parseFloat(t))&&isFinite(t)}var y=0;for(i in q.fuelStreams){if(!!q.fuelStreams[i].selected&&!!q.fuelStreams[i].data&&q.fuelStreams[i].data.length){y++}}for(i in q.fuelStreams){if(!!q.fuelStreams[i].selected&&!!q.fuelStreams[i].data&&q.fuelStreams[i].data.length){var G=q.fuelStreams[i];z=z?z:{cols:[{id:"time",label:"Time",type:"datetime"}],rows:[]};var F=w(G.data[0].d)?"number":"string";z.cols.push({id:i,label:(G.c?G.c+" \\ ":"")+G.n,type:F});var u=[];for(k=0;kB+1){q.chart.view.columns.pop()}};q.toggleFuelStream=function(s){s.selected=!s.selected;q.prepareFuelStream(s)};q.setStatus=function(s){if(a){c.cancel(a)}a=null;q.status=s;if(q.status){a=c(function(){q.setStatus()},10000)}};q.sortByDisplay=function(t,s){return(t.d>s.d)?1:((s.d>t.d)?-1:0)};q.sortByName=function(t,s){return(t.n>s.n)?1:((s.n>t.n)?-1:0)};q.home=function(){q.initialized=false;e.path("/")};q.initChart=function(t,s){q.chart={type:"AreaChart",displayed:false,data:null,options:{isStacked:false,fill:20,displayExactValues:true,interpolateNulls:true,explorer:{axis:"horizontal"},is3D:true,width:"100%",height:"100%",pointSize:6,dataOpacity:0.5,pointShape:"square",series:{0:{pointShape:"circle"},1:{pointShape:"square"},2:{pointShape:"diamond"},3:{pointShape:"polygon"},4:{pointShape:"triangle"},5:{pointShape:"star"}},chartArea:{left:96,top:16,right:16,width:"100%",height:"80%"},legend:{position:"bottom"}},hAxis:{title:"Date/Time"},formatters:{},view:{columns:[]}}};q.hideSeries=function(t){var s=t.column;if(t.row===null){if(q.chart.view.columns[s]==s){q.chart.view.columns[s]={label:q.chart.data.cols[s].label,type:q.chart.data.cols[s].type,calc:function(){return null}}}else{q.chart.view.columns[s]=s}}};var r=navigator.userAgent||navigator.vendor||window.opera;if(r.match(/Android/i)){q.android=true}q.url=window.location.href;q.mobile=window.mobileCheck();q.tablet=(!q.mobile)&&(window.mobileOrTabletCheck());q.formatTime=formatTime;q.utcToString=utcToString;window.scope=q;var h=setInterval(function(){if(o.ready()){clearInterval(h);q.init()}},1)}]);config.controller("visors",["$scope","$rootScope","dataService","$timeout","$interval","$location","$sce","$routeParams","ngDialog","$window",function(q,l,o,c,j,g,k,p,n,b){var m=128;var f=128;var e=false;q.visor={tiles:[],grid:{cols:15,rows:8}};q.placeholders=[];q.dragger={};q.scale=1;q.tileTypes=[{name:"Temperature",type:"temperature",template:"temperature",icon:"thermometer",description:"Provides information about temperature",attributes:["temperature"]},{name:"Switch",type:"switch",template:"switch",icon:"switch",description:"Provides information about a generic switch",attributes:["switch"]},{name:"Contact",type:"contact",template:"contact",icon:"circle-o-notch",description:"Provides information about a generic contact sensor",attributes:["contact"]},{name:"Contact (door)",type:"door-contact",template:"contact",icon:"circle-o-notch",description:"Provides information about a door contact sensor",attributes:["contact"]},{name:"Contact (window)",type:"window-contact",template:"contact",icon:"circle-o-notch",description:"Provides information about a window contact sensor",attributes:["contact"]},{name:"Presence",type:"presence",template:"presence",icon:"circle-o-notch",description:"Provides information about a window contact sensor",attributes:["presence"]},{name:"Presence Map",type:"presence-map",template:"presence-map",icon:"circle-o-notch",description:"Provides information about a window contact sensor",attributes:["latitude","longitude"]}];q.getTemplateName=function(h){for(i in q.tileTypes){if(q.tileTypes[i].type==h){return q.tileTypes[i].template}}return"unknown"};q.getTemplateAttributes=function(h){for(i in q.tileTypes){if(q.tileTypes[i].type==h){return q.tileTypes[i].attributes}}return"unknown"};q.init=function(){q.visor.tiles.push({z:"Piston state",t:"contact",i:0,g:"auto",sz:{w:8,h:1}});q.visor.tiles.push({z:"xx",i:8,t:"contact",g:"auto",sz:{w:1,h:1}});q.visor.tiles.push({z:"yy",i:9,t:"contact",g:"auto",sz:{w:1,h:1}});q.visor.tiles.push({z:"zz",i:71,t:"contact",g:"auto",sz:{w:4,h:4}});q.prepare();q.ds=o;q.mobile=window.mobileCheck();window.scope=q};q.copy=function(h){return angular.fromJson(angular.toJson(h))};q.prepare=function(){var w=q.visor.grid.cols;var v=q.visor.grid.rows;for(var s=0;su?u:w;q.scale=t;return{width:(m*s)+"px",height:(f*r)+"px",transform:"scale("+t+")",left:((document.documentElement.clientWidth-h*t)/2)+"px",top:((document.documentElement.clientHeight-48-v*t)/2)+"px"}};q.onSizeChanged=function(){q.$apply()};q.setDesignerType=function(h){q.designer.type=h;q.designer.tile.t=h;(!q.designer.page)?q.nextPage():q.refreshSelects();if(q.designer.ontypechanged){q.designer.ontypechanged(q.designer,h)}q.updateDeviceList(h)};q.listDevicesWithAttributes=function(s){var r=o.listDevices();if(!s||!(s instanceof Array)||!s.length){return r}var h=[];for(d in r){var t=r[d];var u=0;for(a in t.a){if(s.indexOf(t.a[a].n)>=0){u++;if(u==s.length){break}}}if(u==s.length){h.push(t)}}return h};q.updateDeviceList=function(h){q.designer.devices=q.listDevicesWithAttributes(q.getTemplateAttributes(h));return;q.designer.devices=[];for(deviceId in devices){q.designer.devices.push(mergeObjects({id:deviceId},devices[deviceId]))}};q.closeDialog=function(){if(q.designer.dialog){q.designer.dialog.close();q.designer.dialog=null}};q.nextPage=function(){q.designer.page++;q.refreshSelects()};q.prevPage=function(){if(q.designer.page){q.designer.page--}};q.range=function(h){return new Array(h)};q.refreshSelects=function(h){if(h){q.$$postDigest(function(){$("select["+h+"]").selectpicker("refresh");c(function(){$("select["+h+"]").selectpicker("refresh")},0,false)})}else{q.$$postDigest(function(){$("select[selectpicker]").selectpicker("refresh");c(function(){$("select[selectpicker]").selectpicker("refresh")},0,false)})}};q.startDrag=function(h,r){if(!!q.dragger.dragging&&(q.dragger.tile==r)){return}q.dragger={tile:r,dragging:false,start:{x:h.pageX,y:h.pageY},offset:{x:h.clientX,y:h.clientY}}};q.drag=function(t){if(!q.dragger.tile){return}var z=q.visor.grid.cols;var w=q.visor.grid.rows;q.dragger.dragging=q.dragger.dragging|((Math.abs(t.pageX-q.dragger.start.x)>5)||(Math.abs(t.pageY-q.dragger.start.y)>5));if(!q.dragger.dragging){return}var v=q.dragger.tile;var s=Math.round((v.$$pos.x+(t.pageX-q.dragger.start.x)/q.scale)/m);var u=s+v.sz.w;var A=Math.round((v.$$pos.y+(t.pageY-q.dragger.start.y)/q.scale)/f);var h=A+v.sz.h;s=s<0?0:(u>z?z-v.sz.w:s);A=A<0?0:(h>w?w-v.sz.h:A);q.dragger.index=z*A+s;v.$$style.left=(s*m)+"px";v.$$style.top=(A*f)+"px"};q.endDrag=function(h){if(q.dragger.dragging){q.dragger.tile.i=q.dragger.index;q.prepare();e=true;c(function(){e=false},50)}q.dragger={}};q.setTileSize=function(r,h){if((r>q.visor.grid.cols)||(h>q.visor.grid.rows)){return}q.designer.tile.sz={w:r,h:h}};q.addTile=function(r,h){return q.editTile(null,h,r)};q.editTile=function(s,r,h){if(r){r.stopPropagation()}if(e){e=false;return}q.selectedTile=null;if(!s){s={};s.i=h;s.t=null;s.d=[];s.s="i";s.z="";s.sz={w:1,h:1};s.f="a";s.fc="#000000";s.b="a";s.bc="#eeeeee";s.g="auto"}q.designer={};q.designer.$obj=s;q.designer.$tile=s;q.designer.tile=q.copy(s);q.designer.$new=s.t?false:true;q.designer.page=s.t?1:0;if(s.t){q.updateDeviceList(s.t)}q.designer.dialog=n.open({template:"dialog-edit-tile",className:"ngdialog-theme-default ngdialog-large",closeByDocument:false,disableAnimation:true,scope:q})};q.updateTile=function(){var h=q.designer.tile;if(q.designer.$new){q.visor.tiles.push(h)}else{$.extend(q.designer.$tile,h)}q.prepare();q.closeDialog()};q.init()}]); \ No newline at end of file +function dashify(b,a){if(typeof b!=="string"){throw new TypeError("expected a string")}return b.trim().replace(/([a-z])([A-Z])/g,"$1-$2").replace(/\W/g,function(c){return/[À-ž]/.test(c)?c:"-"}).replace(/^-+|-+$/g,"").replace(/-{2,}/g,function(c){return a&&a.condense?"-":c}).toLowerCase()};var app=angular.module("webCoRE",["ng","ngRoute","ngSanitize","ngResource","ngDialog","ngAnimate","angular-svg-round-progressbar","angular-bootstrap-select","swipe","dndLists","ui.toggle","chart.js","smartArea","ui.bootstrap.contextMenu","ngFitText","googlechart","ngMap"]);var cdn="";var theme="";app.directive("head",["$rootScope","$compile",function(a,b){return{restrict:"E",link:function(e,f){var c='';f.append(b(c)(e));e.routeStyles={};a.$on("$routeChangeStart",function(k,g,h){if(h&&h.$$route&&h.$$route.css){if(!angular.isArray(h.$$route.css)){h.$$route.css=[h.$$route.css]}angular.forEach(h.$$route.css,function(l){delete e.routeStyles[l]})}if(g&&g.$$route&&g.$$route.css){if(!angular.isArray(g.$$route.css)){g.$$route.css=[g.$$route.css]}angular.forEach(g.$$route.css,function(l){e.routeStyles[l]=l})}})}}}]);app.directive("ngWheel",["$parse",function(a){return function(f,c,b){var e=a(b.ngWheel);c.bind("wheel",function(g){f.$apply(function(){e(f,{$event:g})})})}}]);app.directive("refresh",["$interval",function(e){var c=0;var a=null;var b=null;return{restrict:"A",link:function(g,h,f){h.on("$destroy",function(){if(b!=null){e.cancel(b)}});if(angular.isDefined(f.refresh)&&!isNaN(parseInt(f.refresh))){c=f.refresh}if(angular.isDefined(f.onRefresh)&&angular.isFunction(g[f.onRefresh])){a=g[f.onRefresh];b=e(function(){a(h[0])},c*1000);f.$observe("refresh",function(k){if(!angular.equals(k,c)){if(b!=null){e.cancel(b)}c=k;if(c>0){b=e(function(){a(h[0])},c*1000)}}})}}}}]);app.directive("textcomplete",["Textcomplete",function(a){return{restrict:"EA",scope:{members:"=",message:"=",callback:"&"},template:'',link:function(f,g,e){var b=f.members;var c=g.find("textarea");var h=new a(c,[{match:/(\b)(\w{2,})$/,search:function(k,l){l($.map(b,function(m){return m.toLowerCase().indexOf(k.toLowerCase())===0?m:null}))},index:2,replace:function(k){return"$1"+k+" "}}]);if(f.callback){f.$watch("message",function(l,k){f.callback()})}$(h).on({"textComplete:select":function(l,k){f.$apply(function(){f.message=k})},"textComplete:show":function(k){$(this).data("autocompleting",true)},"textComplete:hide":function(k){$(this).data("autocompleting",false)}})}}}]);app.directive("masonry",["$parse",function(a){return{restrict:"AC",link:function(g,h,e){g.items=[];var b=h[0];var c=angular.extend({itemSelector:"tile"},JSON.parse(e.masonry));var f=g.masonry=new Masonry(b,c);var k=0;g.update=function(){if(k){window.clearTimeout(k)}k=window.setTimeout(function(){k=0;f.reloadItems();f.layout();h.children(c.itemSelector).css("visibility","visible")},120)};g.update()}}}]).directive("masonryTile",function(){return{restrict:"AC",link:function(a,c){c.css("visibility","hidden");var b=c.parent("*[masonry]:first").scope(),e=b.update;imagesLoaded(c.get(0),e);c.ready(e)}}});app.directive("tileHeight",function(){var a={restrict:"A",link:function(g,h,b,c,e){var f=1;if(b.tileHeight){f=b.tileHeight}var k=function(){var l=h[0].parentElement.offsetWidth/Math.round(h[0].parentElement.offsetWidth/h[0].offsetWidth)*f;h.outerHeight(l)};g.$watch(b.tileHeight,function(l){f=1*l;k()});$(window).resize(k);k();g.$on("$destroy",function(){$(window).unbind("resize",k)})}};return a});app.directive("tileMeta",["$parse","$sce",function(b,a){var c={restrict:"A",scope:false,link:function(f,g,e){function h(){var l=b(e.tileMeta)(f);var k=b(e.tileIndex)(f)+1;var m=renderString(a,l["t"+k]).meta;if(!m||!m.type){m=renderString(a,l["f"+k]).meta}if(!m||!m.type){m=renderString(a,l["i"+k]).meta}f.$parent.meta=m}f.$watchCollection(e.tileMeta,h);f.$watch(e.tileIndex,h)}};return c}]);app.directive("help",["$compile",function(a){var b={restrict:"A",link:function(g,e,c){var h=c.help?c.help:e.text();var f=angular.element("');e.append(a(f)(g))}};return b}]);app.directive("script",function(){return{restrict:"E",scope:false,link:function(b,e,a){if(a.type==="text/javascript"){var c=e.text();var g=new Function(c);g()}}}});app.directive("devData",function(a){return function(e,c,b){var f=function(k,h,g){var l=a(g.devData)(k);if(l){for(attr in l){h.attr("data-"+attr,l[attr])}}};e.$watch(b.devData,function(){f(e,c,b)})}});app.directive("onSizeChanged",["$window",function(a){return{restrict:"A",scope:{onSizeChanged:"&"},link:function(f,c,b){var e=c[0];g(f,e);a.addEventListener("resize",h);function g(l,k){l.cachedElementWidth=k.offsetWidth;l.cachedElementHeight=k.offsetHeight}function h(){var k=f.cachedElementWidth!=e.offsetWidth||f.cachedElementHeight!=e.offsetHeight;if(k){var l=f.onSizeChanged();l()}}}}}]);app.directive("title",function(){return{restrict:"A",link:function(c,b,a){if(!mobileCheck()){$(b).hover(function(){$(b).tooltip({container:"body",html:true,placement:"bottom"});$(b).tooltip("show")},function(){$(b).tooltip("hide")});$(b).on("$destroy",function(){$(b).tooltip("hide")})}}}});app.directive("collapseControl",["dataService",function(a){return function(f,e,b){var g=(b.target||b.ariaControls||"").replace("#","");var c=a.isCollapsed(g);if(c&&"ariaExpanded" in b){e.attr("aria-expanded","false")}e.bind("click",function(h){var k=a.isCollapsed(g);a.setCollapsed(g,!k)})}}]);app.directive("collapseTarget",["dataService",function(a){return function(e,c,b){var h=b.id||"";var f=b.collapseClass||"in";var g=a.isCollapsed(h);if(g){c.removeClass(f)}else{c.addClass(f)}}}]);app.directive("taskedit",function(){return{restrict:"A",scope:false,link:function(b,c,a){var e=[];function f(k){if(e.length>0){for(var h=0;hf[e]?1:-1)});if(c){b.reverse()}return b}});app.filter("dashify",function(){return function(a){return dashify(a,{condense:true})}});app.filter("uniqueDashify",function(){var a={};return function(e,c){e=dashify(e,{condense:true});var f=e;var b=1;while(f in a&&a[f]!==c){f=e+"-"+b++}a[f]=c;return f}});var config=app.config(["$routeProvider","$locationProvider","$sceDelegateProvider","$rootScopeProvider","$animateProvider",function(c,a,g,f,b){f.digestTtl(10000);var e=".module.css";g.resourceUrlWhitelist(["self",cdn+"**"]);b.classNameFilter(/^(?:(?!no-ng-animate).)*$/);c.when("/",{templateUrl:cdn+theme+"html/modules/dashboard.module.html?v="+version(),controller:"dashboard",css:cdn+theme+"css/modules/dashboard"+e+"?v="+version()}).when("/register",{templateUrl:cdn+theme+"html/modules/register.module.html?v="+version(),controller:"register",css:cdn+theme+"css/modules/register"+e+"?v="+version()}).when("/init/:init",{redirectTo:function(h){app.initialInstanceUri=atou(h.init);return"/"}}).when("/piston/:pistonId",{templateUrl:cdn+theme+"html/modules/piston.module.html?v="+version(),controller:"piston",css:cdn+theme+"css/modules/piston"+e+"?v="+version(),reloadOnSearch:false}).when("/fuel",{templateUrl:cdn+theme+"html/modules/fuel.module.html?v="+version(),controller:"fuel",css:cdn+theme+"css/modules/fuel"+e+"?v="+version()}).when("/visors",{templateUrl:cdn+theme+"html/modules/visors.module.html?v="+version(),controller:"visors",css:cdn+theme+"css/modules/visors"+e+"?v="+version()}).when("/init/:instId1/:instId2",{redirectTo:function(h){app.initialInstanceUri=atou(h.instId1+"/"+h.instId2);return"/"}}).otherwise({redirectTo:"/"});a.html5Mode(true)}]);config.factory("dataService",["$http","$location","$rootScope","$window","$q",function(r,B,p,I,b){var J={};var o="";var q=null;var s={};var G=null;var w={};var a={};var l="N7zqL6a8Texs4wY5y&y2YPLzus+_dZ%s";var K=l;var g=null;var O=null;var N=null;var y=false;var A={};var C=1;var m=false;if(localforage){localforage.config({name:"webCoRE"});localforage.keys().then(function(Q){C=Q.length;if(C){localforage.iterate(function(T,S,R){A[S]=n(T);C--;if(!C&&!m){c()}})}else{c()}})}var h=function(Q){return JSON.parse(Q)};var P=function(Q,S,R){return Array(S-String(Q).length+1).join(R||"0")+Q};var D=function(Q){return P(Q.getFullYear(),4)+"-"+P(1+Q.getMonth(),2)+"-"+P(Q.getDate(),2)+" "+P(Q.getHours(),2)+":"+P(Q.getMinutes(),2)+":"+P(Q.getSeconds(),2)};var e=function(Q){return JSON.stringify(Q)};var F=function(S,Q){try{return utoa(I.sjcl.encrypt(Q?Q:K,angular.toJson(S),{ks:256}))}catch(R){return null}};J.encryptBackup=function(R,Q){return F(R,K+(Q?Q:""))};var n=function(R,Q){try{return angular.fromJson(I.sjcl.decrypt(Q?Q:K,atou(R)))}catch(S){return null}};var M=function(Q,S,R){localforage.setItem("core:"+Q,F(S,R));A["core:"+Q]=S;return};var E=function(Q,R){return A["core:"+Q]};var u=function(Q){q=Q;s[q.id]=q;M("locations",s);return q};var t=function(Q){if(!Q||!Q.uri){return null}if(Q.uri.indexOf("?access_token=")){var R=Q.uri.split("?access_token=");Q.uri=R[0];Q.accessToken=R[1]}return Q};var L=function(S){var R=(!G);if(!G||(G.id!=S.id)){G=S}var Q=a[G.id];if(!Q){Q={}}Q.token=S.token?S.token:Q.token;Q.uri=S.uri?S.uri.replace(":443",""):Q.uri;a[G.id]=t(Q);delete (G.token);delete (G.uri);if(S.contacts){G.contacts=S.contacts}G.contacts=G.contacts?G.contacts:(w[G.id]&&w[G.id].contacts?w[G.id].contacts:[]);if(S.devices){G.devices=S.devices;R=true}G.devices=G.devices?G.devices:(w[G.id]&&w[G.id].devices?w[G.id].devices:[]);if(!!G.pistons){for(i=0;iG.coreVersion){z("A newer SmartApp version ("+version()+") is available, please update and publish all the webCoRE SmartApps in the SmartThings IDE.",true)}else{z("A newer UI version ("+G.coreVersion+") is available, please hard reload this web page to get the newest version.",true)}}return G};var z=function(Q,R){if(g){g(Q,R)}};var v=function(Q){if(!Q){return""}return Q.replace(/([\uD83C-\uDBFF][\uDC00-\uDFFF])/g,function(R){return":"+encodeURIComponent(R)+":"})};var k=function(Q){if(!Q){return""}return Q.replace(/(\:%[0-9A-F]{2}%[0-9A-F]{2}%[0-9A-F]{2}%[0-9A-F]{2}\:)/g,function(R){return decodeURIComponent(R.substr(1,12))})};var f=function(Q){return(Q&&Q.accessToken?"access_token="+Q.accessToken+"&":"")};J.openWebSocket=function(T){if(T&&G){N=T;if(O){return O}var S=G.id;var Q=a[G.id];if(!Q){Q={}}var R=(Q&&Q.uri&&Q.uri.startsWith("https://graph-eu"))?"eu":"us";O=new WebSocket("wss://api-"+R+"-"+S[32]+".webcore.co:9297");O.onopen=function(U){O.send(G.id)};O.onclose=function(U){O=null;if(N){setTimeout(function(){J.openWebSocket(N)},5000)}};O.onmessage=function(U){if(N){try{N(U)}catch(V){}}};O.onerror=function(U){O=null;if(N){setTimeout(function(){J.openWebSocket(N)},5000)}};return O}else{N=null;O.close();O=null}};J.closeWebSocket=function(){J.openWebSocket(null)};J.ready=function(){return !!m};J.logout=function(){s={};w={};A={};return localforage.clear()};J.setStatusCallback=function(Q){g=Q};J.saveToStore=function(Q,R){return M(Q,R)};J.loadFromStore=function(Q){return E(Q)};J.deleteFromStore=function(Q){return localforage.removeItem("core:"+Q)};J.loadFromStore=function(Q){return E(Q)};J.deleteInstance=function(Q){if(Q){if(Q==G){G=null;M("instance",null,l)}delete (a[Q.id]);delete (w[Q.id]);M("instances",w);M("store",a)}};J.listLocations=function(){var Q=[];for(lid in s){Q.push(JSON.parse(JSON.stringify(s[lid])))}return Q};J.getLocation=function(Q){if(Q){for(lid in s){if(lid==Q){return JSON.parse(JSON.stringify(s[lid]))}}}else{return JSON.parse(JSON.stringify(q))}return null};J.listInstances=function(R){var Q=[];for(iid in w){if(!R||(w[iid].locationId==R)){Q.push(JSON.parse(JSON.stringify(w[iid])))}}return Q};J.getInstanceCount=function(R){var Q=0;for(iid in w){if(!R||(w[iid].locationId==R)){Q++}}return Q};J.getInstance=function(R,Q){if(G&&!R){return G}if(G&&(G.id==R)){return G}if(R){for(iid in w){if(iid==R){return JSON.parse(JSON.stringify(w[iid]))}}}else{try{return JSON.parse(JSON.stringify(G?G:(w?w[E("instance")]:null)))}catch(S){}}if(!!Q&&!!w){for(iid in w){return JSON.parse(JSON.stringify(w[iid]))}}return null};J.getPistonInstance=function(Q){for(iid in w){for(i in w[iid].pistons){if(w[iid].pistons[i].id==Q){return JSON.parse(JSON.stringify(w[iid]))}}}return null};J.loadInstance=function(U,R,Q,Y){var T=U?a[U.id]:null;var X=!U||!(U.devices instanceof Object)||!(Object.keys(U.devices).length)?0:(U.deviceVersion?U.deviceVersion:0);if(!T||!T.token){if((app.initialInstanceUri&&app.initialInstanceUri.length)||(R&&R.length)){R=app.initialInstanceUri?app.initialInstanceUri:R;if(!R.startsWith("https://")){if(R&&(R.indexOf("tat.comapi")>0)){var S=R.split("api");if(S[1].length>=33){var V=S[1].substr(0,32);var Z=S[1].substr(32);R="https://"+S[0]+"/api/"+V.substr(0,8)+"-"+V.substr(8,4)+"-"+V.substr(12,4)+"-"+V.substr(16,4)+"-"+V.substr(20,12)+"/apps/"+Z}}else{if(R&&!(R instanceof Object)&&(R.length>=69)){var ab=R.substr(0,R.length-64);if(!ab.endsWith(".com")){ab+=".api.smartthings.com"}R=R.substr(0,8)=="https://"?R:"https://"+ab+"/api/token/"+R.substr(-64,8)+"-"+R.substr(-56,4)+"-"+R.substr(-52,4)+"-"+R.substr(-48,4)+"-"+R.substr(-44,12)+"/smartapps/installations/"+R.substr(-32,8)+"-"+R.substr(-24,4)+"-"+R.substr(-20,4)+"-"+R.substr(-16,4)+"-"+R.substr(-12)+"/"}}}T=t({uri:R});for(id in a){if(a[id].uri==R){T=t(a[id]);if(w&&w[id]&&w[id].devices instanceof Object&&Object.keys(w[id].devices).length&&w[id].deviceVersion){X=w[id].deviceVersion}break}}}}delete (app.initialInstanceUri);if(!T){var aa=E("instance");if(aa){T=a[aa];if(w&&w[aa]&&w[aa].devices instanceof Object&&Object.keys(w[aa].devices).length&&w[aa].deviceVersion){X=w[aa].deviceVersion}}}if(!T){B.path("/register")}else{var W=document.getElementById("error");if(W){W.parentNode.removeChild(W)}}return r.jsonp((T?T.uri:"about:blank/")+"intf/dashboard/load?"+f(T)+"token="+(T&&T.token?T.token:"")+(Q?"&pin="+Q:"")+"&dashboard="+(Y?1:0)+"&dev="+X,{jsonpCallbackParam:"callback"}).then(function(ac){var ad=ac.data;if(ad.now){adjustTimeOffset(ad.now)}if(ad.error&&T){ad.uri=T.uri;ad.accessToken=T.accessToken}if(ad.location){u(ad.location)}if(ad.instance){ad.instance=L(ad.instance)}ad.endpoint=T.uri;ad.accessToken=T.accessToken;return ad},function(ac){z("There was a problem loading the dashboard data. The data shown below may be outdated; please log out if this problem persists.");return ac})};J.tap=function(Q){return r({method:"GET",url:"tap/"+Q})};J.getApiUri=function(){var Q=J.getInstance();si=a?a[Q.id]:null;return si?si.uri:null};J.refreshDashboard=function(){var R=J.getInstance();si=a&&R?a[R.id]:null;var Q=!R||!(R.devices instanceof Object)||!(Object.keys(R.devices).length)?0:(R.deviceVersion?R.deviceVersion:0);z("Loading dashboard...");return r.jsonp((si?si.uri:"about:blank/")+"intf/dashboard/refresh?"+f(si)+"token="+(si&&si.token?si.token:""),{jsonpCallbackParam:"callback"}).then(function(S){data=S.data;return data},function(S){return null})};J.getPiston=function(T){var S=J.getPistonInstance(T);if(!S){S=J.getInstance()}si=a&&S?a[S.id]:null;var R=!S||!(S.devices instanceof Object)||!(Object.keys(S.devices).length)?0:(S.deviceVersion?S.deviceVersion:0);var Q=E("db.version",l);z("Loading piston...");return r.jsonp((si?si.uri:"about:blank/")+"intf/dashboard/piston/get?"+f(si)+"id="+T+"&db="+Q+"&token="+(si&&si.token?si.token:"")+"&dev="+R,{jsonpCallbackParam:"callback"}).then(function(U){data=U.data;if(data.now){adjustTimeOffset(data.now)}if(data.dbVersion){M("db.version",data.dbVersion,l);M("db",data.db);z("Database updated to version "+data.dbVersion)}else{data.db=E("db");z()}if(data.location){u(data.location)}if(data.instance){data.instance=L(data.instance)}data.endpoint=si.uri;return data},function(U){return null})};J.backupPistons=function(R,Q){var S=J.getInstance(R);if(!S){S=J.getInstance()}si=a&&S?a[S.id]:null;return r.jsonp((si?si.uri:"about:blank/")+"intf/dashboard/piston/backup?"+f(si)+"ids="+Q+"&token="+(si&&si.token?si.token:""),{jsonpCallbackParam:"callback"}).then(function(T){data=T.data;if(data.now){adjustTimeOffset(data.now)}return data},function(T){return null})};J.getActivity=function(S,Q){var R=J.getPistonInstance(S);if(!R){R=J.getInstance()}si=a?a[R.id]:null;return r.jsonp((si?si.uri:"about:blank/")+"intf/dashboard/piston/activity?"+f(si)+"id="+S+"&log="+(Q?Q:0)+"&token="+(si&&si.token?si.token:""),{jsonpCallbackParam:"callback"}).then(function(T){return T.data})};J.generateBackupBin=function(R,S){var Q=J.getInstance();return r({method:"POST",url:"https://api.webcore.co/bins/"+(S?"":md5(Q.account.id)),data:R?(S?{d:F(R,l)}:{e:F(R,l+Q.account.id)}):{},transformResponse:function(T){try{T=JSON.parse(T);if(T&&T.bin){return T.bin}if(T&&T.uri){T=T.uri.split("/");if(T&&T.length){return T[T.length-1]}}}catch(U){}return null}})};J.saveToBin=function(S,R){z("Saving piston to backup bin...");var Q=J.getInstance();if(Q&&Q.account&&Q.account.id){R={e:F(R,l+Q.account.id)}}else{R={};S=null}return r({method:"PUT",url:"https://api.webcore.co/bins/"+md5(Q.account.id)+"/"+S,data:R,transformResponse:function(T){z("Backup bin updated");return true}})};J.loadFromBin=function(S,Q){z("Loading piston from backup bin...");var R=J.getInstance();if(!(R&&R.account&&R.account.id)){S=null}return r({method:"GET",url:"https://api.webcore.co/bins/"+md5(R.account.id)+"/"+S,transformResponse:function(T){if(S){try{T=JSON.parse(T);if(T&&T.e){return n(T.e,l+R.account.id)}if(T&&T.d){return n(T.d,l)}z()}catch(U){z("Sorry, an error occurred while importing the backup bin")}}return null}})};J.generateNewPistonName=function(){var Q=J.getInstance();si=a?a[Q.id]:null;return r.jsonp((si?si.uri:"about:blank/")+"intf/dashboard/piston/new?"+f(si)+"token="+(si&&si.token?si.token:""),{jsonpCallbackParam:"callback"}).then(function(R){return R.data})};J.createPiston=function(Q,R,T){var S=J.getInstance();si=a?a[S.id]:null;return r.jsonp((si?si.uri:"about:blank/")+"intf/dashboard/piston/create?"+f(si)+"author="+encodeURIComponent(R)+"&name="+encodeURIComponent(Q)+"&bin="+encodeURIComponent(T?T:"")+"&token="+(si&&si.token?si.token:""),{jsonpCallbackParam:"callback"}).then(function(U){return U.data})};var H=function(R,T,Q,S){if(QQ){var V=[].concat.apply([],S.split("").map(function(X,Y){return Y%Q?[]:S.slice(Y,Y+Q)},S));z("Preparing to save chunked piston...");return r.jsonp((si?si.uri:"about:blank/")+"intf/dashboard/piston/set.start?"+f(si)+"id="+U.id+"&chunks="+V.length.toString()+"&token="+(si&&si.token?si.token:""),{jsonpCallbackParam:"callback"}).then(function(X){if(X&&(X.status==200)&&X.data&&(X.data.status=="ST_READY")){return H(si,V,0,W)}})}else{z("Saving piston...");return r.jsonp((si?si.uri:"about:blank/")+"intf/dashboard/piston/set?"+f(si)+"id="+U.id+"&data="+encodeURIComponent(S)+"&bin="+encodeURIComponent(W)+"&token="+(si&&si.token?si.token:""),{jsonpCallbackParam:"callback"}).then(function(X){z();return X})}};J.setPistonBin=function(Q,R){var S=J.getPistonInstance(Q);if(!S){S=J.getInstance()}si=a?a[S.id]:null;z("Setting piston bin to "+R+"...");return r.jsonp((si?si.uri:"about:blank/")+"intf/dashboard/piston/set.bin?"+f(si)+"id="+Q+"&bin="+R+"&token="+(si&&si.token?si.token:""),{jsonpCallbackParam:"callback"}).then(function(T){z();return T.data})};J.clickPistonTile=function(Q,R){var S=J.getPistonInstance(Q);if(!S){S=J.getInstance()}si=a?a[S.id]:null;return r.jsonp((si?si.uri:"about:blank/")+"intf/dashboard/piston/tile?"+f(si)+"id="+Q+"&tile="+R+"&token="+(si&&si.token?si.token:""),{jsonpCallbackParam:"callback"}).then(function(T){z();return T.data})};J.setPistonCategory=function(Q,R){var S=J.getPistonInstance(Q);if(!S){S=J.getInstance()}si=a?a[S.id]:null;z("Setting piston category...");return r.jsonp((si?si.uri:"about:blank/")+"intf/dashboard/piston/set.category?"+f(si)+"id="+Q+"&category="+R+"&token="+(si&&si.token?si.token:""),{jsonpCallbackParam:"callback"}).then(function(T){z();return T.data})};J.setPistonLogging=function(Q,S){var R=J.getPistonInstance(Q);if(!R){R=J.getInstance()}si=a?a[R.id]:null;z("Setting piston logging level to "+S+"...");return r.jsonp((si?si.uri:"about:blank/")+"intf/dashboard/piston/logging?"+f(si)+"id="+Q+"&level="+S+"&token="+(si&&si.token?si.token:""),{jsonpCallbackParam:"callback"}).then(function(T){z();return T.data})};J.clearPistonLogs=function(Q){var R=J.getPistonInstance(Q);if(!R){R=J.getInstance()}si=a?a[R.id]:null;z("Clearing piston logs...");return r.jsonp((si?si.uri:"about:blank/")+"intf/dashboard/piston/clear.logs?"+f(si)+"id="+Q+"&token="+(si&&si.token?si.token:""),{jsonpCallbackParam:"callback"}).then(function(S){z();return S.data})};J.pausePiston=function(Q){var R=J.getPistonInstance(Q);if(!R){R=J.getInstance()}si=a?a[R.id]:null;z("Pausing piston...");return r.jsonp((si?si.uri:"about:blank/")+"intf/dashboard/piston/pause?"+f(si)+"id="+Q+"&token="+(si&&si.token?si.token:""),{jsonpCallbackParam:"callback"}).then(function(S){z();return S.data})};J.resumePiston=function(Q){var R=J.getPistonInstance(Q);if(!R){R=J.getInstance()}si=a?a[R.id]:null;z("Resuming piston...");return r.jsonp((si?si.uri:"about:blank/")+"intf/dashboard/piston/resume?"+f(si)+"id="+Q+"&token="+(si&&si.token?si.token:""),{jsonpCallbackParam:"callback"}).then(function(S){z();return S.data})};J.testPiston=function(Q){var R=J.getPistonInstance(Q);if(!R){R=J.getInstance()}si=a?a[R.id]:null;z("Testing piston...");return r.jsonp((si?si.uri:"about:blank/")+"intf/dashboard/piston/test?"+f(si)+"id="+Q+"&token="+(si&&si.token?si.token:""),{jsonpCallbackParam:"callback"}).then(function(S){z();return S.data})};J.createPresenceSensor=function(R,Q){var S=J.getPistonInstance();if(!S){S=J.getInstance()}si=a?a[S.id]:null;return r.jsonp((si?si.uri:"about:blank/")+"intf/dashboard/presence/create?"+f(si)+"name="+encodeURIComponent(R)+"&dni="+encodeURIComponent(Q?Q:"")+"&token="+(si&&si.token?si.token:""),{jsonpCallbackParam:"callback"}).then(function(T){return T.data})};J.deletePiston=function(Q){var R=J.getPistonInstance(Q);if(!R){R=J.getInstance()}si=a?a[R.id]:null;return r.jsonp((si?si.uri:"about:blank/")+"intf/dashboard/piston/delete?"+f(si)+"id="+Q+"&token="+(si&&si.token?si.token:""),{jsonpCallbackParam:"callback"}).then(function(S){return S.data})};J.setVariable=function(R,U,Q){var T=Q?J.getPistonInstance(Q):J.getInstance();si=a?a[T.id]:null;if(U&&U.t){switch(U.t){case"time":var V=new Date(U.v);U.v=V.getTime()-V.getTimezoneOffset()*60000;break;case"date":case"datetime":U.v=(new Date(U.v)).getTime();break}}var S=U?utoa(angular.toJson(U)):"";return r.jsonp((si?si.uri:"about:blank/")+"intf/dashboard/variable/set?"+f(si)+"name="+R+"&value="+encodeURIComponent(S)+(Q?"&id="+Q:"")+"&token="+(si&&si.token?si.token:""),{jsonpCallbackParam:"callback"}).then(function(W){return W.data})};J.setSettings=function(Q){var S=J.getInstance();si=a?a[S.id]:null;var R=Q?utoa(angular.toJson(Q)):"";return r.jsonp((si?si.uri:"about:blank/")+"intf/dashboard/settings/set?"+f(si)+"settings="+encodeURIComponent(R)+"&token="+(si&&si.token?si.token:""),{jsonpCallbackParam:"callback"}).then(function(T){return T.data})};J.evaluateExpression=function(R,U,Q){var T=J.getPistonInstance(R);if(!T){T=J.getInstance()}si=a?a[T.id]:null;var S=utoa(angular.toJson(U));return r.jsonp((si?si.uri:"about:blank/")+"intf/dashboard/piston/evaluate?"+f(si)+"id="+R+"&expression="+encodeURIComponent(S)+"&dataType="+(Q?encodeURIComponent(Q):"")+"&token="+(si&&si.token?si.token:""),{jsonpCallbackParam:"callback"}).then(function(V){return V.data})};J.registerDashboard=function(Q){return r.post("https://api.webcore.co/dashboard/register/"+Q).then(function(R){return R.data})};J.listFuelStreams=function(){var Q=J.getInstance();if(Q){var U=Q.id;var R=a[Q.id];if(!R){R={}}var T=(R&&R.uri&&R.uri.startsWith("https://graph-eu"))?"eu":"us";var S={method:"POST",url:"https://api-"+T+"-"+U[32]+".webcore.co:9287/fuelStreams/list",headers:{"Auth-Token":"|"+U},data:{i:U}};return r(S).then(function(V){return V.data})}};J.login=function(W,R){var T=G||J.getInstance(null,true);if(!T){T={id:"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"+[0,1,2,3,4,5,6,7,8,9,"a","b","c","d","e","f"][Math.floor(Math.random()*16)]}}if(T){var V=T.id;var Q=a[T.id];if(!Q){Q={}}var U=(Q&&Q.uri&&Q.uri.startsWith("https://graph-eu"))?"eu":"us";var S={method:"POST",url:"https://api-"+U+"-"+V[32]+".webcore.co:9287/user/login",headers:{"Auth-Token":"|"+V},data:{u:W,p:R}};return r(S).then(function(X){var Y=X.data;a=JSON.parse("{}");for(x in a){if(!w[x]||!w[x].account){w[x]={id:x,name:"Unknown",locationId:"?"};s["?"]={id:"?",name:"Unknown"}}}G=J.getInstance(null,true);M("store",a);M("instances",w);M("locations",s);if(G){M("instance",G.id)}B.path("/");if(Y&&Y.result){a=Y.store;return true}return false})}};J.listFuelStreamData=function(R){var Q=J.getInstance();if(Q){var V=Q.id;var S=a[Q.id];if(!S){S={}}var U=(S&&S.uri&&S.uri.startsWith("https://graph-eu"))?"eu":"us";var T={method:"POST",url:"https://api-"+U+"-"+V[32]+".webcore.co:9287/fuelStreams/get",headers:{"Auth-Token":"|"+V},data:{i:V,f:R}};return r(T).then(function(W){return W.data})}};J.registerHandler=function(){navigator.registerProtocolHandler("web+core","https://"+window.location.hostname+"/handler/%s","webCoRE")};J.determineDeviceType=function(Q){if(Q&&Q.cn){if(Q.cn.indexOf("Water Sensor")>=0){return"waterSensor"}if(Q.cn.indexOf("Contact Sensor")>=0){return"contactSensor"}if(Q.cn.indexOf("Thermostat")>=0){return"thermostat"}if(Q.cn.indexOf("Garage Door Control")>=0){return"garageDoor"}if(Q.cn.indexOf("Music Player")>=0){return"musicPlayer"}if(Q.cn.indexOf("Door Control")>=0){return"door"}if(Q.cn.indexOf("Presence Sensor")>=0){return"presenceSensor"}if(Q.cn.indexOf("Motion Sensor")>=0){return"motionSensor"}if(Q.cn.indexOf("Color Control")>=0){return"rgbBulb"}if(Q.cn.indexOf("Color Temperature")>=0){return"whiteBulb"}if(Q.cn.indexOf("Switch Level")>=0){var R=Q.n.toLowerCase();if(R.indexOf("light")>=0){return"whiteBulb"}if(R.indexOf("keen")>=0){return"vent"}if(R.indexOf("vent")>=0){return"vent"}return"dimmer"}if(Q.cn.indexOf("Lock")>=0){return"lock"}if((Q.cn.indexOf("Button")>=0)&&(Q.cn.indexOf("Button")>=0)){return"keypad"}if(Q.cn.indexOf("Button")>=0){return"button"}if(Q.cn.indexOf("Temperature Measurement")>0){return"temperatureSensor"}if((Q.cn.indexOf("Switch")>=0)&&(Q.cn.indexOf("Power Meter")>=0)){return"outlet"}if(Q.cn.indexOf("Switch")>=0){return"switch"}if(Q.cn.indexOf("Power Meter")>=0){return"powerMeter"}}return"unknownDevice"};J.getAllCollapsed=function(){return J.loadFromStore("collapsed")||[]};J.isCollapsed=function(Q){return J.getAllCollapsed().indexOf(Q)>=0};J.setCollapsed=function(T,R){var S=J.getAllCollapsed();var Q=S.indexOf(T);if(R&&Q<0){S.push(T)}else{if(!R&&Q>=0){S.splice(Q,1)}}J.saveToStore("collapsed",S)};var c=function(){a=E("store");if(!a){a={}}s=E("locations");if(!s){s={}}w=E("instances");if(!w){w={}}userId=0;m=true;window.ds=J;if(!!a.user){J.login(a.user.name,a.user.token).then(function(Q){console.log(Q)})}};return J}]);app.run(["$rootScope","$window","$location",function(a,b,c){a.getTime=function(e){if(e){return e.format("h:mmtt")}};a.$on("$viewContentLoaded",function(e){var f=c.path();if(!f.startsWith("/")){f="/"+f}if(f.startsWith("/init/")){f="/init"}if(f.startsWith("/piston/")){f="/piston"}b.ga("send","pageview",{page:f})});a.bytesToSize=function(e){var g=["bytes","kB","MB","GB","TB"];if(e==0){return"0 Byte"}var f=parseInt(Math.floor(Math.log(e)/Math.log(1024)));return(e/Math.pow(1024,f)).toFixed(f==0?0:2)+" "+g[f]}}]);Date.prototype.format=function(A,a){var u=["\x00","January","February","March","April","May","June","July","August","September","October","November","December"];var c=["\x01","Jan.","Feb.","Mar.","Apr.","May","June","July","Aug.","Sept.","Oct.","Nov.","Dec."];var b=["\x02","Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"];var g=["\x03","Sun","Mon","Tue","Wed","Thu","Fri","Sat"];function p(h,f){var m=h+"";f=f||2;while(m.length12?o-12:o==0?12:o;A=A.replace(/(^|[^\\])hh+/g,"$1"+p(B));A=A.replace(/(^|[^\\])h/g,"$1"+B);var v=a?this.getUTCMinutes():this.getMinutes();A=A.replace(/(^|[^\\])mm+/g,"$1"+p(v));A=A.replace(/(^|[^\\])m/g,"$1"+v);var r=a?this.getUTCSeconds():this.getSeconds();A=A.replace(/(^|[^\\])ss+/g,"$1"+p(r));A=A.replace(/(^|[^\\])s/g,"$1"+r);var C=a?this.getUTCMilliseconds():this.getMilliseconds();A=A.replace(/(^|[^\\])fff+/g,"$1"+p(C,3));C=Math.round(C/10);A=A.replace(/(^|[^\\])ff/g,"$1"+p(C));C=Math.round(C/10);A=A.replace(/(^|[^\\])f/g,"$1"+C);var e=o<12?"AM":"PM";A=A.replace(/(^|[^\\])TT+/g,"$1"+e);A=A.replace(/(^|[^\\])T/g,"$1"+e.charAt(0));var q=e.toLowerCase();A=A.replace(/(^|[^\\])tt+/g,"$1"+q);A=A.replace(/(^|[^\\])t/g,"$1"+q.charAt(0));var E=-this.getTimezoneOffset();var l=a||!E?"Z":E>0?"+":"-";if(!a){E=Math.abs(E);var F=Math.floor(E/60);var w=E%60;l+=p(F)+":"+p(w)}A=A.replace(/(^|[^\\])K/g,"$1"+l);var z=(a?this.getUTCDay():this.getDay())+1;A=A.replace(new RegExp(b[0],"g"),b[z]);A=A.replace(new RegExp(g[0],"g"),g[z]);A=A.replace(new RegExp(u[0],"g"),u[k]);A=A.replace(new RegExp(c[0],"g"),c[k]);A=A.replace(/\\(.)/g,"$1");return A};function formatTime(c){try{var a=(new Date(c)).getTime()+(window.timeOffset?window.timeOffset:0);var f=new Date(a);return f.format("h:mm TT")}catch(b){}}function currentTime(){return(new Date()).getTime()+(window.timeOffset?window.timeOffset:0)}function fixTime(b){if(b<86400000){var c=new Date();var a=c.getTime();b+=a-(a%86400000)+c.getTimezoneOffset()*60000}return b}function utcToString(a){return(new Date(fixTime(a))).toLocaleString()}function utcToTimeString(a){return(new Date(fixTime(a))).toLocaleTimeString()}function utcToDateString(a){return(new Date(fixTime(a))).toLocaleDateString()}function timeSince(f){if(!f){return"never"}switch(typeof f){case"number":break;case"string":f=+new Date(f);break;case"object":if(f.constructor===Date){f=f.getTime()}break;default:f=+new Date()}var e=[[60,"seconds",1],[120,"1 minute ago","1 minute from now"],[3600,"minutes",60],[7200,"1 hour ago","1 hour from now"],[86400,"hours",3600],[172800,"yesterday","tomorrow"],[604800,"days",86400],[1209600,"last week","next week"],[2419200,"weeks",604800],[4838400,"last month","next month"],[29030400,"months",2419200],[58060800,"last year","next year"],[2903040000,"years",29030400],[5806080000,"last century","next century"],[58060800000,"centuries",2903040000]];var h=(+new Date()+(window.timeOffset?window.timeOffset:0)-f)/1000,b="ago",g=1;if(h==0){return"Just now"}if(h<0){h=Math.abs(h);b="from now";g=2}var a=0,c;while(c=e[a++]){if(h-20){return"pending"}f=true;g=-g}var b="";if(g>86400){b=Math.floor(g/86400).toString()+"d ";g=g%86400}var e=Math.floor(g/3600);var a=Math.floor((g-e*3600)/60);var c=g%60;b+=(e>0?(e<10?"0":"")+e.toString()+":":"")+(a<10?"0":"")+a.toString()+":"+(c<10?"0":"")+c.toString();return b}function timeLeft(c,a){if(!c){return 0}c+=window.timeOffset?window.timeOffset:0;var b=Math.round((c-(new Date().getTime()))/1000);switch(a){case"h":return Math.floor(b/3600);break;case"m":return b>=3600?60:Math.floor(b/60);break;case"s":return b>=60?60:Math.floor(b%60);break}return b}function adjustTimeOffset(a){var b=a-(new Date()).getTime();if(isNaN(window.timeOffset)||(Math.abs(b)":u+=">";break;case"[":var k=f.indexOf("|",e);if(k>e){var r=f.substring(e+1,k);e=k+1;u+=g(r)}else{e++;u+=g()}break;case"]":if(l==undefined){return"["+u+"]"}var s=l.trim();while(/(\bsrc=\S+),/.test(s)){s=s.replace(/(\bsrc=\S+),/,"$1:webCoRE-comma:")}s=s.replace(/\s+/g,",").split(",");var o="";var m="";var n="";var t="";for(x in s){if(!s[x]){continue}switch(s[x]){case"b":case"u":case"i":case"s":case"pre":case"mono":case"blink":case"flash":case"left":case"center":case"condensed":case"right":case"full":o+="s-"+s[x]+" ";break;case"chart-gauge":h.type=s[x].replace("chart-","");break;case"img":case"image":h.type="image";break;case"vid":case"video":h.type="video";break;default:if(/^\d+(\.\d+)?(x|em)/.test(s[x])){t=s[x].replace("x","em")}else{if(s[x].startsWith("b-")){n=s[x].substr(2).replace(/[^#0-9a-z]/gi,"")}else{if(s[x].startsWith("bk-")||s[x].startsWith("bg-")){n=s[x].substr(3).replace(/[^#0-9a-z]/gi,"")}else{if(s[x].startsWith("back-")){n=s[x].substr(5).replace(/[^#0-9a-z]/gi,"")}else{if(s[x].indexOf("=")>0){var k=s[x].indexOf("=");h.options[s[x].substr(0,k)]=s[x].substr(k+1).replace(/:webCoRE-comma:/g,",")}else{m=s[x].replace(/[^#0-9a-z]/gi,"")}}}}}}}h.className=o;h.color=m;h.backColor=n;return""+u+"";default:u+=q}e++}return u};h.html=g(f).replace(/\:fa-([a-z0-9\-\s]*)\:/gi,function(k){return''}).replace(/\:fa5-([a-z0-9\-\s]*)\:/gi,function(k){return''}).replace(/\:fal-([a-z0-9\-\s]*)\:/gi,function(k){return''}).replace(/\:far-([a-z0-9\-\s]*)\:/gi,function(k){return''}).replace(/\:fas-([a-z0-9\-\s]*)\:/gi,function(k){return''}).replace(/\:fab-([a-z0-9\-\s]*)\:/gi,function(k){return''}).replace(/\:wu-([a-k]|v[1-4])-([a-z0-9_\-]+)\:/gi,function(l){var k=l[4];if(k=="v"){k+=l[5];var m=l.substr(7,l.length-8);return''}else{var m=l.substr(6,l.length-7);return''}}).replace(/(?![^<]*[>])#[a-z0-9]{6}/gi,function(k){return'    '+k}).replace(/\\[rn]/gi,"
");var c=document.createElement("DIV");c.innerHTML=h.html;h.text=c.textContent||c.innerText||"";var a=b.trustAsHtml(h.html);a.meta=h;return a}Object.defineProperty(Array.prototype,"unique",{enumerable:false,value:function(){if(!this){return[]}var e={},c=[];for(var f=0,b=this.length;fthis.length){a=this.length}return this.substring(a-b.length,a)===b}}version=function(){return"v0.3.106.20180731"};config.controller("dashboard",["$scope","$rootScope","dataService","$timeout","$interval","$location","$sce","$routeParams","ngDialog","$window",function(s,n,q,d,h,g,m,r,o,c){var b=null;var f=null;var j=null;s.initialized=false;s.loading=true;s.data=null;s.error="";s.designer={};s.locations=null;s.instances=null;s.requestId=0;s.dropDownMenu=false;s.endpoint="";s.rawEndpoint="";s.categories=[];s.pausedPistons=[];s.view="piston";s.isAppHosted=!!window.BridgeCommander;s.hostDeviceId="";s.sidebarCollapsed=q.isCollapsed("dashboardSidebar");s.completedInitialRender=false;s.init=function(w,z,y){if(s.$$destroyed){return}if(j){d.cancel(j)}j=null;s.requestId++;var A=0+s.requestId;s.loading=!s.initialized||!s.instance;q.setStatusCallback(s.setStatus);q.loadInstance(w,z,y,(s.view=="dashboard")).then(function(E){if(s.$$destroyed){return}if(A!=s.requestId){return}if(E){s.endpoint=E.endpoint+"execute/:pistonId:";s.rawEndpoint=E.endpoint;s.rawAccessToken=E.accessToken;if(E.error){switch(E.error){case"ERR_INVALID_TOKEN":s.dialogLogIn(E.name,E.uri,E.accessToken);break}}else{s.initialized=true;s.location=q.getLocation();s.instance=q.getInstance();s.currentInstanceId=s.instance.id;s.instanceCount=q.getInstanceCount();s.sidebarCollapsed=q.isCollapsed("dashboardSidebar");if(!s.devices){s.devices=s.listAvailableDevices()}if(!s.virtualDevices){s.virtualDevices=s.listAvailableVirtualDevices()}window.scope=s;window.dataService=q;s.loading=false;var C=s.getCategories();while(s.categories.length>C.length){s.categories.pop()}while(s.categories.length0){D=D.substr(0,C)+w+"="+(new Date()).getTime()}else{D+=(D.indexOf("?")>0?"&":"?")+w+"="+(new Date()).getTime()}var z=new Image();z.onload=function(){y.src=D};z.src=D;y.src=D};s.getGaugeChart=function(z,w,y){return{type:"Gauge",options:y.options,data:{cols:[{id:"gauge",label:y.text,type:"number"}],rows:[{c:[{v:s.renderString(z.meta.s["t"+(w+1)]).meta.text,f:z.meta.s["o"+(w+1)]?s.renderString(z.meta.s["o"+(w+1)]).meta.text:null}]}]}}};s.clock=function(){if(s.instance){for(pistonIndex in s.instance.pistons){var w=s.instance.pistons[pistonIndex];w.opacity=w.meta?s.getOpacity(w.meta.t):0}}};s.setStatus=function(w,y){if(y){s.permanentStatus=w;return}if(b){d.cancel(b)}b=null;s.status=w;if(s.status){b=d(function(){s.setStatus()},10000)}};s.clickPistonTile=function(w,z,y){if(w.originalEvent.ctrlKey||w.originalEvent.shiftKey){s.openPiston(z.id)}else{q.clickPistonTile(z.id,y).then(function(A){if(A&&(A.status=="ST_SUCCESS")&&!!(A["new"])&&!!z&&!!(z.meta)){z.meta.s=A}})}};s.copy=function(w){return angular.fromJson(angular.toJson(w))};s.getPlaces=function(){var w=(!!s.instance&&!!s.instance.settings&&(s.instance.settings.places instanceof Array))?s.copy(s.instance.settings.places):[];return w};s.getCategories=function(){var w=(!!s.instance&&!!s.instance.settings&&(s.instance.settings.categories instanceof Array))?s.copy(s.instance.settings.categories):[];if(!w.length){w=[{n:"Uncategorized",t:"d",i:0}]}return w};s.getCategory=function(y){y=parseInt(y);if(isNaN(y)){y=0}for(var w in s.categories){if(s.categories[w].i==y){return s.categories[w]}}for(var w in s.categories){if(s.categories[w].i==0){return s.categories[w]}}s.categories.push({n:"Uncategorized",t:"d",i:0});return s.categories[s.categories.length-1]};s.updateLocation=function(w){s.coords=w.coords};s.showSettings=function(){ga("send","event","settings","show");if(navigator.geolocation){navigator.geolocation.getCurrentPosition(s.updateLocation)}s.checkPresenceSensor();s.closeNavBar();s.settings=s.copy(s.instance.settings);s.settings.categories=s.getCategories();s.settings.places=s.getPlaces();s.view="settings"};s.addCategory=function(){var w=0;for(x in s.settings.categories){if(s.settings.categories[x].i>=w){w=s.settings.categories[x].i+1}}s.settings.categories.push({n:"New Category "+w,t:"d",i:w})};s.randomHash=function(w){var A="0123456789abcdef".split("");var z="";for(var y=0;yy.o){var w=y.i;y.i=y.o;y.o=w}if(y.i+100>=y.o){y.o=y.i+100}if(s.designer.$new){s.settings.places.push(y)}if(y.h){for(i in s.settings.places){s.settings.places[i].h=(s.settings.places[i]==y)}}s.closeDialog()};s.movePlace=function(z,A){var y=z?z.latLng:this.center;s.designer.position=[y.lat(),y.lng()];switch(A){case"i":s.designer.inner=this.radius;break;case"o":s.designer.outer=this.radius;break}if(s.designer.inner>s.designer.outer){var w=s.designer.inner;s.designer.inner=s.designer.outer;s.designer.outer=w}if(s.designer.inner<50){s.designer.inner=50}if(s.designer.inner+200>=s.designer.outer){s.designer.outer=s.designer.inner+200}};s.deletePlace=function(){for(var w=0;w=s.settings.categories)){return}var w=s.settings.categories[y];s.settings.categories[y]=s.settings.categories[y-1];s.settings.categories[y-1]=w};s.moveCategoryDown=function(y){if((y<0)||(y>=s.settings.categories-1)){return}var w=s.settings.categories[y];s.settings.categories[y]=s.settings.categories[y+1];s.settings.categories[y+1]=w};s.deleteCategory=function(w){s.settings.categories.splice(w,1)};s.hideSettings=function(){ga("send","event","settings","hide");s.view="piston"};s.messageHost=function(B,A,z){if(!window.BridgeCommander){return}var y=window.BridgeCommander.getPlatformName?window.BridgeCommander.getPlatformName():"unknown";switch(y){case"iOS":window.BridgeCommander.call(B,JSON.stringify(A)).then(function(C){if(z){z(C?JSON.parse(C):null)}});break;case"Android":if(window.BridgeCommander.hasOwnProperty(B)){var w=window.BridgeCommander[B](JSON.stringify(A));if(z){z(w?JSON.parse(w):null)}}break;default:window.BridgeCommander.subscribe(s.onAppRequest);window.BridgeCommander.call(B,JSON.stringify(A)).then(function(C){if(z){z(C)}});break}};s.checkPresenceSensor=function(){s.messageHost("getStatus",{i:s.instance.id},function(w){s.hostDeviceId=w instanceof Object?(w.dni?w.dni:""):"";s.presenceSensorId=w instanceof Object?!!w.s:!!w})};s.registerPresenceSensor=function(){s.designer.name="";window.designer=s.designer;s.designer.dialog=o.open({template:"dialog-register-presence-sensor",className:"ngdialog-theme-default ngdialog-large",closeByDocument:false,disableAnimation:true,scope:s})};s.doRegisterPresenceSensor=function(){var w=s.designer.name;s.closeDialog();if(!w){return}q.createPresenceSensor(w,s.hostDeviceId).then(function(z){var y=z.deviceId;if(y){s.messageHost("register",{e:s.rawEndpoint,a:s.rawAccessToken,i:s.instance.id,d:y},function(A){s.presenceSensorId=y})}})};s.unregisterPresenceSensor=function(){if(!s.presenceSensorId){return}q.destroyPresenceSensor(s.presenceSensorId).then(function(w){s.messageHost("unregister",{i:s.instance.id})})};s.updatePlaces=function(){s.messageHost("update",{i:s.instance.id,p:s.instance.settings.places})};s.saveSettings=function(){ga("send","event","settings","save");s.instance.settings=s.settings;q.setSettings(s.settings).then(function(w){s.instance.settings=s.settings;s.updatePlaces();s.hideSettings()})};s.showFuelStreams=function(){ga("send","event","fuel","show");s.initialized=false;s.loading=true;g.path("fuel")};s.showDashboard=function(){s.view="dashboard";ga("send","event","dashboard","show");q.openWebSocket(s.onWSUpdate);s.dropDownMenu=false;s.refreshing=true;q.refreshDashboard().then(function(z){for(deviceId in z){if(deviceId.startsWith(":")){var y=s.instance.devices[deviceId];if(y){var w=z[deviceId];for(attr in w){for(i in y.a){if(y.a[i].n==attr){y.a[i].v=w[attr];break}}}y.data=s.getDeviceData(y)}}}s.refreshing=false;s.setStatus()})};s.hideDashboard=function(){ga("send","event","dashboard","hide");q.closeWebSocket();s.view="piston";s.dropDownMenu=false};s.onWheel=function(w){s.dropDownMenu=w&&w.originalEvent&&(w.currentTarget.scrollTop==0)&&(w.originalEvent.deltaY<0);return true};s.onSwipe=function(w,y){s.dropDownMenu=(w.currentTarget.scrollTop==0)&&(y=="down");return true};s.range=function(w){return new Array(w)};s.listLocations=function(){return q.listLocations()};s.listInstances=function(w){return q.listInstances(w)};s.listAllInstances=function(){var y=[];var w=s.listLocations();for(l in w){var z=q.listInstances(w[l].id);for(i in z){y.push({id:z[i].id,name:w[l].name+" \\ "+z[i].name,pistons:z[i].pistons})}}return y};s.listAvailableDevices=function(){var w=[];for(deviceIndex in s.instance.devices){s.instance.devices[deviceIndex].id=deviceIndex;w.push(s.instance.devices[deviceIndex])}return w.sort(s.sortByName)};s.listAvailableVirtualDevices=function(){var w=[];for(deviceIndex in s.instance.virtualDevices){var y=s.instance.virtualDevices[deviceIndex];w.push(mergeObjects({id:deviceIndex},y))}return w.sort(s.sortByName)};s.sortByDisplay=function(y,w){return(y.d>w.d)?1:((w.d>y.d)?-1:0)};s.sortByName=function(y,w){return(y.n>w.n)?1:((w.n>y.n)?-1:0)};s.switchInstance=function(y){if(y!=s.instance.id){var w=q.getInstance(y);if(w){s.instance=null;if(j){d.cancel(j)}j=null;s.devices=null;s.init(w);s.closeNavBar()}}};s.$on("$destroy",function(){if(b){d.cancel(b)}if(f){h.cancel(f)}if(j){d.cancel(j)}});s.getDeviceData=function(w){var y={};for(a in w.a){y[w.a[a].n]=w.a[a].v}return y};s.getBatteryLevel=function(w){if(isNaN(w)){return 0}w=Math.floor(w/20);if(w<=0){return 0}if(w>=4){return 4}return w};s.renderString=function(w){return renderString(m,w)};s.onWSUpdate=function(w){if(w.isTrusted&&w.data){try{var z=JSON.parse(w.data);if(z.d&&z.n){var y=s.instance.devices[z.d];if(y){for(a in y.a){if(y.a[a].n==z.n){y.a[a].v=z.v;y.data=s.getDeviceData(y);break}}}}s.$apply()}catch(A){}}};s.getDeviceAttribute=function(z,y){for(a in z.a){var w=z.a[a];if(y==w.n){return w.v}}return""};s.openPiston=function(w){ga("send","event","piston","view",w);s.loading=true;s.initialized=false;g.path("piston/"+w)};s.newPiston=function(){s.loading=true;q.generateNewPistonName().then(function(w){s.loading=false;s.designer={};s.designer.author=q.loadFromStore("author.handle");s.designer.name=w.name;s.designer.page=0;s.designer.backup=!!q.loadFromStore("backup.auto");s.designer.disclaimer=!s.designer.backup;s.designer.items=[{type:"blank",name:"Create a blank piston",icon:"code",cssClass:"wide btn-default"},{type:"duplicate",name:"Create a duplicate piston",icon:"code",cssClass:"wide btn-info"},{type:"template",name:"Create a piston from a template",icon:"code",cssClass:"wide btn-success"},{type:"restore",name:"Restore a piston using a backup code",icon:"code",cssClass:"wide btn-warning"},{type:"import",name:"Import a piston from an external source",icon:"code",cssClass:"wide btn-danger"}];s.designer.dialog=o.open({template:"dialog-add-piston",className:"ngdialog-theme-default ngdialog-large",closeByDocument:false,disableAnimation:true,scope:s})})};s.backup=function(){s.designer={page:0,pistons:[]};s.designer.dialog=o.open({template:"dialog-backup-piston",className:"ngdialog-theme-default ngdialog-large",closeByDocument:false,disableAnimation:true,scope:s,onOpenCallback:function(){s.$$postDigest(function(){$("select").selectpicker("selectAll")})}})};s.backupPistons=function(){s.designer.progress=0;s.designer.page=1;s.designer.instances={};for(i in s.designer.pistons){var y=s.designer.pistons[i].substr(0,34);var w=s.designer.pistons[i].substr(34);s.designer.instances[y]=s.designer.instances[y]?s.designer.instances[y]:[];s.designer.instances[y].push({pid:w,requested:false})}s.designer.results=[];s.backupBatch()};s.backupBatch=function(){if(!s.designer||!s.designer.instances){return}var y="";var z=[];for(i in s.designer.instances){if(y!=""){break}var w=s.designer.instances[i];for(p in w){if(((y=="")||(y==i))&&(!w[p].requested)){y=i;w[p].requested=true;z.push(w[p].pid);if(z.length>=10){break}}}}if(z.length){q.backupPistons(y,z).then(function(A){if(A&&(A.pistons instanceof Array)){s.designer.results=s.designer.results.concat(A.pistons);s.designer.progress=s.designer.results.length}s.backupBatch()})}else{if(s.designer.pistons.length==s.designer.results.length){s.designer.page=2}else{s.designer.page=3}}};s.saveBackup=function(){var w=new Blob([q.encryptBackup(s.designer.results,s.designer.password)],{type:"text/plain"});var y=document.createElement("a");y.href=window.URL.createObjectURL(w);y.download="webCoRE."+(new Date()).toJSON()+".backup";y.click();s.closeDialog()};s.movePiston=function(){s.designer={pistons:[],instance:""};s.designer.dialog=o.open({template:"dialog-move-piston",className:"ngdialog-theme-default ngdialog-large",closeByDocument:false,disableAnimation:true,scope:s})};s.movePistons=function(){alert("Sorry, not ready yet")};s.createPiston=function(){var w=function(y){s.closeDialog();s.initialized=false;g.path("piston/"+y.id).search({description:s.designer.description,type:s.designer.type,piston:s.designer.piston,bin:s.designer.bin})};s.loading=true;q.saveToStore("backup.auto",!!s.designer.backup);q.saveToStore("author.handle",s.designer.author);if(s.designer.backup){q.generateBackupBin().then(function(y){var z=y.data;q.createPiston(s.designer.name,s.designer.author,z).then(w)})}else{q.createPiston(s.designer.name,s.designer.author).then(w)}};s.dialogLogIn=function(y,z,w){if(j){d.cancel(j)}j=null;s.loading=false;s.initialized=false;s.designer={};s.designer.sender=y;s.designer.uri=z;s.designer.accessToken=w;s.designer.dialog=o.open({template:"dialog-auth",className:"ngdialog-theme-default ngdialog-large",closeByDocument:false,disableAnimation:true,scope:s})};s.logOut=function(){q.logout().then(function(){s.loading=true;s.initialized=false;g.path("register")})};s.onAppRequest=function(w){s.setStatus(w)};s.initAds=function(){if(s.isAppHosted){return}window.adsbygoogle=(window.adsbygoogle||[]);window.adsbygoogle.push({google_ad_client:"ca-pub-4643048739403893",enable_page_level_ads:true})};s.authenticate=function(){s.closeDialog();s.init(null,s.designer.uri+(s.designer.accessToken?"?access_token="+s.designer.accessToken:""),window.md5("pin:"+s.designer.password));s.designer=null};s.dialogDeleteInstance=function(w){if(w){s.loading=false;s.initialized=false;s.designer={};s.designer.sender=w.locationName+" \\ "+w.name;s.designer.instance=w;s.designer.dialog=o.open({template:"dialog-del-instance",className:"ngdialog-theme-default ngdialog-large",closeByDocument:false,disableAnimation:true,scope:s})}};s.deleteInstance=function(){s.closeDialog();q.deleteInstance(s.designer.instance);s.designer=null;s.init()};s.setDesignerType=function(w){s.designer.type=w;s.nextPage()};s.closeDialog=function(){if(s.designer.dialog){s.designer.dialog.close();s.designer.dialog=null}};s.nextPage=function(){s.designer.page++};s.prevPage=function(){if(s.designer.page){s.designer.page--}};s.getOpacity=function(w){if(!w){return 0}w=currentTime()-w;if((w<0)||(w>60000)){return 0}return 1-w/60000};s.getLocationMode=function(){var y=s.location.mode;for(var w=0;w"+s.utcToString(w.date)+"
- ";result+=w.message.replace(/\{\{(.*)\}\}/gi,function(y){return w.args[y.substr(2,y.length-4).trim()]});result+="";return m.trustAsHtml(result)};s.breakList=function(w){return w.replace(/,/g,"
")};var v=function(y){var z=y.getFullYear();var A=(1+y.getMonth()).toString();A=A.length>1?A:"0"+A;var w=y.getDate().toString();w=w.length>1?w:"0"+w;return A+"/"+w+"/"+z};s.getMonth=function(w){if(w){return["JAN","FEB","MAR","APR","MAY","JUN","JUL","AUG","SEP","OCT","NOV","DEC"][w.getMonth()]}};s.getDay=function(w){if(w){return("0"+w.getDate()).substr(-2)}};s.timeSince=timeSince;s.timeCounter=timeCounter;s.timeLeft=timeLeft;s.tap=function(w){q.tap(w).then(function(y){})};s.togglePiston=function(z,y){if((!z)&&(!s.viewerPiston||!s.viewerPiston.app)){return}var A=z?z.i:s.pistonId;if(A){d.cancel(tmrRefresh);var w=!(z?z.e:s.viewerPiston.app.enabled);if(z){z.e=w}else{s.viewerPiston.app.enabled=w}if(w){q.resumePiston(A).then(function(B){s.onRefresh(B)})}else{q.pausePiston(A).then(function(B){s.onRefresh(B)})}}if(y&&e.preventDefault){y.preventDefault()}if(y&&y.stopPropagation){y.stopPropagation()}};s.configurePiston=function(w){s.configuredPistonId=s.configuredPistonId==w.i?null:w.i};s.showPiston=function(w){document.body.scrollTop=0;s.viewerPiston=null;s.pistonId=w.i;s.refresh();window.onSwipeRight=s.hidePiston};s.hidePiston=function(){document.body.scrollTop=0;s.pistonId=null;window.onSwipeRight=null};s.prepareActions=function(z){var B=[];var w=[];var E=[];var F=(z.id<=0);var A=s.viewerPiston.tasks;var D=s.viewerPiston.app.actions;for(action in D){if(D[action].pid==z.id){if(D[action].t){var C=D[action].t;for(t in C){var y=0;for(task in A){if((A[task].type=="cmd")&&(A[task].ownerId==D[action].id)&&(A[task].taskId==C[t].i)){if((y==0)||(y>A[task].time)){y=A[task].time}}}C[t].time=y}}if(F){B.push(D[action])}else{if(D[action].rs==false){E.push(D[action])}else{w.push(D[action])}}}}var y=0;for(task in A){if((A[task].type=="evt")&&(A[task].ownerId==z.id)){if((y==0)||(y>A[task].time)){y=A[task].time}}}z.time=y;z.actions=B;z.trueActions=w;z.falseActions=E;z.$scope=s;if(z.children){for(child in z.children){s.prepareActions(z.children[child])}}};s.hadRecentActivity=function(w){return w&&w.le&&w.le.event&&w.le.event.date&&(timeLeft((new Date(w.le.event.date)).getTime())>-120)};s.toggleViewerOptions=function(){s.viewerPiston.showOptions=!s.viewerPiston.showOptions;s.closeNavBar()};s.getSecondaryStatementName=function(){var w=s.viewerPiston.app.mode;switch(w){case"Latching":return"BUT IF";case"Then-If":return"THEN IF";case"Else-If":return"ELSE IF";case"Or-If":return"OR IF";case"And-If":return"AND IF"}return"IF"};s.capturePiston=function(){var w=document.getElementById("viewerPanel");document.body.scrollTop=0;html2canvas(w).then(function(y){s.capturedImage=y.toDataURL("image/png");s.dialogCapture=o.open({template:"dialog-captured-image",className:"ngdialog-theme-default ngdialog-large",disableAnimation:true,scope:s,showClose:true})})};s.pausePiston=function(w){s.loading=true;q.pausePiston(w).then(function(y){s.init()})};s.resumePiston=function(w){s.loading=true;q.resumePiston(w).then(function(y){s.init()})};s.testPiston=function(w){q.testPiston(w)};s.determineDeviceType=function(w){return q.determineDeviceType(w)};s.initSocialMedia=function(){c.FB.XFBML.parse()};s.toggleSidebar=function(){s.sidebarCollapsed=!s.sidebarCollapsed};var u=navigator.userAgent||navigator.vendor||window.opera;if(u.match(/Android/i)){s.android=true}s.url=window.location.href;s.mobile=window.mobileCheck();s.tablet=(!s.mobile)&&(window.mobileOrTabletCheck());s.formatTime=formatTime;s.utcToString=utcToString;var k=setInterval(function(){if(q.ready()){clearInterval(k);s.init()}},1);if(navigator.geolocation){navigator.geolocation.getCurrentPosition(s.updateLocation)}}]);config.controller("register",["$scope","$rootScope","dataService","$timeout","$interval","$location","$sce","$routeParams","ngDialog","$window",function(k,g,i,c,e,d,f,j,h,b){var a=null;k.loading=false;k.code="";k.hasRegistered=i.listLocations().length>0;k.init=function(){};k.setStatus=function(m){if(a){c.cancel(a)}a=null;k.status=m;if(k.status){a=c(function(){k.setStatus()},10000)}};k.$on("$destroy",function(){if(a){c.cancel(a)}});k.register=function(){k.loading=true;i.registerDashboard(k.code).then(function(m){if(m&&(m.length>=80)&&(m.length<=180)){d.path("/init/"+m)}else{k.setStatus("Sorry, the registration code you provided did not work...")}k.loading=false})};k.cancel=function(){d.path("/")};k.init();var l=navigator.userAgent||navigator.vendor||window.opera;if(l.match(/Android/i)){k.android=true}k.url=window.location.href;k.mobile=window.mobileCheck();k.tablet=(!k.mobile)&&(window.mobileOrTabletCheck());k.formatTime=formatTime;k.utcToString=utcToString}]);config.controller("piston",["$scope","$rootScope","dataService","$timeout","$interval","$location","$sce","$routeParams","ngDialog","$window","$animate",function($scope,$rootScope,dataService,$timeout,$interval,$location,$sce,$routeParams,ngDialog,$window,$animate){var tmrReveal;var tmrStatus;var tmrActivity;var tmrClock;var statusAttribute="$status";$scope.lastLogEntry=0;$scope.error="";$scope.loading=true;$scope.initialized=false;$scope.mode="view";$scope.logging="0";$scope.data=null;$scope.error="";$scope.pistonId=$routeParams.pistonId;$scope.piston=null;$scope.designer={};$scope.showAdvancedOptions=false;$scope.dk="N7zqL6a8Texs4wY5y&y2YPLzus+_dZ%s";$scope.params=$location.search();$scope.insertIndexes={};$scope.warnings={};$scope.evalType="v";$scope.evalText="";$scope.evals=[];$scope.lastEval=0;$scope.category="0";$scope.categories=[];if($scope.params){$location.search({})}$scope.stack={undo:[],redo:[]};$scope.weekDays=["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"];$scope.yearMonths=["January","February","March","April","May","June","July","August","September","October","November","December"];$scope.render=function(cancelTimer){if(($scope.mode=="view")&&($scope.view.trace)){if(!tmrClock){tmrClock=$interval($scope.render,1000)}if($scope.trace){}}else{if(tmrClock){$timeout.cancel(tmrClock)}tmrClock=null}};$scope.setStatus=function(status){if(status){console.log(status)}if(tmrStatus){$timeout.cancel(tmrStatus)}tmrStatus=null;$scope.status=status;if($scope.status){tmrStatus=$timeout(function(){$scope.setStatus()},10000)}};$scope.version=function(){return $window.version()};$scope.encodeEmoji=function(value){if(!value){return""}return value.replace(/([\uD83C-\uDBFF][\uDC00-\uDFFF])/g,function(match){return encodeURIComponent(match)})};$scope.listAllPistons=function(){var result=[];var locations=dataService.listLocations();for(l in locations){var instances=dataService.listInstances(locations[l].id);for(i in instances){for(p in instances[i].pistons){result.push({v:instances[i].pistons[p].id,n:locations[l].name+" \\ "+instances[i].name+" \\ "+instances[i].pistons[p].name})}}}return result};$scope.listAvailableContacts=function(){var result=[];for(i in $scope.instance.contacts){var contact=$scope.instance.contacts[i];result.push({v:i,n:(contact.f+" "+contact.l).trim()+(contact.p?" (PUSH)":(contact.t?" ("+contact.t+")":"")),an:contact.an})}if(!result.length){result.push({v:"no one",n:"No available contacts"})}return result};$scope.getPistonName=function(pistonId){var locations=dataService.listLocations();for(l in locations){var instances=dataService.listInstances(locations[l].id);for(i in instances){for(p in instances[i].pistons){if(instances[i].pistons[p].id==pistonId){return locations[l].name+" \\ "+instances[i].name+" \\ "+instances[i].pistons[p].name}}}}return pistonId};$scope.getLifxSceneName=function(sceneId){if(!$scope.instance.lifx.scenes){return sceneId}var sceneName=$scope.instance.lifx.scenes[sceneId];if(!sceneName){return sceneId}return sceneName};$scope.getLifxSelectorName=function(selectorId){if(!$scope.instance.settings){return selectorId}var name=$scope.instance.lifx.lights?$scope.instance.lifx.lights[selectorId]:null;if(name){return name}name=$scope.instance.lifx.groups?$scope.instance.lifx.groups[selectorId]:null;if(name){return name}name=$scope.instance.lifx.locations?$scope.instance.lifx.locations[selectorId]:null;if(name){return name}name=$scope.instance.lifx.scenes?$scope.instance.lifx.scenes[selectorId]:null;if(name){return name}return selectorId};$scope.getModeName=function(modeId){for(modeIndex in $scope.location.modes){if($scope.location.modes[modeIndex].id==modeId){return $scope.location.modes[modeIndex].name}}return modeId};$scope.updateActivity=function(init){if($scope.$$destroyed){return}if($scope.mode!="view"){return}if(tmrActivity){$timeout.cancel(tmrActivity)}if(init){tmrActivity=$timeout($scope.updateActivity,10000);return}dataService.getActivity($scope.pistonId,$scope.lastLogEntry).then(function(response){if($scope.$$destroyed){return}if(response.error=="ERR_INVALID_ID"){$scope.home();return}if(response&&response.activity){if(response.activity.state){$scope.state=response.activity.state}if(response.activity.logs&&response.activity.logs.length){$scope.logs=response.activity.logs.concat($scope.logs)}if(response.activity.trace){$scope.trace=response.activity.trace}if(response.activity.localVars){$scope.localVars=response.activity.localVars}if(response.activity.memory){$scope.memory=response.activity.memory}if(response.activity.lastExecuted){$scope.lastExecuted=response.activity.lastExecuted}if(response.activity.nextSchedule){$scope.nextSchedule=response.activity.nextSchedule}if(response.activity.schedules){$scope.schedules=response.activity.schedules}if(response.activity.name){$scope.meta.name=response.activity.name}if($scope.logs&&$scope.logs.length){$scope.lastLogEntry=$scope.logs[0].t}if(response.activity.globalVars){$scope.updateGlobalVars(response.activity.globalVars)}}tmrActivity=$timeout($scope.updateActivity,3000)},function(error){tmrActivity=$timeout($scope.updateActivity,3000)})};$scope.updateGlobalVars=function(globalVars){$scope.globalVars=$scope.globalVars instanceof Object?$scope.globalVars:{};for(varName in globalVars){var varType=globalVars[varName].t;var varValue=globalVars[varName].v;var v=$scope.globalVars[varName];if(!v){$scope.globalVars[varName]={t:varType,v:varValue}}else{if(v.t!=varType){v.t=varType}if(v.v!=varValue){v.v=varValue}}}for(varName in $scope.globalVars){if(!globalVars[varName]){delete ($scope.globalVars[varName])}}};$scope.init=function(){if($scope.$$destroyed){return}dataService.setStatusCallback($scope.setStatus);$scope.loading=true;if($scope.piston){$scope.loading=true}dataService.getPiston($scope.pistonId).then(function(response){if($scope.$$destroyed){return}$scope.endpoint=data.endpoint+"execute/"+$scope.pistonId;try{var showOptions=$scope.piston?!!$scope.showOptions:false;if(!response||!response.data||!response.data.piston){$scope.error=$sce.trustAsHtml("Sorry, an error occurred while retrieving the piston data.");$scope.loading=false;return}$scope.piston=response.data.piston;$scope.validatePiston($scope.piston);$scope.meta=response.data.meta?response.data.meta:{};$scope.db=response.db;$scope.location=dataService.getLocation();$scope.instance=dataService.getInstance();$scope.view=dataService.loadFromStore("view")||{variables:false,elseIfs:false,restrictions:false,whens:false,advancedStatements:false};$scope.subscriptions=response.data.subscriptions?response.data.subscriptions:{};$scope.logs=response.data.logs?response.data.logs:[];$scope.lastLogEntry=($scope.logs&&$scope.logs.length)?$scope.logs[0].t:0;$scope.stats=response.data.stats?response.data.stats:{};$scope.state=response.data.state?response.data.state:"";$scope.trace=response.data.trace?response.data.trace:{};$scope.logging=""+(response.data.logging?response.data.logging:0);$scope.memory=response.data.memory?response.data.memory:0;$scope.lastExecuted=response.data.lastExecuted;$scope.nextSchedule=response.data.nextSchedule;$scope.schedules=response.data.schedules;$scope.categories=$scope.getCategories();$scope.category=$scope.meta.category?$scope.meta.category:"0";$scope.lifx={lights:!!$scope.instance.settings&&!!$scope.instance.lifx.lights?$scope.objectToArray($scope.instance.lifx.lights):[],groups:!!$scope.instance.settings&&!!$scope.instance.lifx.groups?$scope.objectToArray($scope.instance.lifx.groups):[],locations:!!$scope.instance.settings&&!!$scope.instance.lifx.locations?$scope.objectToArray($scope.instance.lifx.locations):[],scenes:!!$scope.instance.settings&&!!$scope.instance.lifx.scenes?$scope.objectToArray($scope.instance.lifx.scenes):[]};$scope.initChart();if($scope.instance&&$scope.instance.devices){$scope.anonymizeDevices($scope.instance.devices)}if($scope.instance&&$scope.instance.contacts){$scope.anonymizeContacts($scope.instance.contacts)}$scope.devices=$scope.listAvailableDevices();$scope.contacts=$scope.listAvailableContacts();$scope.virtualDevices=$scope.listAvailableVirtualDevices();window.scope=$scope;$scope.localVars=response.data.localVars;$scope.globalVars=$scope.instance.globalVars;$scope.systemVars=response.data.systemVars;$scope.systemVarNames=[];for(name in $scope.systemVars){$scope.systemVarNames.push(name)}$scope.meta.build=$scope.meta.build?1*$scope.meta.build:0;if($scope.piston&&($scope.meta.build==0)){$scope.piston.z=$scope.params&&$scope.params.description?$scope.params.description:"";$scope.mode="edit";if($scope.params&&$scope.params.type!="blank"){switch($scope.params.type){case"duplicate":if($scope.params.piston){$scope.loading=true;dataService.getPiston($scope.params.piston).then(function(response){$scope.loading=false;if(response&&response.data&&response.data.piston){$scope.piston.o=response.data.piston.o?response.data.piston.o:{};$scope.piston.r=response.data.piston.r?response.data.piston.r:[];$scope.piston.rn=!!response.data.piston.rn;$scope.piston.rop=response.data.piston.rop?response.data.piston.rop:"and";$scope.piston.s=response.data.piston.s?response.data.piston.s:[];$scope.piston.v=response.data.piston.v?response.data.piston.v:[]}$scope.initialized=true;$scope.loading=false});return}break;case"restore":if($scope.params.bin){$scope.loading=true;dataService.loadFromBin($scope.params.bin).then(function(response){var piston=response.data;$scope.loading=false;if(piston){$scope.piston.o=piston.o?piston.o:{};$scope.piston.r=piston.r?piston.r:[];$scope.piston.rn=!!piston.rn;$scope.piston.rop=piston.rop?piston.rop:"and";$scope.piston.s=piston.s?piston.s:[];$scope.piston.v=piston.v?piston.v:[];$scope.piston.z=piston.z?piston.z:""}$scope.initialized=true;$scope.loading=false;if(!!piston&&(piston.l instanceof Object)&&($scope.objectToArray(piston.l).length)){$scope.rebuildPiston(piston.l)}});return}break}}}if($scope.mode=="edit"){$scope.loadStack()}else{$scope.updateActivity(true)}$scope.piston.o=$scope.piston.o?$scope.piston.o:{cto:0,ced:0};$scope.piston.r=$scope.piston.r?$scope.piston.r:[];$scope.piston.s=$scope.piston.s?$scope.piston.s:[];$scope.piston.rop=$scope.piston.rop?$scope.piston.rop:"and";$scope.piston.rn=!!$scope.piston.rn;$scope.piston.v=$scope.piston.v?$scope.piston.v:[];$scope.piston.z=$scope.piston.z||"";$scope.initialized=true;$scope.loading=false;$scope.render()}catch(e){alert(e)}})};$scope.initChart=function(){$scope.chart={type:"bar",labels:[],series:["Event delay","Load time","Execution time","Update time"],data:[[],[],[],[]],onClick:function(points,evt){},datasetOverride:[{cubicInterpolationMode:"monotone",lineTension:0,yAxisID:"y-axis-1",fill:true,pointRadius:0,borderColor:"#88bbee",borderWidth:0,backgroundColor:"#99ccff"},{cubicInterpolationMode:"monotone",lineTension:0,yAxisID:"y-axis-1",fill:true,pointRadius:0,borderColor:"#eebb88",borderWidth:"0px",backgroundColor:"#ffcc99"},{cubicInterpolationMode:"monotone",lineTension:0,yAxisID:"y-axis-1",fill:true,pointRadius:0,borderColor:"#ee88bb",borderWidth:"0px",backgroundColor:"#ff99cc"},{cubicInterpolationMode:"monotone",lineTension:0,yAxisID:"y-axis-1",fill:true,pointRadius:0,borderColor:"#999",borderWidth:1,backgroundColor:"#ccff99"}],options:{legend:{display:true},multiTooltipTemplate:"<%=datasetLabel%> : <%= value %>ms",showLines:true,fill:true,scales:{xAxes:[{type:"time"}],yAxes:[{id:"y-axis-1",stacked:true,type:"linear",display:true,position:"left"}]},pan:{enabled:true,mode:"x"},zoom:{enabled:true,mode:"x"}}};if($scope.stats&&$scope.stats.timing){for(var i=0;i<$scope.stats.timing.length;i++){$scope.chart.labels.push(new Date($scope.stats.timing[i].t));$scope.chart.data[0].push($scope.stats.timing[i].d);$scope.chart.data[1].push($scope.stats.timing[i].l);$scope.chart.data[2].push($scope.stats.timing[i].e);$scope.chart.data[3].push($scope.stats.timing[i].u)}}};$scope.$on("$destroy",function(){if(tmrStatus){$timeout.cancel(tmrStatus)}if(tmrReveal){$timeout.cancel(tmrReveal)}if(tmrActivity){$timeout.cancel(tmrActivity)}if(tmrClock){$timeout.cancel(tmrClock)}});$scope.copy=function(object){return angular.fromJson(angular.toJson(object))};$scope.home=function(){$scope.initialized=false;$location.path("/")};$scope.toggleView=function(item){if(item){$scope.view[item]=!$scope.view[item]}dataService.saveToStore("view",$scope.view)};$scope.revealBin=function(){$scope.revealing=!$scope.revealing;if(tmrReveal){$timeout.cancel(tmrReveal)}if($scope.revealing){tmrReveal=$timeout(function(){$scope.revealing=false;tmrReveal=null},10000)}};$scope.getCategories=function(){var categories=(!!$scope.instance&&!!$scope.instance.settings&&($scope.instance.settings.categories instanceof Array))?$scope.copy($scope.instance.settings.categories):[];if(!categories.length){categories=[{n:"Uncategorized",t:"d",i:0}]}return categories};$scope.edit=function(){$scope.mode="edit";$scope.init();$("viewer")[0].scrollTop=0};$scope.cancel=function(){$scope.mode="view";$scope.init()};$scope.enableAutomaticBackup=function(){dataService.generateBackupBin().then(function(response){var binId=response.data;dataService.setPistonBin($scope.pistonId,binId).then(function(response){$scope.meta.bin=binId;$scope.save(true);$scope.loading=false})})};$scope.save=function(saveToBinOnly){$scope.loading=true;var piston=$scope.compilePiston({id:$scope.pistonId,o:$scope.piston.o,s:$scope.piston.s,v:$scope.piston.v,r:$scope.piston.r,rop:$scope.piston.rop,rn:$scope.piston.rn,z:$scope.piston.z,n:$scope.meta.name});var promise=dataService.setPiston(piston,$scope.meta.bin,saveToBinOnly);if(promise){promise.then(function(response){if(saveToBinOnly){return}$scope.loading=false;if(response&&response.data&&response.data.build){$scope.meta.active=response.data.active;$scope.meta.modified=response.data.modified;$scope.meta.build=response.data.build;$scope.saveStack(true);$scope.mode="view";$scope.init()}})}};$scope.pause=function(){$scope.loading=true;dataService.pausePiston($scope.pistonId).then(function(data){$scope.loading=false;if(data&&data.status&&(data.status=="ST_SUCCESS")){$scope.meta.active=data.active;$scope.subscriptions={};$scope.updateActivity()}})};$scope.setLoggingLevel=function(obj){$scope.loading=true;dataService.setPistonLogging($scope.pistonId,$scope.logging).then(function(data){$scope.loading=false})};$scope.setCategory=function(){$scope.loading=true;dataService.setPistonCategory($scope.pistonId,$scope.category).then(function(data){$scope.loading=false})};$scope.resume=function(){$scope.loading=true;dataService.resumePiston($scope.pistonId).then(function(data){$scope.loading=false;if(data&&data.status&&(data.status=="ST_SUCCESS")){$scope.meta.active=data.active;if(data.subscriptions){$scope.subscriptions=data.subscriptions}$scope.updateActivity()}})};$scope.del=function(){$scope.loading=true;dataService.deleteFromStore("stack"+$scope.pistonId);dataService.deletePiston($scope.pistonId).then(function(data){$scope.closeDialog();$location.path("/")})};$scope.padComment=function(comment,sz){if(!comment){comment=""}sz=sz-6-comment.replace(/\u200E/g,"").trim().length;while(sz>0){comment+=" ";sz--}return"/* "+comment+" */"};$scope.range=function(n){return new Array(n)};$scope.wiki=function(item){$scope.wikiUrl=$sce.trustAsUrl("https://wiki.webcore.co/"+item+"?content-only");$window.mydialog=ngDialog.open({template:"dialog-wiki",className:"ngdialog-theme-default ngdialog-large ngdialog-wiki",closeByDocument:true,disableAnimation:true,scope:$scope})};$scope.formatVariableValue=function(variable,name){if((variable.v==null)&&!!name&&$scope.localVars){variable=$scope.copy(variable);variable.v=$scope.localVars[name]}var t=(name=="$localNow")||(name=="$utc")?"long":variable.t;if((variable.v==="")||(variable.v===null)||((variable.v instanceof Array)&&!variable.v.length)){return"(not set)"}switch(t){case"time":return utcToTimeString(variable.v);case"datetime":return utcToString(variable.v);case"date":return utcToDateString(variable.v);case"contact":return $scope.renderContactNameList(variable.v);case"device":return $scope.renderDeviceNameList(variable.v)}if(variable.v instanceof Object){return angular.toJson(variable.v)}return variable.v};$scope.deleteDialog=function(){$scope.designer.dialog=ngDialog.open({template:"dialog-del-piston",className:"ngdialog-theme-default ngdialog-large",closeByDocument:false,disableAnimation:true,scope:$scope})};$scope.listDevicesWithAttributes=function(attributes){if(!attributes||!(attributes instanceof Array)||!attributes.length){return $scope.instance.devices}if(attributes.length===1&&attributes[0]===statusAttribute){return $scope.instance.devices}var isThreeAxis;attributes=attributes.filter(function(a){switch(a){case"orientation":case"axisX":case"axisY":case"axisZ":isThreeAxis=true;case statusAttribute:return false}return true}).concat(isThreeAxis?"threeAxis":[]);var result={};for(d in $scope.instance.devices){var device=$scope.instance.devices[d];var found=0;for(a in device.a){if(attributes.indexOf(device.a[a].n)>=0){found++;if(found==attributes.length){break}}}if(found==attributes.length){result[d]=device}}return result};$scope.rebuildPiston=function(legend){if(!legend){return}for(key in legend){var item=legend[key];item.id="";switch(item.t){case"device":item.i=$scope.listDevicesWithAttributes(item.a);break;case"contact":item.i=$scope.instance.contacts;break;case"mode":item.i=$scope.instance.virtualDevices.mode.o;for(i in item.i){if(item.i[i]==item.n){item.id=i;break}}break;case"routine":item.i=$scope.instance.virtualDevices.routine.o;break}}$scope.designer={legend:legend};$scope.designer.dialog=ngDialog.open({template:"dialog-rebuild-piston",className:"ngdialog-theme-default ngdialog-large",closeByDocument:false,disableAnimation:true,scope:$scope})};$scope.doRebuildPiston=function(){$scope.piston=$scope.compilePiston($scope.piston,false,$scope.designer.legend);$scope.closeDialog()};$scope.doValidatePiston=function(){$scope.validatePiston($scope.piston)};$scope.getExpressionConfig=function(){var attributes=[];for(attribute in $scope.db.attributes){attributes.push(": "+attribute+"]");if(attribute=="threeAxis"){attributes.push(": axisX]");attributes.push(": axisY]");attributes.push(": axisZ]");attributes.push(": orientation]")}}return{autocomplete:[{words:[]},{words:$scope.listAutoCompleteFunctions(),cssClass:"hl kwd"},{words:$scope.listAutoCompleteVariables(),cssClass:"hl var"},{words:$scope.listAutoCompleteDevices(),cssClass:"hl dev"},{words:attributes,cssClass:"hl dev"},{words:[/([0-9]+)(\.[0-9]+)?/g],cssClass:"hl num"}]}};$scope.removeFromArray=function(array,value){if(!(array instanceof Array)){return}var idx=array.indexOf(value);if(idx!==-1){array.splice(idx,1)}return array};$scope.deleteObject=function(obj,parent){var dialog=!obj;if(dialog){obj=$scope.designer.$obj;parent=$scope.designer.parent}if(!obj){return}if((parent instanceof Array)&&(obj)){$scope.autoSave();parent=$scope.removeFromArray(parent,obj);if(dialog){$scope.closeDialog()}}if(parent&&(parent.t=="action")&&(parent.k instanceof Array)&&(obj)){$scope.autoSave();parent.k=$scope.removeFromArray(parent.k,obj);if(dialog){$scope.closeDialog()}}};$scope.getIFTTTUri=function(eventName){var uri=dataService.getApiUri();if(!uri){return"An error has occurred retrieving the IFTTT Maker URL"}return uri+"ifttt/"+eventName};$scope.toggleAdvancedOptions=function(){$scope.designer.showAdvancedOptions=!$scope.designer.showAdvancedOptions};$scope.getClipboard=function(){var clipboard=dataService.loadFromStore("clipboard");if(!clipboard){clipboard=[]}return clipboard};$scope.getClipboardItems=function(itemType){var clipboard=$scope.getClipboard();var result=[];for(i in clipboard){if(clipboard[i].t.startsWith(itemType)){result.push(clipboard[i])}}return result};$scope.saveToClipboard=function(object,objectType){var clipboard=$scope.getClipboard();clipboard.push({s:(new Date()).getTime(),t:objectType,o:$scope.copy(object)});if(clipboard.length>MAX_STACK_SIZE){clipboard=clipboard.slice(-MAX_STACK_SIZE)}dataService.saveToStore("clipboard",clipboard)};$scope.deleteClipboardItem=function(item){var clipboard=$scope.getClipboard();$scope.removeFromArray($scope.designer.clipboard,item);for(i=0;i=0)?"1":"0";statement.tcp=$scope.designer.tcp;statement.tep=$scope.designer.tep;statement.tsp=$scope.designer.tsp;statement.z=$scope.designer.description;statement.r=statement.r?statement.r:[];statement.rop=$scope.designer.roperator;statement.rn=$scope.designer.rnot=="1";statement.di=$scope.designer.disabled=="1";switch(statement.t){case"action":statement.d=$scope.designer.devices;statement.k=statement.k?statement.k:[];break;case"do":statement.s=statement.s?statement.s:[];break;case"on":statement.c=statement.c?statement.c:[];statement.o="or";statement.n=false;statement.s=statement.s?statement.s:[];break;case"if":statement.o=$scope.designer.operator;statement.n=$scope.designer.not=="1";statement.c=statement.c?statement.c:[];statement.s=statement.s?statement.s:[];statement.ei=statement.ei?statement.ei:[];statement.e=statement.e?statement.e:[];break;case"switch":statement.lo=$scope.designer.operand.data;statement.cs=statement.cs||[];statement.e=statement.e?statement.e:[];statement.ctp=$scope.designer.ctp;break;case"for":statement.x=$scope.designer.x;statement.lo=$scope.designer.operand.data;statement.lo2=$scope.designer.operand2.data;statement.lo3=$scope.designer.operand3.data;statement.s=statement.s?statement.s:[];break;case"each":statement.x=$scope.designer.x;statement.lo=$scope.designer.operand.data;statement.s=statement.s?statement.s:[];break;case"while":statement.o=$scope.designer.operator;statement.n=$scope.designer.not=="1";statement.c=statement.c?statement.c:[];statement.s=statement.s?statement.s:[];break;case"every":statement.lo=$scope.designer.operand.data;statement.lo2=$scope.designer.operand2.data;if(statement.lo2.c instanceof Date){statement.lo2.c=statement.lo2.c.getHours()*60+statement.lo2.c.getMinutes()}statement.lo3=$scope.designer.operand3.data;statement.s=statement.s?statement.s:[];break;case"repeat":statement.o=$scope.designer.operator;statement.n=$scope.designer.not=="1";statement.c=statement.c?statement.c:[];statement.s=statement.s?statement.s:[];break;case"break":break;case"exit":statement.lo=$scope.designer.operand.data;break;default:statement.t=null}if(statement.t){statement.$$html=null;if($scope.designer.$new){if($scope.designer.parent instanceof Array){$scope.designer.parent.push(statement)}else{if(($scope.designer.parent.s)&&($scope.designer.parent.s instanceof Array)){$scope.designer.parent.s.push(statement)}else{$scope.designer.parent.s=[statement]}}}else{$scope.designer.$statement=statement}}$scope.doValidatePiston();$scope.closeDialog();if(nextDialog){switch(statement.t){case"action":$scope.addTask(statement);return;case"if":$scope.addCondition(statement.c,false,defaultType);return;case"on":$scope.addEvent(statement.c);return;case"while":$scope.addCondition(statement.c);return;case"do":case"for":case"each":case"repeat":case"every":$scope.addStatement(statement.s);return;case"switch":$scope.addCase(statement.cs);return}}};$scope.upgradeStatement=function(){$scope.updateStatement();var statement=$scope.designer.$statement;if(statement&&statement.c&&(statement.c instanceof Array)){statement.c=[{t:"group",n:false,o:"and",c:statement.c}]}};$scope.addCase=function(parent){return $scope.editCase(null,parent)};$scope.editCase=function(_case,parent){if($scope.mode!="edit"){return}var _new=_case?false:true;if(!_case){_case={};_case.t="s";_case.s=[];_case.ro={};_case.ro2={};_case.z=""}$scope.designer={config:$scope.getExpressionConfig()};$scope.designer.$obj=_case;$scope.designer.$case=_case;$scope.designer.$new=_new;$scope.designer.parent=parent;$scope.designer.type=_case.t;$scope.designer.operand={data:_case.ro,multiple:false};$scope.designer.operand2={data:_case.ro2,multiple:false};$scope.designer.autoDialogs=true;$scope.designer.description=_case.z;window.designer=$scope.designer;$scope.validateOperand($scope.designer.operand);$scope.validateOperand($scope.designer.operand2);$scope.designer.dialog=ngDialog.open({template:"dialog-edit-case",className:"ngdialog-theme-default ngdialog-large",closeByDocument:false,disableAnimation:true,scope:$scope})};$scope.updateCase=function(nextDialog){$scope.autoSave();var _case=$scope.designer.$case;_case.t=$scope.designer.type;_case.s=_case.s||[];_case.ro=$scope.designer.operand.data;_case.ro2=$scope.designer.operand2.data;_case.z=$scope.designer.description;if(_case.t){_case.$$html=null;_case.$$html2=null;if($scope.designer.$new){if($scope.designer.parent instanceof Array){$scope.designer.parent.push(_case)}else{if(($scope.designer.parent.cs)&&($scope.designer.parent.cs instanceof Array)){$scope.designer.parent.cs.push(_case)}else{$scope.designer.parent.cs=[_case]}}}else{$scope.designer.$case=_case}}$scope.doValidatePiston();$scope.closeDialog();if(nextDialog){$scope.addStatement(_case.s);return}};$scope.addEvent=function(parent){return $scope.editEvent(null,parent)};$scope.editEvent=function(event,parent){if($scope.mode!="edit"){return}var _new=!event;if(!event){event={};event.t="event";event.lo={t:"p",d:[],a:null,g:"any",v:null,c:"",x:null,e:""};event.z="";event.sm="auto"}$scope.designer={config:$scope.getExpressionConfig(),clipboard:_new?$scope.getClipboardItems("event"):[]};$scope.designer.$event=event;$scope.designer.$obj=event;$scope.designer.type=event.t;$scope.designer.$new=_new;$scope.designer.parent=parent;$scope.designer.comparison={event:true,type:"event",left:{data:event.lo?$scope.copy(event.lo):{},event:true}};$scope.validateComparison($scope.designer.comparison,true);$scope.designer.smode=event.sm;$scope.designer.description=event.z;window.designer=$scope.designer;$scope.designer.dialog=ngDialog.open({template:"dialog-edit-event",className:"ngdialog-theme-default ngdialog-large",closeByDocument:false,disableAnimation:true,scope:$scope})};$scope.updateEvent=function(nextDialog){$scope.autoSave();var event=$scope.designer.$new?{t:$scope.designer.type}:$scope.designer.$event;event.lo=$scope.fixOperand($scope.designer.comparison.left.data);event.sm=$scope.designer.smode;event.z=$scope.designer.description;if(event.t){event.$$html=null;if($scope.designer.$new){if($scope.designer.parent instanceof Array){$scope.designer.parent.push(event)}else{if(($scope.designer.parent.c)&&($scope.designer.parent.c instanceof Array)){$scope.designer.parent.c.push(event)}else{$scope.designer.parent.c=[event]}}}else{$scope.designer.$event=event}}$scope.doValidatePiston();$scope.closeDialog();if(event.t&&nextDialog){$scope.addEvent($scope.designer.parent);return}};$scope.addCondition=function(parent,newElseIf,defaultType,groupingMethod){return $scope.editCondition(null,parent,newElseIf,defaultType,groupingMethod?groupingMethod:(parent?parent.o:null))};$scope.editCondition=function(condition,parent,newElseIf,defaultType,groupingMethod){if($scope.mode!="edit"){return}var _new=!condition;var list=parent instanceof Array?parent:(parent instanceof Object?parent.c:null);var followedBy=(groupingMethod=="followed by")&&(list instanceof Array)&&(list.length>0)&&(list[0]!=condition);if(!condition){condition={};condition.t=defaultType;condition.d=[];condition.n=false;condition.o="and";condition.lo={t:"p",d:[],a:null,g:"any",v:null,c:"",x:null,e:""};condition.co=null;condition.ro={t:"c",d:[],a:null,g:"any",v:null,c:"",x:null,e:""};condition.ro2={t:"c",d:[],a:null,g:"any",v:null,c:"",x:null,e:""};condition.to={t:"c",d:[],a:null,g:"any",v:null,c:"",x:null,e:""};condition.to2={t:"c",d:[],a:null,g:"any",v:null,c:"",x:null,e:""};condition.z="";condition.sm="auto";condition.ts=[];condition.fs=[]}$scope.designer={config:$scope.getExpressionConfig(),clipboard:_new?$scope.getClipboardItems("condition"):[]};$scope.designer.$condition=condition;$scope.designer.followedBy=followedBy;$scope.designer.$obj=condition;$scope.designer.type=condition.t;$scope.designer.$new=!defaultType&&!!condition.t?false:true;$scope.designer.newElseIf=newElseIf;$scope.designer.page=$scope.designer.$new&&!defaultType?0:1;$scope.designer.parent=parent;$scope.designer.devices=condition.d;$scope.designer.not=condition.n?"1":"0";$scope.designer.operator=condition.o;$scope.designer.comparison={type:"condition",followedBy:followedBy,left:{data:condition.lo?$scope.copy(condition.lo):{},showSubDevices:true,showInteraction:true},operator:condition.co,right:{data:condition.ro?$scope.copy(condition.ro):{}},right2:{data:condition.ro2?$scope.copy(condition.ro2):{}},time:{data:condition.to?$scope.copy(condition.to):{t:"c",c:0},dataType:"duration"},time2:{data:condition.to2?$scope.copy(condition.to2):{t:"c",c:0},dataType:"duration"}};if(followedBy){$scope.designer.comparison.within={data:condition.wd?$scope.copy(condition.wd):{t:"c",c:1,vt:"m"},style:"success",dataType:"duration",hideMilliseconds:true};$scope.designer.comparison.withinOpt=(condition.wt?condition.wt:"l")}$scope.validateComparison($scope.designer.comparison,true);$scope.designer.smode=condition.sm;$scope.designer.description=condition.z;window.designer=$scope.designer;$scope.designer.items=[{type:"condition",name:"Condition",icon:"code",cssClass:"btn-info"},{type:"group",name:"Group",icon:"code-fork",cssClass:"btn-warning"}];$scope.designer.dialog=ngDialog.open({template:"dialog-edit-condition",className:"ngdialog-theme-default ngdialog-large",closeByDocument:false,disableAnimation:true,scope:$scope})};$scope.fixOperand=function(data){switch(data.vt){case"time":data.c=data.c instanceof Date?data.c.getHours()*60+data.c.getMinutes():data.c;break;case"date":case"datetime":data.c=data.c instanceof Date?data.c.getTime():(new Date(data.c)).getTime();break}return data};$scope.updateCondition=function(nextDialog){$scope.autoSave();var condition=$scope.designer.$new?{t:$scope.designer.type}:$scope.designer.$condition;switch(condition.t){case"condition":condition.lo=$scope.fixOperand($scope.designer.comparison.left.data);condition.co=$scope.designer.comparison.operator;condition.ro=$scope.fixOperand($scope.designer.comparison.right.data);condition.ro2=$scope.fixOperand($scope.designer.comparison.right2.data);condition.to=$scope.designer.comparison.time.data;condition.to2=$scope.designer.comparison.time2.data;if($scope.designer.followedBy){condition.wd=$scope.designer.comparison.within.data;condition.wt=$scope.designer.comparison.withinOpt}break;case"group":condition.c=condition.c?condition.c:[];condition.o=$scope.designer.operator;condition.n=$scope.designer.not=="1";if($scope.designer.followedBy){condition.wd=$scope.designer.comparison.within.data;condition.wt=$scope.designer.comparison.withinOpt}break}condition.sm=$scope.designer.smode;condition.ts=condition.ts?condition.ts:[];condition.fs=condition.fs?condition.fs:[];condition.z=$scope.designer.description;if(condition.t){condition.$$html=null;if($scope.designer.$new){if($scope.designer.newElseIf){var elseIf={o:"and",n:false,c:[],s:[]};elseIf.c.push(condition);$scope.designer.parent.push(elseIf)}else{if($scope.designer.parent instanceof Array){$scope.designer.parent.push(condition)}else{if(($scope.designer.parent.c)&&($scope.designer.parent.c instanceof Array)){$scope.designer.parent.c.push(condition)}else{$scope.designer.parent.c=[condition]}}}}else{$scope.designer.$condition=condition}}$scope.doValidatePiston();$scope.closeDialog();if(condition.t&&nextDialog){$scope.addCondition(condition.t=="group"?condition:$scope.designer.parent);return}};$scope.upgradeCondition=function(){$scope.updateCondition();var parent=$scope.designer.parent;if($scope.designer.$condition&&parent&&(parent instanceof Array)){var index=parent.indexOf($scope.designer.$condition);if(index>=0){var condition={};condition.t=$scope.designer.$condition.t;condition.n=$scope.designer.$condition.n;condition.o=$scope.designer.$condition.o;condition.c=$scope.designer.$condition.c;$scope.designer.$condition={};$scope.designer.$condition.t="group";$scope.designer.$condition.n=false;$scope.designer.$condition.o="and";$scope.designer.$condition.c=[condition];parent[index]=$scope.designer.$condition}}};$scope.editConditionGroup=function(group,parent,groupingMethod){if($scope.mode!="edit"){return}var followedBy=(groupingMethod=="followed by")&&(parent instanceof Array)&&(parent.length>0)&&(parent[0]!=group);$scope.designer={operator:group.o||"and",not:group.n?"1":"0",description:(group.t=="group"?group.z:group.zc)};$scope.designer.group=group;$scope.designer.followedBy=followedBy;$scope.designer.$obj=group;$scope.designer.parent=parent;if(followedBy){$scope.designer.within={data:group.wd?$scope.copy(group.wd):{t:"c",c:1,vt:"m"},style:"success",dataType:"duration",hideMilliseconds:true};$scope.designer.withinOpt=(group.wt?group.wt:"l");$scope.validateOperand($scope.designer.within)}window.designer=$scope.designer;$scope.designer.dialog=ngDialog.open({template:"dialog-edit-condition-group",className:"ngdialog-theme-default ngdialog-large",closeByDocument:false,disableAnimation:true,scope:$scope})};$scope.updateConditionGroup=function(){$scope.autoSave();var group=$scope.designer.group;group.n=$scope.designer.not=="1";group.o=$scope.designer.operator;if(group.t=="group"){group.z=$scope.designer.description;if($scope.designer.followedBy){group.wd=$scope.designer.within.data;group.wt=$scope.designer.withinOpt}}else{group.zc=$scope.designer.description}$scope.closeDialog()};$scope.addRestriction=function(parent){return $scope.editRestriction(null,parent)};$scope.editRestriction=function(restriction,parent){if($scope.mode!="edit"){return}var _new=!restriction;if(!restriction){restriction={};restriction.t=null;restriction.d=[];restriction.rn=false;restriction.rop="and";restriction.lo={t:"p",d:[],a:null,g:"any",v:null,c:"",x:null,e:""};restriction.co=null;restriction.ro={t:"c",d:[],a:null,g:"any",v:null,c:"",x:null,e:""};restriction.ro2={t:"c",d:[],a:null,g:"any",v:null,c:"",x:null,e:""};restriction.to={t:"c",d:[],a:null,g:"any",v:null,c:"",x:null,e:""};restriction.to2={t:"c",d:[],a:null,g:"any",v:null,c:"",x:null,e:""};restriction.z=""}$scope.designer={config:$scope.getExpressionConfig(),clipboard:_new?$scope.getClipboardItems("restriction"):[]};$scope.designer.$restriction=restriction;$scope.designer.$obj=restriction;$scope.designer.type=restriction.t;$scope.designer.$new=restriction.t?false:true;$scope.designer.page=$scope.designer.$new?0:1;$scope.designer.parent=parent;$scope.designer.devices=restriction.d;$scope.designer.not=restriction.rn?"1":"0";$scope.designer.operator=restriction.rop;$scope.designer.comparison={type:"restriction",left:{data:restriction.lo?$scope.copy(restriction.lo):{}},operator:restriction.co,right:{data:restriction.ro?$scope.copy(restriction.ro):{}},right2:{data:restriction.ro2?$scope.copy(restriction.ro2):{}},time:{data:restriction.to?$scope.copy(restriction.to):{t:"c",c:0},dataType:"duration"},time2:{data:restriction.to2?$scope.copy(restriction.to2):{t:"c",c:0},dataType:"duration"}};$scope.validateComparison($scope.designer.comparison,true);$scope.designer.smode=restriction.sm;$scope.designer.description=restriction.z;window.designer=$scope.designer;$scope.designer.items=[{type:"restriction",name:"Restriction",icon:"code",cssClass:"btn-info"},{type:"group",name:"Group",icon:"code-fork",cssClass:"btn-warning"}];$scope.designer.dialog=ngDialog.open({template:"dialog-edit-restriction",className:"ngdialog-theme-default ngdialog-large",closeByDocument:false,disableAnimation:true,scope:$scope})};$scope.updateRestriction=function(nextDialog){$scope.autoSave();var restriction=$scope.designer.$new?{t:$scope.designer.type}:$scope.designer.$restriction;switch(restriction.t){case"restriction":restriction.lo=$scope.fixOperand($scope.designer.comparison.left.data);restriction.co=$scope.designer.comparison.operator;restriction.ro=$scope.fixOperand($scope.designer.comparison.right.data);restriction.ro2=$scope.fixOperand($scope.designer.comparison.right2.data);restriction.to=$scope.designer.comparison.time.data;restriction.to2=$scope.designer.comparison.time2.data;break;case"group":restriction.r=restriction.r?restriction.r:[];restriction.rop=$scope.designer.operator;restriction.rn=$scope.designer.not=="1";break}restriction.z=$scope.designer.description;if(restriction.t){restriction.$$html=null;if($scope.designer.$new){if($scope.designer.parent instanceof Array){$scope.designer.parent.push(restriction)}else{if(($scope.designer.parent.r)&&($scope.designer.parent.r instanceof Array)){$scope.designer.parent.r.push(restriction)}else{$scope.designer.parent.r=[restriction]}}}else{$scope.designer.$restriction=restriction}}$scope.doValidatePiston();$scope.closeDialog();if(restriction.t&&nextDialog){$scope.addRestriction(restriction.t=="group"?restriction:$scope.designer.parent);return}};$scope.upgradeRestriction=function(){$scope.updateRestriction();var parent=$scope.designer.parent;if($scope.designer.$restriction&&parent&&(parent instanceof Array)){var index=parent.indexOf($scope.designer.$restriction);if(index>=0){var restriction={};restriction.t=$scope.designer.$restriction.t;restriction.rn=$scope.designer.$restriction.rn;restriction.rop=$scope.designer.$restriction.rop;restriction.c=$scope.designer.$restriction.c;$scope.designer.$restriction={};$scope.designer.$restriction.t="group";$scope.designer.$restriction.rn=false;$scope.designer.$restriction.rop="and";$scope.designer.$restriction.c=[restriction];parent[index]=$scope.designer.$restriction}}};$scope.editRestrictionGroup=function(group,parent){if($scope.mode!="edit"){return}$scope.designer={operator:group.rop||"and",not:group.rn?"1":"0",description:(group.t=="group"?group.z:group.zr)};$scope.designer.group=group;$scope.designer.$obj=group;$scope.designer.parent=parent;window.designer=$scope.designer;$scope.designer.dialog=ngDialog.open({template:"dialog-edit-restriction-group",className:"ngdialog-theme-default ngdialog-large",closeByDocument:false,disableAnimation:true,scope:$scope})};$scope.updateRestrictionGroup=function(){$scope.autoSave();var group=$scope.designer.group;group.rn=$scope.designer.not=="1";group.rop=$scope.designer.operator;if(group.t=="group"){group.z=$scope.designer.description}else{group.zr=$scope.designer.description}$scope.closeDialog()};$scope.addTask=function(parent){return $scope.editTask(null,parent)};$scope.editTask=function(task,parent){if($scope.mode!="edit"){return}if(!task){task={};task.c="";task.a="0";task.m="";task.z=""}var _new=task.c?false:true;$scope.designer={clipboard:_new?$scope.getClipboardItems("task"):[]};var insertIndex=_new?$scope.insertIndexes[parent.$$hashkey]:parent.k.indexOf(task);if(isNaN(insertIndex)){insertIndex=parent.k.length}$scope.designer.insertIndex=insertIndex;$scope.designer.$task=task;$scope.designer.$obj=task;$scope.designer.$new=_new;$scope.designer.page=0;$scope.designer.parent=parent;$scope.designer.command=task.c;$scope.designer.mode=task.m;$scope.designer.description=task.z;$scope.prepareParameters(task);window.designer=$scope.designer;window.scope=$scope;$scope.designer.commands=$scope.listAvailableCommands(parent.d);$("a-ckolor-wheel").remove();$scope.designer.dialog=ngDialog.open({template:"dialog-edit-task",className:"ngdialog-theme-default ngdialog-large",closeByDocument:false,disableAnimation:true,scope:$scope})};$scope.updateTask=function(nextDialog){$scope.autoSave();var task=$scope.designer.$new?{}:$scope.designer.$task;task.c=$scope.designer.command;task.a=$scope.designer.async;task.z=$scope.designer.description;task.m=$scope.designer.mode;if(task.c){task.$$html=null;task.p=[];for(parameterIndex in $scope.designer.parameters){var param=$scope.designer.parameters[parameterIndex].data;if(param.t=="c"){switch(param.vt){case"time":param.c=param.c instanceof Date?param.c.getHours()*60+param.c.getMinutes():param.c;break;case"date":case"datetime":param.c=param.c instanceof Date?param.c.getTime():(new Date(param.c)).getTime();break}}task.p.push(param)}if($scope.designer.$new){if(($scope.designer.parent)&&($scope.designer.parent.k instanceof Array)){$scope.designer.parent.k.push(task);$scope.insertIndexes[parent.$$hashkey]=$scope.designer.insertIndex+1}}else{$scope.designer.$task=task}}var tasks=$scope.designer.parent.k;if(tasks&&tasks.length){var currentIndex=tasks.indexOf(task);var insertIndex=$scope.designer.insertIndex;if(insertIndex>currentIndex){insertIndex--}if(insertIndex>=tasks.length){insertIndex=tasks.length-1}if($scope.designer.insertIndex!=currentIndex){tasks.splice(insertIndex,0,tasks.splice(currentIndex,1)[0])}}$scope.doValidatePiston();$scope.closeDialog();if(nextDialog){$scope.addTask($scope.designer.parent);return}};$scope.addVariable=function(){return $scope.editVariable(null)};$scope.editVariable=function(variable){if($scope.mode!="edit"){return}if(!variable){variable={};variable.t="dynamic";variable.n="";variable.v={data:{}};variable.a="d";variable.z=""}if(variable.v instanceof Array){variable.v={data:{}}}$scope.designer={};$scope.designer.$variable=variable;$scope.designer.$obj=variable;$scope.designer.$new=variable.n?false:true;$scope.designer.page=0;$scope.designer.parent=$scope.piston.v;$scope.designer.type=variable.t;$scope.designer.assignment=variable.a||"d";$scope.designer.name=variable.n;$scope.designer.operand={data:variable.v,multiple:false,dataType:variable.t,optional:true};$scope.designer.description=variable.z;window.designer=$scope.designer;window.scope=$scope;$scope.validateOperand($scope.designer.operand);$scope.designer.dialog=ngDialog.open({template:"dialog-edit-variable",className:"ngdialog-theme-default ngdialog-large",closeByDocument:false,disableAnimation:true,scope:$scope});$scope.refreshSelects()};$scope.updateVariable=function(nextDialog){$scope.autoSave();var variable=$scope.designer.$new?{}:$scope.designer.$variable;variable.t=$scope.designer.operand.dataType;variable.n=$scope.designer.name.trim().replace(/[^a-z0-9]|\s+|\r?\n|\r/gmi,"_");variable.z=$scope.designer.description;variable.a=$scope.designer.assignment;var value=$scope.fixOperand($scope.designer.operand.data);switch(value.t){case"":variable.v=null;break;default:variable.v=value;break}variable.$$html=null;if($scope.designer.$new){$scope.piston.v.push(variable)}else{$scope.designer.variable=variable}$scope.doValidatePiston();$scope.closeDialog();if(nextDialog){$scope.addVariable();return}};$scope.editLocalVariable=function(variable){if(!variable||!variable.n||!variable.t||!!variable.v){return}var value=$scope.localVars[variable.n];if((value instanceof Array)&&(value.length==0)){value=null}if(!variable){return}$scope.designer={};$scope.designer.$variableName=variable.n;$scope.designer.$variable=variable;$scope.designer.$obj=variable;$scope.designer.name=variable.n;$scope.designer.type=variable.t;$scope.designer.operand={data:{t:!value?"":(variable.t=="device"?"d":"c"),c:value,d:value},multiple:false,dataType:variable.t,optional:true,onlyAllowConstants:true,disableExpressions:true};window.designer=$scope.designer;window.scope=$scope;$scope.validateOperand($scope.designer.operand);$scope.designer.dialog=ngDialog.open({template:"dialog-edit-local-variable",className:"ngdialog-theme-default ngdialog-large",closeByDocument:false,disableAnimation:true,scope:$scope})};$scope.updateLocalVariable=function(nextDialog){var variable=$scope.designer.$variable;var value=variable.t=="device"?$scope.designer.operand.data.d:($scope.designer.operand.data.t=="c"?$scope.designer.operand.data.c:null);dataService.setVariable($scope.designer.$variableName,{t:variable.t,v:value},$scope.pistonId).then(function(data){if(data&&data.localVars&&data.id&&(data.id==$scope.pistonId)){$scope.localVars=data.localVars}});$scope.closeDialog()};$scope.addGlobalVariable=function(){return $scope.editGlobalVariable(null)};$scope.editGlobalVariable=function(variableName){if($scope.mode!="edit"){return}var variable=$scope.globalVars[variableName];if(!variable){variable={t:"dynamic",v:""}}$scope.designer={};$scope.designer.$variableName=variableName;$scope.designer.$variable=variable;$scope.designer.$obj=variable;$scope.designer.$new=variableName?false:true;$scope.designer.name=variableName?""+variableName:"@";$scope.designer.type=variable.t;$scope.designer.operand={data:{t:!variable.v?"":(variable.t=="device"?"d":"c"),c:variable.v,d:variable.v},multiple:false,dataType:variable.t,optional:true,onlyAllowConstants:true};window.designer=$scope.designer;window.scope=$scope;$scope.validateOperand($scope.designer.operand);$scope.designer.dialog=ngDialog.open({template:"dialog-edit-global-variable",className:"ngdialog-theme-default ngdialog-large",closeByDocument:false,disableAnimation:true,scope:$scope})};$scope.updateGlobalVariable=function(nextDialog){$scope.autoSave();var variable=$scope.designer.$new?{}:$scope.designer.$variable;variable.t=$scope.designer.operand.dataType;variable.n=$scope.designer.name.trim().replace(/[^@a-z0-9]|\s+|\r?\n|\r/gmi,"_");var value=$scope.designer.operand.data;switch(value.t){case"":variable.v=variable.t=="device"?[]:null;break;case"c":variable.v=value.c?value.c:"";break;case"d":variable.v=value.d?value.d:[];break;default:variable.v=null;break}delete (variable.$$html);dataService.setVariable($scope.designer.$variableName,variable).then(function(data){if(data&&data.globalVars){$scope.updateGlobalVars(data.globalVars)}});$scope.doValidatePiston();$scope.closeDialog();if(nextDialog){$scope.addGlobalVariable();return}};$scope.deleteGlobalVariable=function(){if((!$scope.designer)||(!$scope.designer.$variableName)){return}dataService.setVariable($scope.designer.$variableName,null).then(function(data){if(data&&data.globalVars){$scope.updateGlobalVars(data.globalVars)}});$scope.closeDialog()};$scope.validateGlobalVariableName=function(){if(!$scope.designer){return false}var name=$scope.designer.name;if(!name){return false}if(!name.startsWith("@")){name="@"+name}while(name.startsWith("@@@")){name=name.substr(1)}if($scope.designer.name!=name){$scope.designer.name=name}return name&&(name!="@")&&(name!="@@")&&(($scope.designer.$variableName==name)||!($scope.globalVars[name]))};$scope.getDeviceAttributeValue=function(device,attributeName){for(i in device.a){if(device.a[i].n==attributeName){var result={v:device.a[i].v,t:device.a[i].v};if(result.v==undefined){result.v=""}if((attributeName=="battery")&&(!isNaN(result.v))){result.t=result.t+"%";result.v=Math.floor(parseInt(result.v)/20);if(result.v>4){result.v=4}}if((attributeName=="temperature")&&(!isNaN(result.v))){result.v=Math.round(parseFloat(result.v)).toString()+"°";result.t=result.v}return result}}return{v:"",t:""}};$scope.renderDevice=function(device){var sSwitch=$scope.getDeviceAttributeValue(device,"switch");var sSwitch=sSwitch?'class="fa fa-toggle-off" switch="'+sSwitch+'"':"";var attributes=["temperature","battery","switch","motion","presence"];var result="
"+device.n+"
";for(a in attributes){var value=$scope.getDeviceAttributeValue(device,attributes[a]);result+="
'}return $sce.trustAsHtml(result)};$scope.drag=function(list,index){list.splice(index,1);$scope.autoSave();$scope.doValidatePiston()};$scope.copyVariable=function(list,index){var variable=list[index];for(var i=0;iparameterIndex)){p.data=$scope.copy(task.p[parameterIndex])}$scope.validateOperand(p);$scope.designer.parameters.push(p)}}else{$scope.designer.custom=!!$scope.designer.command;for(i in task.p){var param={dataType:task.p[i].vt,data:$scope.copy(task.p[i])};$scope.validateOperand(param);$scope.designer.parameters.push(param)}}if($scope.designer.command=="setVariable"){$scope.designer.parameters[0].linkedOperand=$scope.designer.parameters[1];$scope.validateOperand($scope.designer.parameters[0])}$scope.refreshSelects()};$scope.renameParameters=function(){if(!$scope.designer.custom){return}for(i in $scope.designer.parameters){$scope.designer.parameters[i].name="Parameter #"+(parseInt(i)+1).toString()+" ("+$scope.designer.parameters[i].dataType+")"}$scope.refreshSelects()};$scope.addParameter=function(dataType){if(!$scope.designer.custom){return}var param={dataType:dataType,name:"",data:{t:"c"}};$scope.validateOperand(param);$scope.designer.parameters.push(param);$scope.renameParameters()};$scope.deleteParameter=function(parameter){if(!$scope.designer.custom){return}var index=$scope.designer.parameters.indexOf(parameter);if(index>-1){$scope.designer.parameters.splice(index,1)}$scope.renameParameters()};$scope.getParameterInputType=function(parameter){switch(parameter.t){case"color":case"duration":case"enum":case"boolean":return parameter.t;case"number":return Math.abs(parameter.M-parameter.m)>360?"number":"range"}return"text"};$scope.getParameterMin=function(parameter){switch(parameter.t){case"level":case"saturation":case"hue":return 0;case"colorTemperature":return 1500}return null};$scope.getParameterMax=function(parameter){switch(parameter.t){case"level":case"saturation":return 100;case"hue":return 360;case"colorTemperature":return 10000}return null};$scope.getContactById=function(contactId){return $scope.instance.contacts[contactId]};$scope.getRoutineById=function(routineId){if($scope.instance.virtualDevices.routine&&$scope.instance.virtualDevices.routine.o){return $scope.instance.virtualDevices.routine.o[routineId]}return null};$scope.getLocationModeById=function(locationModeId){if($scope.instance.virtualDevices.mode&&$scope.instance.virtualDevices.mode.o){return $scope.instance.virtualDevices.mode.o[locationModeId]}return null};$scope.getDeviceById=function(deviceId){if(deviceId==$scope.location.id){return{id:deviceId,n:$scope.location.name,an:"Location"}}return $scope.instance.devices[deviceId]};$scope.getDeviceByName=function(deviceName){for(deviceIndex in $scope.instance.devices){if($scope.instance.devices[deviceIndex].n==deviceName){return mergeObjects({id:deviceIndex},$scope.instance.devices[deviceIndex])}}return null};$scope.getVirtualDeviceById=function(deviceId){if(deviceId==$scope.location.id){return{id:deviceId,name:$scope.location.name}}return $scope.instance.virtualDevices[deviceId]};$scope.getCapabilityById=function(capabilityId){return $scope.db.capabilities[capabilityId]};$scope.getCapabilityByName=function(capabilityName){for(capabilityIndex in $scope.db.capabilities){if($scope.db.capabilities[capabilityIndex].n==capabilityName){return mergeObjects({id:capabilityIndex},$scope.db.capabilities[capabilityIndex])}}return null};$scope.getCommandById=function(commandId){return $scope.db.commands.physical[commandId]||$scope.db.commands.virtual[commandId]};$scope.getCommandByName=function(commandName){for(commandIndex in $scope.db.commands.physical){if($scope.db.commands.physical[commandIndex].n==commandName){return mergeObjects({id:commandIndex},$scope.db.commands.physical[commandIndex])}}for(commandIndex in $scope.db.commands.virtual){if($scope.db.commands.virtual[commandIndex].n==commandName){return mergeObjects({id:commandIndex},$scope.db.commands.virtual[commandIndex])}}return null};$scope.getAttributeById=function(attributeId){return $scope.db.attributes[attributeId]};$scope.getAttributeByName=function(attributeName){for(attributeIndex in $scope.db.attributes){if($scope.db.attributes[attributeIndex].n==attributeName){return $scope.db.attributes[attributeIndex]}}return null};$scope.getDeviceAttributeById=function(device,attributeId){if(!device){return null}for(i in device.a){if(device.a[i].n==attributeId){return device.a[i]}}return null};$scope.buildName=function(name,noQuotes,pedantic,itemPrefix,grouping){if((name==null)||(name==undefined)){return""}if(name instanceof Array){return $scope.buildNameList(name,grouping?grouping:"or","","",false,noQuotes,pedantic,itemPrefix)}if(pedantic||(name.length==34)){for(deviceId in $scope.instance.virtualDevices){var device=$scope.instance.virtualDevices[deviceId];if(device.o){for(id in device.o){noQuotes=noQuotes||!pedantic;if(name==id){return(!noQuotes?"'":"")+device.o[id]+(!noQuotes?"'":"")}}}}}return(!noQuotes?"'":"")+(itemPrefix?itemPrefix:"")+name+(!noQuotes?"'":"")};$scope.buildNameList=function(list,suffix,tag,className,possessive,noQuotes,pedantic,itemPrefix){var cnt=1;var result="";for(i in list){var an="";var it=list[i];if(it instanceof Object){tag=it.t?it.t:tag;an=it.a?it.a:"[unknown]";it=it.n}var item=$scope.buildName(it,noQuotes,pedantic,itemPrefix);result+=""+item+""+(possessive?"'"+(item.substr(-1)=="s"?"":"s"):"")+(cnt"+(cnt==list.length-1?(list.length>2?", ":" ")+suffix+" ":", ")+"
":"");cnt++}return result.trim()};$scope.buildLocationModeNameList=function(modes){var modeNames=[];if(modes instanceof Array){for(modeIndex in modes){modeNames.push($scope.getModeName(modes[modeIndex]))}if(modeNames.length){return $scope.buildNameList(modeNames,"or","lit","",false,true)}}return""};$scope.buildDeviceNameList=function(devices){var deviceNames=[];if(devices instanceof Array){for(deviceIndex in devices){var device=$scope.getDeviceById(devices[deviceIndex]);if(device){deviceNames.push({n:device.n,a:device.an,t:"dev"})}else{deviceNames.push({n:"{"+devices[deviceIndex]+"}",t:"var"})}}if(deviceNames.length){return $scope.buildNameList(deviceNames,"and","dev","",false,true)}}return"Location"};$scope.buildContactNameList=function(contacts){var contactNames=[];if(contacts instanceof Array){for(contactIndex in contacts){var contact=$scope.getContactById(contacts[contactIndex]);if(contact){contactNames.push({n:(contact.f+" "+contact.l).trim()+" ("+contact.t+"/"+(contact.p?"PUSH":"SMS")+")",a:contact.an,t:"cnt"})}else{contactNames.push({n:"{"+contacts[contactIndex]+"}",a:"Unknown Contact",t:"var"})}}if(contactNames.length){return $scope.buildNameList(contactNames,"and","cnt","",false,true)}}return"(empty)"};$scope.formatHour=function(hour){return(!location.timeZone||location.timeZone.id.startsWith("America"))?((hour%12?hour%12:"12")+(hour<12?"am":"pm")):("00"+hour).substr(-2)};$scope.renderDeviceNameList=function(devices){return $sce.trustAsHtml($scope.buildDeviceNameList(devices))};$scope.renderContactNameList=function(contacts){return $sce.trustAsHtml($scope.buildContactNameList(contacts))};$scope.hasCommand=function(device,commandName){if(!device||!device.c){return false}return $scope.hasName(device.c,commandName)};$scope.hasName=function(arrayOfObjects,name){if(!arrayOfObjects||!arrayOfObjects.length){return false}for(obj in arrayOfObjects){if(arrayOfObjects[obj]&&(arrayOfObjects[obj].n===name)){return true}}return false};$scope.hasId=function(arrayOfObjects,id){if(!arrayOfObjects||!arrayOfObjects.length){return false}for(obj in arrayOfObjects){if(arrayOfObjects[obj]&&(arrayOfObjects[obj].id===id)){return true}}return false};$scope.listAvailableCommands=function(devices){var commands={};var deviceCount=devices?devices.length:0;for(deviceIndex in devices){var deviceId=devices[deviceIndex]||"";var cmds=[];var all=false;if(deviceId.startsWith(":")){var device=$scope.getDeviceById(devices[deviceIndex]);if(device){cmds=device.c}}else{all=true;cmds=$scope.db.commands.physical}for(commandIndex in cmds){var commandName=all?commandIndex:cmds[commandIndex].n;if(commands[commandName]){commands[commandName]+=1}else{commands[commandName]=1}}}var result={common:[],partial:[],virtual:[]};for(commandName in commands){var command=$scope.db.commands.physical[commandName];if(!command){command={n:commandName+"(..)",cm:true}}if(commands[commandName]==deviceCount){result.common.push(mergeObjects({id:commandName},command))}else{result.partial.push(mergeObjects({id:commandName},command))}}for(commandName in $scope.db.commands.virtual){var command=$scope.db.commands.virtual[commandName];if(command.r){var count=0;for(deviceIndex in devices){var deviceId=devices[deviceIndex]||"";var ok=false;if(deviceId.startsWith(":")){var device=$scope.getDeviceById(devices[deviceIndex]);ok=!!device;if(ok){for(req in command.r){if(!$scope.hasCommand(device,command.r[req])){ok=false;break}}}}else{ok=true}if(ok){count++}}if(count>0){if(count==deviceCount){if(!$scope.hasId(result.common,commandName)){result.common.push(mergeObjects({id:commandName,em:true},command))}}else{if(!$scope.hasId(result.partial,commandName)){result.partial.push(mergeObjects({id:commandName,em:true},command))}}}}else{result.virtual.push(mergeObjects({id:commandName},command))}}result.common.sort($scope.sortByName);result.partial.sort($scope.sortByName);result.virtual.sort($scope.sortByName);return result};$scope.listAvailableDevices=function(){var result=[];for(deviceIndex in $scope.instance.devices){var device=$scope.instance.devices[deviceIndex];var tokens="";for(i in device.a){tokens+=":"+device.a[i].n+" "}result.push(mergeObjects({id:deviceIndex,tokens:tokens+device.n},device))}return result.sort($scope.sortByName)};$scope.listAvailableVirtualDevices=function(){var result=[];for(deviceIndex in $scope.instance.virtualDevices){var device=$scope.instance.virtualDevices[deviceIndex];result.push(mergeObjects({id:deviceIndex},device))}return result.sort($scope.sortByName)};$scope.escapeRegExp=function(str){return str};$scope.listAutoCompleteFunctions=function(){var result=[];for(functionIndex in $scope.db.functions){result.push(($scope.db.functions[functionIndex].d?$scope.db.functions[functionIndex].d:functionIndex)+"(")}return result.sort()};$scope.listAutoCompleteDevices=function(){var result=[];for(deviceIndex in $scope.instance.devices){var device=$scope.instance.devices[deviceIndex];result.push($scope.escapeRegExp("["+device.n+" :"))}return result.sort()};$scope.listAutoCompleteVariables=function(){var result=[];for(varIndex in $scope.piston.v){var v=$scope.piston.v[varIndex];result.push($scope.escapeRegExp(v.n))}if($scope.systemVars){for(varName in $scope.systemVars){result.push($scope.escapeRegExp(varName))}}if($scope.globalVars){for(varName in $scope.globalVars){result.push($scope.escapeRegExp(varName))}}return result.sort()};$scope.getVariableByName=function(name){if($scope.systemVars&&$scope.systemVars[name]){return $scope.systemVars[name]}if($scope.globalVars&&$scope.globalVars[name]){return $scope.globalVars[name]}for(varIndex in $scope.piston.v){if($scope.piston.v[varIndex].n==name){return $scope.piston.v[varIndex]}}return null};$scope.getVariableValue=function(name,dt){if($scope.localVars){var variable=$scope.localVars[name];if(variable!=undefined){if(dt=="datetime"){return utcToString(variable)}return""+variable}}return"(not set)"};$scope.autoAddVariable=function(name){if(!name){return false}name=name?name.trim():"";var v=$scope.getVariableByName(name);$scope.piston.v.push({t:"dynamic",n:name})};$scope.hasAttribute=function(device,attributeName){for(a in device.a){if(device.a[a].n==attributeName){return true}}return false};$scope.listAvailableAttributeNames=function(devices,restrictAttribute){var result=[];var list=$scope.listAvailableAttributes(devices,restrictAttribute);for(i in list){if(list[i].n!=statusAttribute){result.push({n:list[i].n,v:list[i].id})}}return result};$scope.listAvailableAttributes=function(devices,restrictAttribute){var result=[];var device=null;if(devices&&devices.length){var attributes={};var deviceCount=devices.length;var hasThreeAxis=false;for(deviceIndex in devices){device=$scope.getDeviceById(devices[deviceIndex]);if(device){for(attributeIndex in device.a){var attribute=device.a[attributeIndex];if(!restrictAttribute||(attribute.n==restrictAttribute)){if(attributes[attribute.n]){attributes[attribute.n]+=1}else{attributes[attribute.n]=1}}}}else{for(attributeName in $scope.db.attributes){if(!restrictAttribute||(attributeName==restrictAttribute)){if(attributes[attributeName]){attributes[attributeName]+=1}else{attributes[attributeName]=1}}}}}for(attributeId in attributes){if(attributes[attributeId]==deviceCount){var attribute=$scope.getAttributeById(attributeId);if(attribute){result.push(mergeObjects({id:attributeId},attribute));if(attributeId=="threeAxis"){hasThreeAxis=true}}else{for(a in device.a){if(device.a[a].n==attributeId){attribute=device.a[a];break}}if(attribute){var obj=mergeObjects({id:attributeId,c:true},attribute);obj.n="⌂ "+obj.n;obj.t=(obj.t||"string").toLowerCase().replace("number","decimal");result.push(obj)}}}}if(hasThreeAxis){result.push({id:"axisX",n:"X axis",t:"decimal"});result.push({id:"axisY",n:"Y axis",t:"decimal"});result.push({id:"axisZ",n:"Z axis",t:"decimal"});result.push({id:"orientation",n:"orientation",t:"string"})}result.push({id:statusAttribute,n:"⌂ "+statusAttribute,t:"string"});result.sort($scope.sortByName)}return result};$scope.sortByDisplay=function(a,b){return(a.d>b.d)?1:((b.d>a.d)?-1:0)};$scope.sortByName=function(a,b){a=a.n.toLowerCase();b=b.n.toLowerCase();return(a>b)?1:((b>a)?-1:0)};$scope.getStackData=function(){var data=angular.toJson($scope.compilePiston($scope.piston));return{hash:$scope.md5(data),timestamp:(new Date()).getTime(),data:angular.fromJson(data)}};$scope.autoSave=function(stack){var clearRedo=stack?false:true;stack=stack?stack:$scope.stack.undo;pushToStack=true;var obj=$scope.getStackData();if(stack&&stack.length){if(obj.hash==stack[stack.length-1].hash){pushToStack=false}}if(pushToStack){stack.push(obj);if(stack.length>MAX_STACK_SIZE){stack=stack.slice(-MAX_STACK_SIZE)}}if(clearRedo){$scope.stack.redo=[]}};$scope.objectToArray=function(object){var result=[];for(property in object){result.push({v:property,n:object[property]})}return result};$scope.saveStack=function(justSaved){$scope.stack.current=$scope.getStackData();if(justSaved){$scope.stack.current.timestamp=0}$scope.stack.build=$scope.meta.build;dataService.saveToStore("stack"+$scope.pistonId,$scope.stack)};$scope.loadStack=function(){$scope.stack=dataService.loadFromStore("stack"+$scope.pistonId);$scope.stack=$scope.stack instanceof Object?$scope.stack:{};$scope.stack.undo=($scope.stack.undo instanceof Array?$scope.stack.undo:[]);$scope.stack.redo=($scope.stack.redo instanceof Array?$scope.stack.redo:[]);if($scope.stack.current instanceof Object&&$scope.stack.current.data&&($scope.stack.build==$scope.meta.build)&&($scope.meta.modified<$scope.stack.current.timestamp)){$scope.setStatus();$scope.dialogChooseVersion()}else{}};$scope.dialogChooseVersion=function(){$scope.designer.dialog=ngDialog.open({template:"dialog-choose-version",className:"ngdialog-theme-default ngdialog-large",closeByDocument:false,disableAnimation:true,scope:$scope})};$scope.getLocalVariable=function(name){for(i in $scope.piston.v){if($scope.piston.v[i].n==name){return $scope.piston.v[i]}}return null};$scope.getLocalVariableType=function(name){for(i in $scope.piston.v){if($scope.piston.v[i].n==name){return $scope.piston.v[i].t}}return""};$scope.chooseVersion=function(keepLocal){if(keepLocal){$scope.piston=$scope.stack.current.data;$scope.validatePiston($scope.piston)}else{$scope.autoSave()}$scope.closeDialog()};$scope.undo=function(){if($scope.stack&&$scope.stack.undo&&$scope.stack.undo.length){$scope.autoSave($scope.stack.redo);$scope.stack.current=$scope.stack.undo.pop();$scope.piston=$scope.stack.current.data;$scope.validatePiston($scope.piston);$scope.saveStack()}};$scope.redo=function(){if($scope.stack&&$scope.stack.redo&&$scope.stack.redo.length){$scope.autoSave($scope.stack.undo);$scope.stack.current=$scope.stack.redo.pop();$scope.piston=$scope.stack.current.data;$scope.validatePiston($scope.piston);$scope.saveStack()}};$scope.localTimeToDate=function(time){time=time?time:0;var today=new Date();today.setHours(Math.floor(time/60));today.setMinutes(time%60);today.setSeconds(0);today.setMilliseconds(0);return today};$scope.validateOperand=function(operand,reinit,managed){if(!!$scope.designer.comparison&&!managed){$scope.validateComparison($scope.designer.comparison,reinit);return}operand=operand||{};if(!operand.initialized||reinit){operand.data=operand.data||{};operand.data.a=operand.data.a||"";operand.data.c=(operand.data.c==undefined)||(operand.data.c==null)?"":operand.data.c;operand.data.v=operand.data.v||"";operand.data.e=operand.data.e||"";operand.data.x=operand.data.x||"";operand.data.d=operand.data.d||[];operand.data.g=operand.data.g||(operand.multiple?"any":"avg");operand.data.f=operand.data.f||"l";operand.options=operand.options||[]}if(true||!operand.initialized||reinit){var dataType=(operand.dataType||"string").toLowerCase();if(dataType=="variables"){operand.multiple=true;dataType="variable"}if(dataType=="devices"){operand.multiple=true;dataType="device"}if(dataType=="pistons"){operand.multiple=true;dataType="piston"}if(dataType=="routines"){operand.multiple=true;dataType="routine"}if(dataType=="attributes"){operand.multiple=true;dataType="attribute"}if(dataType=="modes"){operand.multiple=true;dataType="mode"}if(dataType=="alarmsystemstatus"){dataType="alarmSystemStatus"}if(dataType=="alarmsystemstatuses"){operand.multiple=true;dataType="alarmSystemStatus"}if(dataType=="modes"){operand.multiple=true;dataType="mode"}if(dataType=="enums"){operand.multiple=true;dataType="enum"}if(dataType=="lifxScenes"){operand.multiple=true;dataType="lifxScene"}if(dataType=="lifxscene"){dataType="lifxScene"}if(dataType=="lifxselector"){dataType="lifxSelector"}if(dataType=="contacts"){operand.multiple=true;dataType="contact"}if(dataType=="number"){dataType="decimal"}if(dataType=="bool"){dataType="boolean"}if((dataType=="enum")&&!operand.options&&!operand.options.length){dataType="string"}switch(operand.data.vt){case"time":if(!(operand.data.c instanceof Date)){if(operand.data.c!=undefined){operand.data.c=$scope.localTimeToDate(operand.data.c)}}break;case"date":case"datetime":if(!(operand.data.c instanceof Date)){operand.data.c=new Date(operand.data.c);if(operand.data.c=="Invalid Date"){operand.data.c=new Date()}}break}var disableExpressions=(!!operand.disableExpressions)||(dataType=="piston")||(dataType=="routine")||(dataType=="askAlexaMacro")||(dataType=="attribute");operand.onlyAllowConstants=operand.onlyAllowConstants||disableExpressions;var strict=!!operand.strict;if(operand.onlyAllowConstants||(dataType=="contact")){operand.allowArgument=false;operand.allowDevices=(dataType=="device");operand.allowPhysical=false;operand.allowVirtual=false;operand.allowConstant=(dataType!="device");operand.allowVariable=false;operand.allowExpression=!disableExpressions}else{operand.allowDevices=dataType=="device";operand.allowPhysical=(dataType!="datetime")&&(dataType!="date")&&(dataType!="time")&&(dataType!="device")&&(dataType!="variable")&&(!strict||(dataType!="boolean"))&&(dataType!="duration");operand.allowPreset=(!operand.event)&&(dataType=="datetime")||(dataType=="time")||(dataType=="color");operand.allowVirtual=(dataType!="datetime")&&(dataType!="date")&&(dataType!="time")&&(dataType!="device")&&(dataType!="variable")&&(dataType!="decimal")&&(dataType!="integer")&&(dataType!="number")&&(dataType!="boolean")&&(dataType!="enum")&&(dataType!="color")&&(dataType!="duration");operand.allowVariable=(dataType!="device"||((dataType=="device")&&operand.multiple))&&(!strict||(dataType!="boolean"));operand.allowConstant=(!operand.event)&&(dataType!="device")&&(dataType!="variable");operand.allowArgument=(!operand.event)&&(dataType!="device")&&(dataType!="variable");operand.allowExpression=(!operand.event)&&(dataType!="variable")&&(!strict||(dataType!="boolean"))}if(operand.data.t==null){var t="";if(!operand.optional){t=dataType=="variable"?"x":(!!operand.allowPreset?"s":"c");if(($scope.designer.$condition)||($scope.designer.$restriction)){t="p"}}operand.data.t=t}if(((operand.data.t=="p")&&(!operand.allowPhysical))||((operand.data.t=="v")&&(!operand.allowVirtual))){operand.data.t=(!!operand.allowPreset)?"s":"c"}if(!operand.config){operand.config=$scope.copy($scope.getExpressionConfig());operand.config.autocomplete[5].words=[/([0-9]+)(\.[0-9]+)?/g]}operand.restrictAttribute=null;operand.restrictType=null;switch(dataType){case"color":operand.restrictAttribute="color";break;case"device":operand.restrictType="device";break;case"integer":operand.restrictType="integer,integer[]";break;case"decimal":operand.restrictType="integer,integer[],decimal,decimal[]";break;case"time":operand.restrictType="datetime,datetime[],time,time[]";break;case"date":case"datetime":operand.restrictType="datetime,datetime[],date,date[]";break}operand.dataType=dataType;operand.durationUnit=operand.durationUnit||operand.data.vt||"s";operand.data.vt=dataType=="duration"?operand.durationUnit:dataType}switch(dataType){case"enum":break;case"bool":case"boolean":operand.options=["false","true"];break;case"mode":case"powerSource":case"alarmSystemStatus":case"routine":operand.options=$scope.objectToArray($scope.instance.virtualDevices[dataType].o);break;case"attribute":operand.attrs=operand.attrs?operand.attrs:$scope.listAvailableAttributeNames($scope.designer.parent.d);operand.options=operand.attrs;break;case"piston":operand.options=$scope.listAllPistons();break;case"contact":operand.options=$scope.contacts;break;case"lifxScene":operand.options=$scope.objectToArray($scope.instance.lifx.scenes).sort($scope.sortByName);break;case"lifxSelector":operand.options=[];break;case"integer":case"decimal":case"duration":if((operand.data.t=="c")&&(isNaN(operand.data.c)||(operand.data.c==""))){operand.data.c=0}default:operand.options=null}if((!operand.multiple)&&(operand.options)&&(operand.options.length)&&(operand.data.t=="c")){if(operand.options[0] instanceof Object){var found=false;for(i in operand.options){if(operand.options[i].v==operand.data.c){found=true;break}}if(!found){operand.data.c=operand.options[0].v}}else{if(operand.options.indexOf(operand.data.c)<0){operand.data.c=operand.options[0]}}}operand.initialized=true;operand.allowAggregation=true;operand.allowAll=true;operand.attributes=(operand.data.t=="p")?$scope.listAvailableAttributes(operand.data.d,operand.restrictAttribute):[];operand.valid=false;operand.selectedMultiple=(operand.data.t=="p")&&operand.data.d&&(operand.data.d.length>1)&&((operand.data.g=="all")||(operand.data.g=="any"));operand.error=null;operand.momentary=false;operand.selectedDataType="string";operand.selectedOptions=[];switch(operand.data.t){case"p":var attribute=$scope.db.attributes[operand.data.a];if(!attribute){if(operand.data.d.length){var device=$scope.getDeviceById(operand.data.d[0]);if(device){for(a in device.a){if(device.a[a].n==operand.data.a){attribute=device.a[a];break}}}}}operand.count=0;if(attribute){operand.momentary=attribute.m||(!!attribute.s&&(operand.data.i instanceof Array)&&operand.data.i.length);operand.interactive=!!attribute.p;if(operand.interactive&&!operand.data.p){operand.data.p="a"}if(!!attribute.s){operand.subDeviceName=attribute.sd;var countAttributes=attribute.s.split(",");for(deviceIndex in operand.data.d){var dev=$scope.getDeviceById(operand.data.d[deviceIndex]);var c=0;if(dev){for(i in countAttributes){var attr=$scope.getDeviceAttributeById(dev,countAttributes[i]);if((attr)&&(!isNaN(attr.v))){c=parseInt(attr.v);if(c>operand.count){operand.count=c}}}}}if(operand.count==0){operand.count=32}}else{operand.subDeviceName=""}if(operand.count){if((operand.data.i==null)||(operand.data.i==undefined)){operand.data.i=[]}$scope.refreshSelects()}operand.selectedDataType=attribute.t.toLowerCase();operand.selectedOptions=attribute.o;if(operand.momentary){operand.allowAll=false;operand.allowAggregation=false}}else{operand.selectedDataType="string"}if(operand.data.d&&(operand.data.d.length>1)){if(!operand.data.g){operand.error="Invalid aggregation method"}if(!(["any","all","least","most"].indexOf(operand.data.g)>=0)){operand.selectedDataType="decimal"}}break;case"v":var virtualDevice=$scope.instance.virtualDevices[operand.data.v];operand.selectedDataType=(!!virtualDevice&&!!virtualDevice.t)?virtualDevice.t:"string";if(virtualDevice){operand.momentary=virtualDevice.m;for(o in virtualDevice.o){operand.selectedOptions.push({v:o,n:virtualDevice.o[o]})}}break;case"x":if(operand.data.x instanceof Array){if(operand.data.x.length){operand.selectedDataType="dynamic"}else{operand.error="Invalid list of variables"}}else{var variable=$scope.getVariableByName(operand.data.x);if(variable){operand.selectedDataType=variable.t;if(operand.selectedDataType=="boolean"){operand.selectedOptions=["false","true"]}}else{operand.error="Invalid variable"}}break;case"u":operand.selectedDataType="dynamic";break;case"c":var expression=$scope.parseString(operand.data.c,operand.data.vt);operand.error=expression.err;operand.expressionVar=expression.errVar;operand.data.exp=expression;if(!operand.options){if(!operand.optional&&!operand.data.c&&(operand.requirePositiveNumber)){operand.error="Empty value";operand.expressionVar=""}}else{if((operand.data.c==null)||(operand.data.c==undefined)){operand.error="Invalid selection";operand.expressionVar=""}}operand.selectedDataType=operand.dataType;break;case"e":var expression=$scope.parseExpression(operand.data.e,false,operand.data.vt);operand.error=expression.err;operand.expressionVar=expression.errVar;if(expression.err){var loc=(expression.loc?expression.loc:"0:"+(expression.str.length-1).toString()).split(":");var start=parseInt(loc[0]);var end=loc.length==2?parseInt(loc[1]):start;operand.config.autocomplete[0]={words:[new RegExp(".(?=.{"+(expression.str.length-start-1)+"}$).{"+(end-start)+"}")],cssClass:"hl err",title:expression.err}}else{operand.config.autocomplete[0]={words:[],cssClass:"hl err"}}operand.data.exp=expression;$scope.delayEvaluation(operand);operand.selectedDataType="dynamic";break}if((!operand.error)&&(operand.dataType=="duration")&&(!operand.durationUnit)){operand.error="Invalid duration unit"}operand.valid=(!operand.error)&&(((operand.data.t=="")&&(operand.optional))||((operand.data.t=="d")&&!!operand.data.d&&!!operand.data.d.length)||((operand.data.t=="p")&&!!operand.data.d&&!!operand.data.d.length&&!!operand.data.a)||((operand.data.t=="v")&&!!operand.data.v)||((operand.data.t=="x")&&!!operand.data.x&&!!operand.data.x.length)||((operand.data.t=="s")&&!!operand.data.s)||((operand.data.t=="u")&&!!operand.data.u)||((operand.data.t=="c")&&!((operand.data.c=="Invalid Date")&&(operand.data.c instanceof Object))&&!((dataType=="duration")&&(isNaN(operand.data.c)||(operand.requirePositiveNumber&&(operand.data.c<1)))))||((operand.data.t=="e")&&!!operand.data.e&&!!operand.data.e.length));switch(operand.dataType){case"integer":operand.inputType="number";try{operand.data.c=parseInt(operand.data.c)}catch(all){operand.data.c=0}break;case"duration":case"decimal":operand.inputType="number";try{operand.data.c=parseFloat(operand.data.c)}catch(all){operand.data.c=0}break;default:operand.inputType=operand.dataType}if(operand.linkedOperand){operand.linkedOperand.dataType=operand.selectedDataType;operand.linkedOperand.options=operand.selectedOptions;$scope.validateOperand(operand.linkedOperand,true);$scope.refreshSelects()}};$scope.refreshSelects=function(type){if(type){$scope.$$postDigest(function(){$("select["+type+"]").selectpicker("refresh");$timeout(function(){$("select["+type+"]").selectpicker("refresh")},0,false)})}else{$scope.$$postDigest(function(){$("select[selectpicker]").selectpicker("refresh");$timeout(function(){$("select[selectpicker]").selectpicker("refresh")},0,false)})}};$scope.getOrdinalSuffix=function(value){if(isNaN(value)){return""}value=parseInt(value);var value100=value%100;var value10=value%10;if(((value100>3)&&(value100<21))||(value10==0)||(value10>3)){return"th"}switch(value10){case 1:return"st";case 2:return"nd";case 3:return"rd"}return"th"};$scope.getOrdinal=function(value){if(isNaN(value)){return""}value=parseInt(value);switch(value){case -3:return"third-last";case -2:return"second-last";case -1:return"last"}return value+$scope.getOrdinalSuffix(value)};$scope.listODM=function(){var result=$scope.designer.odm;var sz=(!$scope.designer.operand.data.odw||($scope.designer.operand.data.odw=="d"))?31:5;if(!result||(result.length!=(sz+3))){result=[];for(i=1;i<=sz;i++){result.push({v:i,n:i+$scope.getOrdinalSuffix(i)})}result.push({v:-1,n:"last"});result.push({v:-2,n:"second-last"});result.push({v:-3,n:"third-last"});$scope.designer.odm=result}return result};$scope.listODW=function(){var result=$scope.designer.odw;var sz=($scope.designer.operand.data.odm>5)?0:7;if(!result||(result.length!=(sz+1))){result=[];result.push({v:"d",n:"day"});if(sz){for(i in $scope.weekDays){result.push({v:i.toString(),n:$scope.weekDays[i]})}}$scope.designer.odw=result}return result};$scope.validateComparison=function(comparison,reinit){$scope.validateOperand(comparison.left,reinit,true);if((comparison.left.selectedDataType!=comparison.dataType)||(comparison.left.selectedMultiple!=comparison.selectedMultiple)||(comparison.left.momentary!=comparison.momentary)||(comparison.left.data.t=="v")||(comparison.selectedInteractive!=comparison.left.data.p)){comparison.dataType=comparison.left.selectedDataType;comparison.selectedMultiple=comparison.left.selectedMultiple;comparison.selectedInteractive=comparison.left.data.p;comparison.momentary=comparison.left.momentary;var disableTimedConditions=(comparison.left.data.t!="p")||((comparison.left.data.g!="any")&&(comparison.left.data.g!="all"));var disableConditions=(comparison.left.interactive&&((comparison.left.data.p=="p")||(comparison.left.data.p=="s")));var disableTimedTriggers=disableConditions;var disableTriggers=(comparison.type=="restriction");var optionList=[];var options=[];if(!comparison.dataType){comparison.dataType="dynamic"}switch(comparison.dataType){case"color":case"hexcolor":case"object":case"vector3":case"enum":dt="s";break;case"image":dt="f";break;case"dynamic":dt="";break;case"time":case"date":case"datetime":dt="t";break;default:dt=comparison.dataType.substr(0,1)}dt=(comparison.momentary&&(dt!="e")?(comparison.left.data.t=="v"?"v":"m"):((dt=="n"?"d":dt)));if(!disableConditions){for(conditionId in $scope.db.comparisons.conditions){var condition=$scope.db.comparisons.conditions[conditionId];if(((!dt&&(condition.g!="m"))||(condition.g.indexOf(dt)>=0))&&(!disableTimedConditions||!condition.t)){options.push({id:conditionId,d:(comparison.selectedMultiple?(condition.dd?condition.dd:condition.d):condition.d),c:"Conditions"})}}optionList=optionList.concat(options.sort($scope.sortByDisplay))}if(!disableTriggers){options=[];for(triggerId in $scope.db.comparisons.triggers){var trigger=$scope.db.comparisons.triggers[triggerId];if((trigger.g.indexOf(dt)>=0)&&(!disableTimedTriggers||!trigger.t)){options.push({id:triggerId,d:(comparison.selectedMultiple?(trigger.dd?trigger.dd:trigger.d):trigger.d),c:"Triggers"})}}optionList=optionList.concat(options.sort($scope.sortByDisplay))}comparison.options=optionList;if(comparison.options.length==1){comparison.operator=comparison.options[0].id}}var comp=$scope.db.comparisons.conditions[comparison.operator]||$scope.db.comparisons.triggers[comparison.operator];comparison.operatorValid=!!comp;comparison.parameterCount=comp&&comp.p?comp.p:0;comparison.multiple=comp&&comp.m?true:false;comparison.valid=comparison.left.valid&&comparison.operatorValid;comparison.timed=comp?comp.t:0;if((comparison.parameterCount>0)||(comparison.dataType=="email")){comparison.right.multiple=comparison.multiple;comparison.right.disableAggregation=comparison.multiple;comparison.right.dataType=(comparison.dataType=="email"?"string":comparison.left.selectedDataType);if(angular.toJson(comparison.right.options)!=angular.toJson(comparison.left.selectedOptions)){if((comparison.right.data.t=="c")&&comparison.right.options&&comparison.right.options.length&&(!comparison.left.selectedOptions||!comparison.left.selectedOptions.left)){comparison.right.data.c=""}comparison.right.options=comparison.left.selectedOptions}$scope.validateOperand(comparison.right,reinit,true);comparison.valid=comparison.valid&&comparison.right.valid}if((comparison.parameterCount>1)||(comparison.dataType=="email")){comparison.right2.multiple=comparison.multiple;comparison.right2.disableAggregation=comparison.multiple;comparison.right2.dataType=(comparison.dataType=="email"?"string":comparison.left.selectedDataType);if(angular.toJson(comparison.right2.options)!=angular.toJson(comparison.left.selectedOptions)){comparison.right2.options=comparison.left.selectedOptions}$scope.validateOperand(comparison.right2,reinit,true);comparison.valid=comparison.valid&&comparison.right2.valid}var usingTime=(comparison.timed>0);var usingTime2=false;if(comparison.left.selectedDataType=="time"){usingTime=usingTime||(comparison.right.data.t!="c");usingTime2=(comparison.right2.data.t!="c")}if(usingTime){comparison.time.requirePositiveNumber=!!comparison.timed;$scope.validateOperand(comparison.time,reinit,true);comparison.valid=comparison.valid&&comparison.time.valid}if(usingTime2){comparison.time2.requirePositiveNumber=false;comparison.time2.dataType="duration";$scope.validateOperand(comparison.time2,reinit,true);comparison.valid=comparison.valid&&comparison.time2.valid}if(comparison.followedBy){comparison.within.requirePositiveNumber=false;comparison.within.dataType="duration";$scope.validateOperand(comparison.within,reinit,true);comparison.valid=comparison.valid&&comparison.within.valid}};$scope.detectDataType=function(value){switch(typeof value){case"string":if(!isNaN(parseFloat(value))){return"number"}return"string";case"number":return"number";default:return"string"}};$scope.renderOperand=function(operand,noQuotes,pedantic,noNegatives,grouping){var result="";if(operand){switch(operand.t){case"d":if(operand.d){result=$scope.buildDeviceNameList(operand.d)}break;case"p":if(operand.d&&operand.a){result=$scope.renderDeviceList(operand.d,operand.a,operand.g,true)+" "+operand.a+""}break;case"v":var device=$scope.getVirtualDeviceById(operand.v);result=""+(device?device.n:"(invalid virtual device)")+"";break;case"s":if(operand.s){result=""+operand.s+""}break;case"x":if(operand.x){result="{"+operand.x+($scope.getLocalVariableType(operand.x).endsWith("]")?"["+operand.xi+"]":"")+"}"}break;case"c":var m="num";noQuotes=noQuotes||!isNaN(operand.c);switch(operand.vt){case"time":var date=$scope.localTimeToDate(operand.c);result=""+date.toLocaleTimeString({hour:"2-digit",minute:"2-digit"})+"";break;case"date":result=""+utcToDateString(operand.c)+"";break;case"datetime":result=""+utcToString(operand.c)+"";break;case"email":result=""+operand.c+"";break;case"piston":result=""+$scope.getPistonName(operand.c)+"";break;case"lifxScene":result=""+$scope.getLifxSceneName(operand.c)+"";break;case"lifxSelector":result=""+$scope.getLifxSelectorName(operand.c)+"";break;case"phone":result=""+operand.c+"";break;case"uri":result=""+operand.c+"";break;case"contact":result=$scope.renderContactNameList(operand.c);break;default:if(!noQuotes){if((operand.vt=="boolean")||(operand.vt=="enum")){noQuotes=true}m="lit"}var c=operand.c;if(noNegatives&&!isNaN(c)&&parseInt(c)<0){c=-parseInt(c)}result=""+scope.buildName(c,noQuotes,pedantic,null,grouping)+""}break;case"u":result=result+"{$args."+operand.u+"}";break;case"e":if(operand.e){result="{"+operand.e+"}"}break}}result=result?result:'(empty)';return(result instanceof Object)?result:$sce.trustAsHtml(result)};$scope.renderForOperands=function(statement){var result;result=""+(statement.x?statement.x:"$index")+" = "+$scope.renderOperand(statement.lo)+" to "+$scope.renderOperand(statement.lo2)+" step "+$scope.renderOperand(statement.lo3);return $sce.trustAsHtml(result?result:"(invalid operands)")};$scope.renderForEachOperands=function(statement){var result;result=""+(statement.x?statement.x:"$device")+" in "+$scope.renderOperand(statement.lo);return $sce.trustAsHtml(result?result:"(invalid operands)")};$scope.renderTimeOperand=function(to){if(!to){return""}var isConstant=(to.t=="c");var constantValue=isConstant&&!isNaN(to.c)?parseInt(to.c):0;if(isConstant&&(constantValue==0)){return""}return $scope.renderOperand(to,false,false,true)+" "+$scope.getDurationUnitName(to.vt,(constantValue!=1))+" "+(constantValue<0?"to":"past")+" "};$scope.renderComparison=function(l,o,r,r2,to,to2){var comparison=$scope.db.comparisons.triggers[o];var trigger=!!comparison;if(!comparison){comparison=$scope.db.comparisons.conditions[o]}if(!comparison){return"[ERROR: Invalid comparison]"}var pedantic=l.t=="v";var plural=l&&(l.t=="p")&&l.d&&(l.d.length>1)&&(l.g=="all");var noQuotes=false;var unit="";var a=null;switch(l.t){case"v":switch(l.v){case"locationMode":case"shmState":noQuotes=true;break}break;case"p":a=$scope.getAttributeById(l.a);if(!!a&&!!a.u){unit=a.u}if(unit=="°?"){unit="°"+($scope.location.temperatureScale?$scope.location.temperatureScale:"")}break}var indexes="";if(!!a&&!!a.s&&(l.i instanceof Array)&&l.i.length){indexes=" "+$scope.buildNameList(l.i,"or",null,null,false,true,false,"#")+""}if(!!a&&!!a.p){switch(l.p){case"p":indexes+=" physically";break;case"s":indexes+=" programmatically";break}}var offset1="";var offset2="";if((l.t=="v")&&(l.v=="time")){if(r&&to&&(r.t!="c")){offset1=$scope.renderTimeOperand(to)}if(r2&&to2&&(r2.t!="c")){offset2=$scope.renderTimeOperand(to2)}}var result=$scope.renderOperand(l)+indexes+" "+(plural?(comparison.dd?comparison.dd:comparison.d):comparison.d)+""+(comparison.p>0?" "+offset1+$scope.renderOperand(r,noQuotes,pedantic)+(unit?""+unit+" ":""):"")+(comparison.p>1?" "+(comparison.d.indexOf("between")?"and":"through")+" "+offset2+$scope.renderOperand(r2,noQuotes,pedantic)+(unit?""+unit+" ":""):"");switch(comparison.t){case 1:result+=" "+(trigger?"for":"in the last")+" "+$scope.renderOperand(to)+" "+$scope.getDurationUnitName(to.vt,!((to.t=="c")&&(!isNaN(to.c))&&(parseInt(to.c)==1)))+"";break;case 2:result+=" for "+(to.f=="g"?"at least":"less than")+" "+$scope.renderOperand(to)+" "+$scope.getDurationUnitName(to.vt,!((to.t=="c")&&(!isNaN(to.c))&&(parseInt(to.c)==1)))+"";break}if((l.t=="v")&&(["time","date","datetime"].indexOf(l.v)>=0)){var odw=(l.odw instanceof Array)&&l.odw.length?l.odw:null;var odm=(l.odm instanceof Array)&&l.odm.length?l.odm:null;var owm=!odm&&(l.owm instanceof Array)&&l.owm.length?l.owm:null;var omy=(l.omy instanceof Array)&&l.omy.length?l.omy:null;if(!!odw||!!odm||!!owm||!!omy){var rCount=0;result+=", but only";var odwString="";if(odw){for(i in odw){if((i>0)&&(odw.length>2)){odwString+=", "}if((i>0)&&(i==odw.length-1)){odwString+=" or "}odwString+=""+$scope.weekDays[odw[i]]+"s"}rCount++}if(owm){result+=(rCount?",":"")+" on the ";for(i in owm){if((i>0)&&(owm.length>2)){result+=", "}if((i>0)&&(i==owm.length-1)){result+=" or "}result+=""+$scope.getOrdinal(owm[i])+""}result+=" "+(odwString?odwString:"week"+(owm.length>1?"s":"")+"")+(omy?"":" of the month");rCount++}else{if(odwString){result+=(rCount>1?",":"")+" on "+odwString}}if(odm){result+=(rCount?",":"")+" on the ";for(i in odm){if((i>0)&&(odm.length>2)){result+=", "}if((i>0)&&(i==odm.length-1)){result+=" or "}result+=""+$scope.getOrdinal(odm[i])+""}result+=" day"+(odm.length>1?"s":"")+(omy?"":" of the month");rCount++}if(omy){result+=" "+(owm||odm?"of":"in")+" ";for(i in omy){if((i>0)&&(omy.length>2)){result+=", "}if((i>0)&&(i==omy.length-1)){result+=" or "}result+=""+$scope.yearMonths[omy[i]-1]+""}rCount++}}}return $sce.trustAsHtml(result)};$scope.renderGroupingMethod=function(collection,item){var result=collection.o;if((collection.c instanceof Array)&&(result=="followed by")){var idx=collection.c.indexOf(item)+1;if(idx"+$scope.getDurationUnitName(it.wd.vt,!((it.wd.t=="c")&&(!isNaN(it.wd.c))&&(parseInt(it.wd.c)==1)))+" by"}}return $sce.trustAsHtml(result)};$scope.renderGroupWithin=function(collection,group){var list=collection instanceof Array?collection:collection.c;var result="";if((list instanceof Array)&&(!!list.length)&&(list[0]!=group)){result=" within "+$scope.renderOperand(group.wd)+" "+$scope.getDurationUnitName(group.wd.vt,!((group.wd.t=="c")&&(!isNaN(group.wd.c))&&(parseInt(group.wd.c)==1)))+""+(group.wt=="s"?" (strict)":"")}return $sce.trustAsHtml(result)};$scope.getWeekDayName=function(day){if(isNaN(day)){return"day"}return $scope.weekDays[parseInt(day)]};$scope.getMonthDayName=function(day){switch(day){case -1:return"last";case -2:return"second-last";case -3:return"third-last"}return day+$scope.getOrdinalSuffix(day)};$scope.getMonthName=function(month){return $scope.yearMonths[month]};$scope.getDurationUnitName=function(unit,plural){var suffix=plural?"s":"";switch(unit){case"ms":return"millisecond"+suffix;case"s":return"second"+suffix;case"m":return"minute"+suffix;case"h":return"hour"+suffix;case"d":return"day"+suffix;case"w":return"week"+suffix;case"n":return"month"+suffix;case"y":return"year"+suffix}return""};$scope.renderTimer=function(timer){var result="";var interval=timer.lo;var unit=$scope.getDurationUnitName(interval.vt);var unit2=unit;var level=0;switch(interval.vt){case"ms":level=1;break;case"s":level=2;break;case"m":level=3;break;case"h":level=4;break;case"d":level=5;break;case"w":level=6;unit=$scope.getWeekDayName(interval.odw);break;case"n":level=7;unit=$scope.getMonthDayName(interval.odm)+" "+$scope.getWeekDayName(interval.odw)+" of the month";break;case"y":level=8;unit=$scope.getMonthDayName(interval.odm)+" "+$scope.getWeekDayName(interval.odw)+" of "+$scope.getMonthName(interval.omy);break}switch(interval.t){case"c":if(!isNaN(interval.c)){var c=parseInt(interval.c);switch(c){case 1:result=unit;break;case 2:result="other "+unit;break;default:result=""+c+" "+unit2+"s";switch(interval.vt){case"n":result+=", on the "+$scope.getMonthDayName(interval.odm)+" "+$scope.getWeekDayName(interval.odw)+" of the month";break;case"y":result+=", on the "+$scope.getMonthDayName(interval.odm)+" "+$scope.getWeekDayName(interval.odw)+" of "+$scope.getMonthName(interval.omy);break}}break}default:result=$scope.renderOperand(interval)+" "+unit+"s"}if(level==4){var m=("00"+timer.lo.om).substr(-2);result+=", at :"+m+" past the hour"}if(level>=5){result+=", at ";if(timer.lo2.t!="c"){switch(timer.lo3.t){case"c":var offset=isNaN(timer.lo3.c)?0:parseInt(timer.lo3.c);if(offset==0){result+=$scope.renderOperand(timer.lo2)}else{if(offset<0){result+=""+(-offset).toString()+" "+$scope.getDurationUnitName(timer.lo3.vt,(offset<-1))+" before "+$scope.renderOperand(timer.lo2)}else{result+=""+offset.toString()+" "+$scope.getDurationUnitName(timer.lo3.vt,(offset>1))+" after "+$scope.renderOperand(timer.lo2)}}break;default:result+=$scope.renderOperand(timer.lo2)+" ± "+$scope.renderOperand(timer.lo3)}}else{result+=$scope.renderOperand(timer.lo2)}}var om=(level<=2)&&(interval.om instanceof Array)&&interval.om.length?interval.om:null;var oh=(level<=3)&&(interval.oh instanceof Array)&&interval.oh.length?interval.oh:null;var odw=(level<=5)&&(interval.odw instanceof Array)&&interval.odw.length?interval.odw:null;var odm=(level<=6)&&(interval.odm instanceof Array)&&interval.odm.length?interval.odm:null;var owm=(level<=6)&&!odm&&(interval.owm instanceof Array)&&interval.owm.length?interval.owm:null;var omy=(level<=7)&&(interval.omy instanceof Array)&&interval.omy.length?interval.omy:null;if(!!om||!!oh||!!odw||!!odm||!!owm||!!omy){var rCount=0;result+=", but only";if(om){result+=" at ";for(i in om){if((i>0)&&(om.length>2)){result+=", "}if((i>0)&&(i==om.length-1)){result+=" or "}result+=":"+("00"+om[i]).substr(-2)+""}result+=" minutes past the hour";rCount++}if(oh){result+=(rCount?",":"")+" during the ";for(i in oh){if((i>0)&&(oh.length>2)){result+=", "}if((i>0)&&(i==oh.length-1)){result+=" or "}result+=""+$scope.formatHour(oh[i])+""}result+=" hour"+(oh.length>1?"s":"")+"";rCount++}var odwString="";if(odw){for(i in odw){if((i>0)&&(odw.length>2)){odwString+=", "}if((i>0)&&(i==odw.length-1)){odwString+=" or "}odwString+=""+$scope.weekDays[odw[i]]+"s"}rCount++}if(owm){result+=(rCount?",":"")+" on the ";for(i in owm){if((i>0)&&(owm.length>2)){result+=", "}if((i>0)&&(i==owm.length-1)){result+=" or "}result+=""+$scope.getOrdinal(owm[i])+""}result+=" "+(odwString?odwString:"week"+(owm.length>1?"s":"")+"")+(omy?"":" of the month");rCount++}else{if(odwString){result+=(rCount>1?",":"")+" on "+odwString}}if(odm){result+=(rCount?",":"")+" on the ";for(i in odm){if((i>0)&&(odm.length>2)){result+=", "}if((i>0)&&(i==odm.length-1)){result+=" or "}result+=""+$scope.getOrdinal(odm[i])+""}result+=" day"+(odm.length>1?"s":"")+(omy?"":" of the month");rCount++}if(omy){result+=" "+(owm||odm?"of":"in")+" ";for(i in omy){if((i>0)&&(omy.length>2)){result+=", "}if((i>0)&&(i==omy.length-1)){result+=" or "}result+=""+$scope.yearMonths[omy[i]-1]+""}rCount++}}return $sce.trustAsHtml(result)};$scope.renderString=function(value){return renderString($sce,value)};$scope.renderTask=function(task){var command=$scope.getCommandById(task.c);var display;if(!command){display=task.c+"(";for(i in task.p){display+=(parseInt(i)?", ":"")+$scope.renderOperand(task.p[i],null,null,null,"and")}display+=")"}else{var displayFormat=command.d;if(task.c==="httpRequest"){var method=task.p[1].c;var useQueryString=method==="GET"||method==="DELETE"||method==="HEAD";var requestBodyType=task.p[2].c;if(useQueryString){displayFormat+="[? with query {3}]"}else{if(requestBodyType==="CUSTOM"){displayFormat+="[? with {4}][? as type {5}]"}else{displayFormat+="[? with {2}][? encoded {3}]"}}}display=!displayFormat?command.n:displayFormat.replace(/(?:\[\?(.*?))?\{(\d)\}(?:\s*\])?/g,function(match,prefix,text){var idx=parseInt(text);if((idx<0)||(!task.p)||(idx>=task.p.length)){return" (?) "}var value="";if(command.p[idx].t=="duration"){var unit=$scope.getDurationUnitName(task.p[idx].vt,true);value=$scope.renderOperand(task.p[idx],true)+" "+unit}else{if((task.p[idx].t=="c")&&(!!command.p[idx].d)&&(task.p[idx].c=="false")){value=""}else{value=$scope.renderOperand(task.p[idx],true,null,null,"and")}}if(!value){value=""}if(!!value&&!!command.p[idx].d){value=(!!task.p[idx]&&!!task.p[idx].t)?command.p[idx].d.replace("{v}",value):""}return(value?(prefix||""):"")+value}).replace(/(\{T\})/g,"°"+$scope.location.temperatureScale);var icon=command.i;if(icon){display=' '+display}}if(task.m){display+=" (only while "+$scope.buildLocationModeNameList(task.m)+")"}display+=";";return $sce.trustAsHtml(display)};$scope.renderDeviceList=function(devices,attribute,aggregation,trailing){var result="";var deviceNames=[];suffix=(aggregation=="any"?"or":"and");var prefix="";if(devices instanceof Array){if(devices.length>1){switch(aggregation){case"any":prefix="Any of ";break;case"all":prefix="All of ";break;case"count":prefix="Count of ";break;case"avg":prefix="Average of ";break;case"median":prefix="Median of ";break;case"least":prefix="Least occurring value of ";break;case"most":prefix="Most occurring value of ";break;case"stdev":prefix="Standard deviation of ";break;case"min":prefix="Minimum of ";break;case"max":prefix="Maximum of ";break;case"variance":prefix="Variance of ";break}if(!trailing){prefix=prefix.toLowerCase()}}for(deviceIndex in devices){var device=$scope.getDeviceById(devices[deviceIndex]);if(device){deviceNames.push({n:device.n,a:device.an,t:"dev"})}else{deviceNames.push({n:"{"+devices[deviceIndex]+"}",t:"var"})}}if(deviceNames.length){result=prefix+$scope.buildNameList(deviceNames,suffix,"dev","",!!attribute,true)}}return $sce.trustAsHtml(result)};$scope.validatePiston=function(piston){var idx=0;var level=0;var warnings={};var addWarning=function(object,warning){if(!object){return}object.w=object.w?object.w:[];object.w.push(warning)};var traverseObject=function(object,parentObject,dataType,parentLevel){var level=parentLevel+1;if(object instanceof Array){for(i in object){object[i]=traverseObject(object[i],parentObject,dataType,level)}return object}if(object instanceof Object){for(property in object){object[property]=traverseObject(object[property],object,object.vt?object.vt:object.t,level)}if(!!object.t){delete (object.w);switch(object.t){case"every":if(level>3){addWarning(object,"Timers are designed to be top-level statements and should not be used inside other statements. If you need a conditional timer, please look into using a while loop instead.")}break;case"on":if(level>3){addWarning(object,"On event statements are designed to be top-level statements and should not be used inside other statements.")}break}}}return object};piston=traverseObject(piston,"piston",null,0);$scope.warnings=warnings;return piston};$scope.compilePiston=function(piston,anonymize,legend){var legend=legend?legend:{};var idx=0;var warnings={};var anonymizeValue=function(key,data){if(!anonymize){return(!!legend[key]&&!!legend[key].id)?legend[key].id:key}if(!key){return""}var safeKey;if(legend[key]){var item=legend[key];safeKey=item.key;if(data&&data.a&&(data.a instanceof Array)&&item.value&&item.value.a&&(item.value.a instanceof Array)){for(a in data.a){if(item.value.a.indexOf(data.a[a])<0){item.value.a.push(data.a[a])}}}}else{safeKey=":"+("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"+idx).substr(-32)+":";idx++;legend[key]={key:safeKey,value:data}}return safeKey};var addWarning=function(object,warning){if(!object){return}object.w=object.w?object.w:[];object.w.push(warning)};var traverseObject=function(object,parentObject,dataType){if(object instanceof Array){for(i in object){object[i]=traverseObject(object[i],parentObject,dataType)}return object}if(object instanceof Object){if(object.exp){delete (object.exp)}if(anonymize){switch(object.vt){case"phone":var phones=object.c?object.c.split(/,;\*\|/):[];var safePhones=[];for(p in phones){safePhones.push(anonymizeValue(phones[p],{t:"phone"}))}object.c=safePhones.join(",");object.e="";break;case"contact":case"contacts":var contacts=object.c?(object.c instanceof Array?object.c:object.c.split(/,;\*|/)):[];var safeContacts=[];for(c in contacts){safeContacts.push(anonymizeValue(contacts[c],{t:"contact"}))}object.c=(object.c instanceof Array)?safeContacts:safeContacts[0];object.e="";break;case"email":object.c=anonymizeValue(object.c,{t:"email"});object.e="";break;case"uri":object.c=anonymizeValue(object.c,{t:"uri"});object.e="";break}}delete (object.w);for(property in object){var v=object[property];if((v===false)||(v===null)||(v==="")){delete (object[property])}else{object[property]=traverseObject(object[property],object,object.vt?object.vt:object.t)}}if(!anonymize&&!!object&&!!object.t&&!!object.vt&&((object.t=="c")||(object.t=="e"))){switch(object.t){case"c":object.exp=$scope.parseString(object.c,object.vt);break;case"e":object.exp=$scope.parseExpression(object.e,false,object.vt);break}}return object}var value=object?object.toString():"";if(value.startsWith(":")&&value.endsWith(":")){if(anonymize){var device=$scope.getDeviceById(object);if(device){object=anonymizeValue(object,{t:"device",n:device.an,a:!!parentObject&&!!parentObject.a&&(parentObject.a.length>1)?[parentObject.a]:[]});return object}var locationMode=$scope.getLocationModeById(object);if(locationMode){switch(locationMode){case"Home":case"Night":case"Sleep":case"Away":case"Vacation":break;default:locationMode="Custom Mode"}object=anonymizeValue(object,{t:"mode",n:locationMode});return object}var routine=$scope.getRoutineById(object);if(routine){object=anonymizeValue(object,{t:"routine"});return object}var contact=$scope.getContactById(object);if(contact){object=anonymizeValue(object,{t:"contact"});return object}}else{object=anonymizeValue(object,{t:"unknown"});return object}}return object};piston=traverseObject($scope.copy(piston),"piston");piston.l={};for(l in legend){piston.l[legend[l].key]=legend[l].value}$scope.warnings=warnings;return piston};$scope.determineDeviceType=function(device){return dataService.determineDeviceType(device)};$scope.anonymizeDevices=function(devices){var cache={};for(i in devices){var device=devices[i];var name=dataService.determineDeviceType(device).replace(/([A-Z])/g," $1").replace(/^./,function(str){return str.toUpperCase()}).replace("Rgb ","RGB ");var idx=cache[name]?cache[name]+1:1;cache[name]=idx;devices[i].an=name+" "+idx}return devices};$scope.anonymizeContacts=function(contacts){var cache={};for(i in contacts){var contact=contacts[i];var name="John Doe";var idx=cache[name]?cache[name]+1:1;cache[name]=idx;contacts[i].an=name+" "+idx}return contacts};$scope.breakList=function(list){return list.replace(/,/g,"
")};var formatDate=function(date){var year=date.getFullYear();var month=(1+date.getMonth()).toString();month=month.length>1?month:"0"+month;var day=date.getDate().toString();day=day.length>1?day:"0"+day;return month+"/"+day+"/"+year};$scope.getMonth=function(date){if(date){return["JAN","FEB","MAR","APR","MAY","JUN","JUL","AUG","SEP","OCT","NOV","DEC"][date.getMonth()]}};$scope.getDay=function(date){if(date){return("0"+date.getDate()).substr(-2)}};$scope.timeSince=timeSince;$scope.timeCounter=timeCounter;$scope.timeLeft=timeLeft;$scope.currentTime=currentTime;$scope.tap=function(tapId){dataService.tap(tapId).then(function(response){})};$scope.test=function(){dataService.testPiston($scope.pistonId)};$scope.togglePiston=function(piston,$event){if((!piston)&&(!$scope.viewerPiston||!$scope.viewerPiston.app)){return}var pistonId=piston?piston.i:$scope.pistonId;if(pistonId){$timeout.cancel(tmrRefresh);var enabled=!(piston?piston.e:$scope.viewerPiston.app.enabled);if(piston){piston.e=enabled}else{$scope.viewerPiston.app.enabled=enabled}if(enabled){dataService.resumePiston(pistonId).then(function(response){$scope.onRefresh(response)})}else{dataService.pausePiston(pistonId).then(function(response){$scope.onRefresh(response)})}}if($event&&e.preventDefault){$event.preventDefault()}if($event&&$event.stopPropagation){$event.stopPropagation()}};$scope.configurePiston=function(piston){$scope.configuredPistonId=$scope.configuredPistonId==piston.i?null:piston.i};$scope.showPiston=function(piston){document.body.scrollTop=0;$scope.viewerPiston=null;$scope.pistonId=piston.i;$scope.refresh();window.onSwipeRight=$scope.hidePiston};$scope.hidePiston=function(){document.body.scrollTop=0;$scope.pistonId=null;window.onSwipeRight=null};$scope.prepareActions=function(condition){var actions=[];var trueActions=[];var falseActions=[];var mainGroup=(condition.id<=0);var tasks=$scope.viewerPiston.tasks;var acts=$scope.viewerPiston.app.actions;for(action in acts){if(acts[action].pid==condition.id){if(acts[action].t){var actionTasks=acts[action].t;for(t in actionTasks){var time=0;for(task in tasks){if((tasks[task].type=="cmd")&&(tasks[task].ownerId==acts[action].id)&&(tasks[task].taskId==actionTasks[t].i)){if((time==0)||(time>tasks[task].time)){time=tasks[task].time}}}actionTasks[t].time=time}}if(mainGroup){actions.push(acts[action])}else{if(acts[action].rs==false){falseActions.push(acts[action])}else{trueActions.push(acts[action])}}}}var time=0;for(task in tasks){if((tasks[task].type=="evt")&&(tasks[task].ownerId==condition.id)){if((time==0)||(time>tasks[task].time)){time=tasks[task].time}}}condition.time=time;condition.actions=actions;condition.trueActions=trueActions;condition.falseActions=falseActions;condition.$scope=$scope;if(condition.children){for(child in condition.children){$scope.prepareActions(condition.children[child])}}};$scope.hadRecentActivity=function(piston){return piston&&piston.le&&piston.le.event&&piston.le.event.date&&(timeLeft((new Date(piston.le.event.date)).getTime())>-120)};$scope.toggleViewerOptions=function(){$scope.viewerPiston.showOptions=!$scope.viewerPiston.showOptions};$scope.getSecondaryStatementName=function(){var mode=$scope.viewerPiston.app.mode;switch(mode){case"Latching":return"BUT IF";case"Then-If":return"THEN IF";case"Else-If":return"ELSE IF";case"Or-If":return"OR IF";case"And-If":return"AND IF"}return"IF"};$scope.serializeObject=function(object){return angular.toJson(object)};$scope.anonymizeObject=function(object,returnAsString){var data=$scope.serializeObject(object);var matches=data.match(/(:[a-f0-9]{32}:)/g);if(matches){matches=matches.unique()}for(i in matches){data=data.replace(new RegExp(matches[i],"g"),("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"+i).substr(-32))}return(returnAsString?data:angular.fromJson(data))};$scope.objectToBlob=function(object,contentType){contentType=contentType||"";var sliceSize=1024;var data=utoa($scope.serializeObject(object));data+="|"+data.length.toString();var bytesLength=data.length;var slicesCount=Math.ceil(bytesLength/sliceSize);var byteArrays=new Array(slicesCount);for(var sliceIndex=0;sliceIndexstartIndex){var value=str.slice(startIndex,i-1).trim();var parsedValue=parseFloat(value.trim());if(!isNaN(parsedValue)&&(numExp.test(value.trim()))){arr.push({t:(value.indexOf(".")>=0?"decimal":"integer"),v:parsedValue,l:location(startIndex,i-2)});return true}if(typeof value=="string"){if(["true","false"].indexOf(value)>=0){arr.push({t:"boolean",v:value,l:location(startIndex,i-2)})}else{if(["null"].indexOf(value)>=0){arr.push({t:"dynamic",v:null,l:location(startIndex,i-2)})}else{arr.push({t:"variable",x:value,l:location(startIndex,i-2)})}}return true}arr.push({t:"operand",v:str.slice(startIndex,i-1),l:location(startIndex,i-2)});return true}return false}function addConstant(allowEmpty){if(i-(allowEmpty?0:1)>startIndex){var value=str.slice(startIndex,i-1).replace(/\\[\[\]\{\}\'\"0-9abcdefghijklmopqsuvwxyz]/gi,function(match){return match[1]});var parsedValue=parseFloat(value.trim());if((dataType!="phone")&&!isNaN(parsedValue)&&(numExp.test(value.trim()))){arr.push({t:(value.indexOf(".")>=0?"decimal":"integer"),v:parsedValue,l:location(startIndex,i-2)});return true}arr.push({t:(["true","false"].indexOf(value)>=0?"boolean":"string"),v:(value=="null"?null:value),l:location(startIndex,i-2)})}}function addDevice(){if(i-1>startIndex){var value=str.slice(startIndex,i-1);var pos=value.lastIndexOf(":");var deviceName=value;var attribute="";if(pos>0){var deviceName=value.substr(0,pos).trim().replace(/\\[\[\]\{\}\'\"0-9abcdefghijklmopqsuvwxyz]/gi,function(match){return match[1]});attribute=value.substr(pos+1).trim()}var device=$scope.getDeviceByName(deviceName);if(device&&device.id){var a=attribute.toLowerCase();attribute="";virtualAttribute="";switch(a){case"orientation":case"axisx":case"axisy":case"axisz":virtualAttribute=a.replace("axisx","axisX").replace("axisy","axisY").replace("axisz","axisZ");a="threeaxis"}if(a==statusAttribute){attribute=statusAttribute}else{for(attributeIndex in device.a){var attr=device.a[attributeIndex];if(a==attr.n.toLowerCase()){attribute=virtualAttribute?virtualAttribute:attr.n}}}if(!!a&&!attribute){attribute="?"}arr.push({t:"device",id:device.id,a:attribute,l:location(startIndex-1,i-1)})}else{arr.push({t:"device",x:deviceName,a:attribute,l:location(startIndex-1,i-1)})}}}function addFunction(){var value=str.slice(startIndex,i-1).toLowerCase().trim();if($scope.db.functions[value]){func++;var params=main();var items=[];var item=null;for(p in params){if(!item){item={t:"expression",i:[]}}if((params[p].t=="operator")&&(params[p].o==",")){items.push(item);item={t:"expression",i:[]}}else{item.i.push(params[p])}}if(item){items.push(item)}arr.push({t:"function",n:value,i:items,l:location(startIndex,i-1)});func--}else{addOperand();arr.push({t:"expression",i:main(),l:location(startIndex,i-1)});startIndex=i}}var compositeVariable=isCompositeVariable();while(i":case"?":case":":var c2=(i=","<=","<>","<<",">>"].indexOf(c+c2)>=0){i++;c+=c2}arr.push({t:"operator",o:c,l:location(i-1,i-1)});startIndex=i;compositeVariable=isCompositeVariable()}else{if(c=="\\"){i++;c=c2}}continue;case'"':case"“":case"”":if(exp&&!dv&&!sq){dq=!dq;odq=!odq;(dq?addOperand():addConstant(true));startIndex=i;compositeVariable=isCompositeVariable()}continue;case"'":case"‘":case"’":if(exp&&!dq&&!dv){sq=!sq;osq=!osq;(sq?addOperand():addConstant(true));startIndex=i;compositeVariable=isCompositeVariable()}continue;case"(":if(exp&&!dv&&!dq&&!sq){parenthesis++;addFunction();startIndex=i;compositeVariable=isCompositeVariable()}continue;case")":if(exp&&!dv&&!dq&&!sq){parenthesis--;addOperand();startIndex=i;return arr}continue;case"[":if(!compositeVariable&&exp&&!dq&&!sq&&!dv){dv=true;addOperand();startIndex=i;compositeVariable=isCompositeVariable()}continue;case"]":if(!compositeVariable&&exp&&dv&&!dq&&!sq){addDevice();dv=false;startIndex=i;compositeVariable=isCompositeVariable()}continue;case"{":if(exp==initExp){exp++;addConstant();startIndex=i;arr.push({t:"expression",i:main(),l:location(startIndex-1,i-1)});startIndex=i;compositeVariable=isCompositeVariable()}else{exp++;startIndex=i;arr.push({t:"expression",i:main(),l:location(startIndex-1,i-1)});startIndex=i;compositeVariable=isCompositeVariable()}continue;case"}":addOperand();exp--;return arr;continue}}i++;exp?addOperand():addConstant();return arr}var items=main();var result={t:"expression",i:items,str:str};if(exp!=initExp){result.err="Invalid expression closure termination"}else{if(osq){result.err="Invalid single quote termination"}else{if(odq){result.err="Invalid double quote termination"}else{if(parenthesis){result.err="Invalid parenthesis closure termination"}}}}result.ok=!result.err;if(result.ok){result.ok=$scope.validateExpression(result)}return result};$scope.validateExpression=function(expression){var error="";var errVar="";var errorLoc="";function getSubstring(location,separator,partNo){if(!location){return""}location=location.toString().split(":");var start=parseInt(location[0]);var end=(location.length==2)?parseInt(location[1]):start;var s=expression.str.substr(start,end-start+1);if((s.substr(0,1)=="[")&&(s.substr(-1,1)=="]")){s=s.substr(1,s.length-2)}if(separator){s=s.split(separator);if(partNo>=s.length){return""}return s[partNo].trim()}return s.trim()}function validateItem(item){var ok=true;var err="";var loc="";if(item.i){for(subitem in item.i){ok=ok&&validateItem(item.i[subitem])}}else{switch(item.t){case"device":if(!item.x&&!item.id){ok=false;err="Invalid device "+getSubstring(item.l,":",0);loc=item.l;break}if(!item.id&&item.x&&!(($scope.systemVars&&scope.systemVars[item.x])||($scope.globalVars&&$scope.systemVars[item.x])||$scope.getVariableByName(item.x))){ok=false;err="Invalid device variable "+getSubstring(item.l,":",0);loc=item.l;break}if(item.a=="?"){ok=false;err="Invalid attribute "+getSubstring(item.l,":",1);loc=item.l;break}break;case"variable":if(item.x.startsWith("$args.")&&(item.x.length>6)){break}if(item.x.startsWith("$args[")&&(item.x.length>6)){break}if(item.x.startsWith("$json.")&&(item.x.length>6)){break}if(item.x.startsWith("$json[")&&(item.x.length>6)){break}if(item.x.startsWith("$places.")&&(item.x.length>8)){break}if(item.x.startsWith("$places[")&&(item.x.length>8)){break}if(item.x.startsWith("$response.")&&(item.x.length>10)){break}if(item.x.startsWith("$response[")&&(item.x.length>10)){break}if(item.x.startsWith("$nfl.")&&(item.x.length>5)){break}if(item.x.startsWith("$weather.")&&(item.x.length>9)){break}if(item.x.startsWith("$incidents.")&&(item.x.length>11)){break}if(item.x.startsWith("$incidents[")&&(item.x.length>11)){break}if($scope.systemVars&&$scope.systemVars[item.x]){break}if($scope.globalVars&&$scope.globalVars[item.x]){break}if(!$scope.getVariableByName(item.x)){if(item.x.indexOf("[")>=0){var v=$scope.getVariableByName(item.x.split("[")[0]);if(v&&v.t.endsWith("]")){break}}ok=false;errVar=getSubstring(item.l);err="Variable "+errVar+" not found";loc=item.l;break}break}}item.ok=ok;if(err){item.err=err;if(!error){error=err;errorLoc=loc}}return ok}validateItem(expression);if(error){expression.err=error;expression.errVar=errVar;expression.loc=errorLoc}return expression.ok};$scope.hexToHsl=function(hex){var rgb=/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);if(!rgb){return{h:0,s:0,l:0}}var r=0+parseInt(rgb[1],16)/255;var g=0+parseInt(rgb[2],16)/255;var b=0+parseInt(rgb[3],16)/255;var max=Math.max(r,g,b),min=Math.min(r,g,b);var h,s,l=(max+min)/2;if(max==min){h=s=0}else{var d=max-min;s=(l>0.5)?d/(2-max-min):d/(max+min);switch(max){case r:h=(g-b)/d+(g=0)||($scope.evalText=="")){var delta=(event.originalEvent.keyCode==38?-1:(event.originalEvent.keyCode==40?1:0));if(delta==0){return}var i=$scope.lastEval+delta;if(i>=$scope.evals.length){i=0}if(i<0){i=$scope.evals.length-1}if((i>=0)&&(i<$scope.evals.length)){$scope.evalText=$scope.evals[i].text}$scope.lastEval=i}};$scope.onEvalKeyPress=function(event){if(event.originalEvent.keyCode!=13){return}$scope.lastEval=-1;var text=$scope.evalText;switch(text){case"/clear":$scope.evalText="";$scope.evals=[];return}var eval={type:($scope.evalType=="e"?"expression":"value"),text:text,eval:""};if($scope.evalType=="e"){eval.eval=$scope.evaluateExpression(text,null,eval,true)}else{eval.eval=$scope.evaluateValue(text,null,eval,true)}$scope.evals.push(eval);$scope.evalText="";$scope.$$postDigest(function(){var d=$("console > content");d.scrollTop(d.prop("scrollHeight"))})};$scope.delayEvaluation=function(operand){if(!operand){return}operand.eval="...";var expression=operand.data.exp;var dataType=operand.data.vt;switch(dataType){case"s":case"m":case"h":case"d":case"w":case"n":case"y":dataType="ms"}$timeout.cancel(operand.tmrDelayEvaluation);operand.tmrDelayEvaluation=$timeout(function(){if($scope.designer&&$scope.designer.dialog){operand.eval="(evaluating)";evaluateExpression(expression,dataType,operand)}},2500)};$scope.evaluateValue=function(value,dataType,output,showType){return $scope.evaluateExpression($scope.parseExpression(value,true),dataType,output,showType)};$scope.evaluateExpression=function(expression,dataType,output,showType){var useConsole=!(output instanceof Object);if(!(expression instanceof Object)){expression=$scope.parseExpression(expression)}if(!(expression instanceof Object)){return"Evaluation error: unknown error."}if(expression.err){return"Evaluation error: "+expression.err}dataService.evaluateExpression($scope.pistonId,expression,dataType).then(function(response){var result="";if(!response||(response.status!="ST_SUCCESS")){result="Evaluation error: Received a "+(response?response.status:"(unknown)")+" result."}else{result=(!!useConsole||!!showType?"("+response.value.t+") ":"")+response.value.v}if(useConsole){console.log(result)}else{output.eval=$scope.renderString(result)}});return"(evaluating)"};window.evaluateValue=$scope.evaluateValue;window.evaluateExpression=$scope.evaluateExpression;var userAgent=navigator.userAgent||navigator.vendor||window.opera;if(userAgent.match(/Android/i)){$scope.android=true}$scope.url=window.location.href;$scope.mobile=window.mobileCheck();$scope.tablet=(!$scope.mobile)&&(window.mobileOrTabletCheck());$scope.formatTime=window.formatTime;$scope.utcToString=utcToString;$scope.utcToTimeString=utcToTimeString;$scope.utcToDateString=utcToDateString;$scope.formatLogTime=function(timestamp,offset){return utcToString(timestamp)+"+"+offset};$scope.md5=window.md5;var tmrInit=setInterval(function(){if(dataService.ready()){clearInterval(tmrInit);$scope.init()}},1)}]);function test(g,b,f){scope.evaluateExpression(scope.parseExpression(g,b,f))}var MAX_STACK_SIZE=10;config.controller("fuel",["$scope","$rootScope","dataService","$timeout","$interval","$location","$sce","$routeParams","ngDialog","$window",function(q,m,o,c,f,e,l,p,n,b){var a=null;var d=null;var g=null;q.initialized=false;q.loading=true;q.canisters=[];q.fuelStreams=[];q.selectedCanister="";q.error="";q.designer={};q.locations=null;q.instances=null;q.requestId=0;q.activePistons=0;q.pausedPistons=0;q.dropDownMenu=false;q.view="piston";q.init=function(s,u,t){if(q.$$destroyed){return}o.setStatusCallback(q.setStatus);o.listFuelStreams().then(function(x){if(q.$$destroyed){return}q.loading=false;q.initialized=true;if(!x||!x.fuelStreams||!(x.fuelStreams instanceof Array)){return}q.fuelStreams=x.fuelStreams;var v=[];for(i in q.fuelStreams){var w=q.fuelStreams[i];v.push(w.c)}q.canisters=v.unique().sort()});q.initChart()};q.selectCanister=function(s){q.selectedCanister=s};q.selectFuelStream=function(s){for(i in q.fuelStreams){q.fuelStreams[i].selected=(q.fuelStreams[i]==s)}q.prepareFuelStream(s);return};q.prepareFuelStream=function(s){if(s.selected&&!s.data){q.loading=true;o.listFuelStreamData(s.i).then(function(t){if(t&&t.points&&(t.points instanceof Array)){s.data=t.points;q.populateChart()}q.loading=false})}else{q.populateChart()}};q.populateChart=function(){var B=0;var D=[];var A=[];var z=null;var x=0;var C=!!q.chart.options.isStacked;function w(t){return !isNaN(parseFloat(t))&&isFinite(t)}var y=0;for(i in q.fuelStreams){if(!!q.fuelStreams[i].selected&&!!q.fuelStreams[i].data&&q.fuelStreams[i].data.length){y++}}for(i in q.fuelStreams){if(!!q.fuelStreams[i].selected&&!!q.fuelStreams[i].data&&q.fuelStreams[i].data.length){var G=q.fuelStreams[i];z=z?z:{cols:[{id:"time",label:"Time",type:"datetime"}],rows:[]};var F=w(G.data[0].d)?"number":"string";z.cols.push({id:i,label:(G.c?G.c+" \\ ":"")+G.n,type:F});var u=[];for(k=0;kB+1){q.chart.view.columns.pop()}};q.toggleFuelStream=function(s){s.selected=!s.selected;q.prepareFuelStream(s)};q.setStatus=function(s){if(a){c.cancel(a)}a=null;q.status=s;if(q.status){a=c(function(){q.setStatus()},10000)}};q.sortByDisplay=function(t,s){return(t.d>s.d)?1:((s.d>t.d)?-1:0)};q.sortByName=function(t,s){return(t.n>s.n)?1:((s.n>t.n)?-1:0)};q.home=function(){q.initialized=false;e.path("/")};q.initChart=function(t,s){q.chart={type:"AreaChart",displayed:false,data:null,options:{isStacked:false,fill:20,displayExactValues:true,interpolateNulls:true,explorer:{axis:"horizontal"},is3D:true,width:"100%",height:"100%",pointSize:6,dataOpacity:0.5,pointShape:"square",series:{0:{pointShape:"circle"},1:{pointShape:"square"},2:{pointShape:"diamond"},3:{pointShape:"polygon"},4:{pointShape:"triangle"},5:{pointShape:"star"}},chartArea:{left:96,top:16,right:16,width:"100%",height:"80%"},legend:{position:"bottom"}},hAxis:{title:"Date/Time"},formatters:{},view:{columns:[]}}};q.hideSeries=function(t){var s=t.column;if(t.row===null){if(q.chart.view.columns[s]==s){q.chart.view.columns[s]={label:q.chart.data.cols[s].label,type:q.chart.data.cols[s].type,calc:function(){return null}}}else{q.chart.view.columns[s]=s}}};var r=navigator.userAgent||navigator.vendor||window.opera;if(r.match(/Android/i)){q.android=true}q.url=window.location.href;q.mobile=window.mobileCheck();q.tablet=(!q.mobile)&&(window.mobileOrTabletCheck());q.formatTime=formatTime;q.utcToString=utcToString;window.scope=q;var h=setInterval(function(){if(o.ready()){clearInterval(h);q.init()}},1)}]);config.controller("visors",["$scope","$rootScope","dataService","$timeout","$interval","$location","$sce","$routeParams","ngDialog","$window",function(q,l,o,c,j,g,k,p,n,b){var m=128;var f=128;var e=false;q.visor={tiles:[],grid:{cols:15,rows:8}};q.placeholders=[];q.dragger={};q.scale=1;q.tileTypes=[{name:"Temperature",type:"temperature",template:"temperature",icon:"thermometer",description:"Provides information about temperature",attributes:["temperature"]},{name:"Switch",type:"switch",template:"switch",icon:"switch",description:"Provides information about a generic switch",attributes:["switch"]},{name:"Contact",type:"contact",template:"contact",icon:"circle-o-notch",description:"Provides information about a generic contact sensor",attributes:["contact"]},{name:"Contact (door)",type:"door-contact",template:"contact",icon:"circle-o-notch",description:"Provides information about a door contact sensor",attributes:["contact"]},{name:"Contact (window)",type:"window-contact",template:"contact",icon:"circle-o-notch",description:"Provides information about a window contact sensor",attributes:["contact"]},{name:"Presence",type:"presence",template:"presence",icon:"circle-o-notch",description:"Provides information about a window contact sensor",attributes:["presence"]},{name:"Presence Map",type:"presence-map",template:"presence-map",icon:"circle-o-notch",description:"Provides information about a window contact sensor",attributes:["latitude","longitude"]}];q.getTemplateName=function(h){for(i in q.tileTypes){if(q.tileTypes[i].type==h){return q.tileTypes[i].template}}return"unknown"};q.getTemplateAttributes=function(h){for(i in q.tileTypes){if(q.tileTypes[i].type==h){return q.tileTypes[i].attributes}}return"unknown"};q.init=function(){q.visor.tiles.push({z:"Piston state",t:"contact",i:0,g:"auto",sz:{w:8,h:1}});q.visor.tiles.push({z:"xx",i:8,t:"contact",g:"auto",sz:{w:1,h:1}});q.visor.tiles.push({z:"yy",i:9,t:"contact",g:"auto",sz:{w:1,h:1}});q.visor.tiles.push({z:"zz",i:71,t:"contact",g:"auto",sz:{w:4,h:4}});q.prepare();q.ds=o;q.mobile=window.mobileCheck();window.scope=q};q.copy=function(h){return angular.fromJson(angular.toJson(h))};q.prepare=function(){var w=q.visor.grid.cols;var v=q.visor.grid.rows;for(var s=0;su?u:w;q.scale=t;return{width:(m*s)+"px",height:(f*r)+"px",transform:"scale("+t+")",left:((document.documentElement.clientWidth-h*t)/2)+"px",top:((document.documentElement.clientHeight-48-v*t)/2)+"px"}};q.onSizeChanged=function(){q.$apply()};q.setDesignerType=function(h){q.designer.type=h;q.designer.tile.t=h;(!q.designer.page)?q.nextPage():q.refreshSelects();if(q.designer.ontypechanged){q.designer.ontypechanged(q.designer,h)}q.updateDeviceList(h)};q.listDevicesWithAttributes=function(s){var r=o.listDevices();if(!s||!(s instanceof Array)||!s.length){return r}var h=[];for(d in r){var t=r[d];var u=0;for(a in t.a){if(s.indexOf(t.a[a].n)>=0){u++;if(u==s.length){break}}}if(u==s.length){h.push(t)}}return h};q.updateDeviceList=function(h){q.designer.devices=q.listDevicesWithAttributes(q.getTemplateAttributes(h));return;q.designer.devices=[];for(deviceId in devices){q.designer.devices.push(mergeObjects({id:deviceId},devices[deviceId]))}};q.closeDialog=function(){if(q.designer.dialog){q.designer.dialog.close();q.designer.dialog=null}};q.nextPage=function(){q.designer.page++;q.refreshSelects()};q.prevPage=function(){if(q.designer.page){q.designer.page--}};q.range=function(h){return new Array(h)};q.refreshSelects=function(h){if(h){q.$$postDigest(function(){$("select["+h+"]").selectpicker("refresh");c(function(){$("select["+h+"]").selectpicker("refresh")},0,false)})}else{q.$$postDigest(function(){$("select[selectpicker]").selectpicker("refresh");c(function(){$("select[selectpicker]").selectpicker("refresh")},0,false)})}};q.startDrag=function(h,r){if(!!q.dragger.dragging&&(q.dragger.tile==r)){return}q.dragger={tile:r,dragging:false,start:{x:h.pageX,y:h.pageY},offset:{x:h.clientX,y:h.clientY}}};q.drag=function(t){if(!q.dragger.tile){return}var z=q.visor.grid.cols;var w=q.visor.grid.rows;q.dragger.dragging=q.dragger.dragging|((Math.abs(t.pageX-q.dragger.start.x)>5)||(Math.abs(t.pageY-q.dragger.start.y)>5));if(!q.dragger.dragging){return}var v=q.dragger.tile;var s=Math.round((v.$$pos.x+(t.pageX-q.dragger.start.x)/q.scale)/m);var u=s+v.sz.w;var A=Math.round((v.$$pos.y+(t.pageY-q.dragger.start.y)/q.scale)/f);var h=A+v.sz.h;s=s<0?0:(u>z?z-v.sz.w:s);A=A<0?0:(h>w?w-v.sz.h:A);q.dragger.index=z*A+s;v.$$style.left=(s*m)+"px";v.$$style.top=(A*f)+"px"};q.endDrag=function(h){if(q.dragger.dragging){q.dragger.tile.i=q.dragger.index;q.prepare();e=true;c(function(){e=false},50)}q.dragger={}};q.setTileSize=function(r,h){if((r>q.visor.grid.cols)||(h>q.visor.grid.rows)){return}q.designer.tile.sz={w:r,h:h}};q.addTile=function(r,h){return q.editTile(null,h,r)};q.editTile=function(s,r,h){if(r){r.stopPropagation()}if(e){e=false;return}q.selectedTile=null;if(!s){s={};s.i=h;s.t=null;s.d=[];s.s="i";s.z="";s.sz={w:1,h:1};s.f="a";s.fc="#000000";s.b="a";s.bc="#eeeeee";s.g="auto"}q.designer={};q.designer.$obj=s;q.designer.$tile=s;q.designer.tile=q.copy(s);q.designer.$new=s.t?false:true;q.designer.page=s.t?1:0;if(s.t){q.updateDeviceList(s.t)}q.designer.dialog=n.open({template:"dialog-edit-tile",className:"ngdialog-theme-default ngdialog-large",closeByDocument:false,disableAnimation:true,scope:q})};q.updateTile=function(){var h=q.designer.tile;if(q.designer.$new){q.visor.tiles.push(h)}else{$.extend(q.designer.$tile,h)}q.prepare();q.closeDialog()};q.init()}]); \ 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 328c32bb..67e94455 100644 --- a/smartapps/ady624/webcore-piston.src/webcore-piston.groovy +++ b/smartapps/ady624/webcore-piston.src/webcore-piston.groovy @@ -402,6 +402,10 @@ def pageClearAll() { /*** ***/ /******************************************************************************/ +def isInstalled(){ + return !!state.created +} + def installed() { state.created = now() state.modified = now() @@ -1745,23 +1749,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) { //not supported 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) { //not supported 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()" } } } diff --git a/smartapps/ady624/webcore.src/webcore.groovy b/smartapps/ady624/webcore.src/webcore.groovy index 7cefa3c9..e7f0672d 100644 --- a/smartapps/ady624/webcore.src/webcore.groovy +++ b/smartapps/ady624/webcore.src/webcore.groovy @@ -1055,7 +1055,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 (hubUID && !piston.isInstalled()) piston.installed() result = [status: "ST_SUCCESS", id: hashId(piston.id)] } else { result = api_get_error_result("ERR_INVALID_TOKEN") @@ -1804,6 +1804,7 @@ private def transformCommand(command, overrides){ return command.getName() } + private setPowerSource(powerSource, atomic = true) { if (state.powerSource == powerSource) return if (atomic) { From 769b7ba87fe3c8634ceefd1950df0b68721c964d Mon Sep 17 00:00:00 2001 From: jp0550 Date: Sun, 5 Aug 2018 19:08:35 -0500 Subject: [PATCH 26/55] Hsm fixes --- .../webcore-piston.src/webcore-piston.groovy | 9 ++++++++- smartapps/ady624/webcore.src/webcore.groovy | 19 +++++++++++++++---- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/smartapps/ady624/webcore-piston.src/webcore-piston.groovy b/smartapps/ady624/webcore-piston.src/webcore-piston.groovy index 67e94455..78787225 100644 --- a/smartapps/ady624/webcore-piston.src/webcore-piston.groovy +++ b/smartapps/ady624/webcore-piston.src/webcore-piston.groovy @@ -2388,7 +2388,7 @@ 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 status = rtData.virtualDevices['alarmSystemStatus']?.ac?.find{ (it.key == statusIdOrName) || (it.value == statusIdOrName)}.collect{ [id: it.key, name: it.value] } if (status && status.size()) { sendLocationEvent(name: 'hsmSetArm', value: status[0].id) } else { @@ -3704,6 +3704,9 @@ private evaluateOperand(rtData, node, operand, index = null, trigger = false, ne 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 'powerSource': values = [[i: "${node?.$}:v", v:[t: 'enum', v:rtData.powerSource]]]; break; @@ -4449,6 +4452,10 @@ private void subscribeAll(rtData) { case 'alarmSystemAlert': subscriptionId = "$deviceId${operand.v}" attribute = "hsmAlert" + break; + case 'alarmSystemEvent': + subscriptionId = "$deviceId${operand.v}" + attribute = "hsmSetArm" break; case 'time': case 'date': diff --git a/smartapps/ady624/webcore.src/webcore.groovy b/smartapps/ady624/webcore.src/webcore.groovy index e7f0672d..b1db5776 100644 --- a/smartapps/ady624/webcore.src/webcore.groovy +++ b/smartapps/ady624/webcore.src/webcore.groovy @@ -2792,7 +2792,7 @@ private static Map virtualCommands() { setTile : [ n: "Set piston tile...", a: true, i: "superscript", d: "Set piston tile #{0} title to \"{1}\", text to \"{2}\", footer to \"{3}\", and colors to {4} over {5}{6}", p: [[n:"Tile Index",t:"enum",o:tileIndexes],[n:"Title",t:"string"],[n:"Text",t:"string"],[n:"Footer",t:"string"],[n:"Text Color",t:"color"],[n:"Background Color",t:"color"],[n:"Flash mode",t:"boolean",d:" (flashing)"]], ], clearTile : [ n: "Clear piston tile...", a: true, i: "superscript", d: "Clear piston tile #{0}", p: [[n:"Tile Index",t:"enum",o:tileIndexes]], ], setLocationMode : [ n: "Set location mode...", a: true, i: "", d: "Set location mode to {0}", p: [[n:"Mode",t:"mode"]], ], - setAlarmSystemStatus : [ n: "Set Smart Home Monitor status...", a: true, i: "", d: "Set Smart Home Monitor status to {0}", p: [[n:"Status", t:"alarmSystemStatus"]], ], + 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]}]], ], sendEmail : [ n: "Send email...", a: true, i: "envelope", d: "Send email with subject \"{1}\" to {0}", p: [[n:"Recipient",t:"email"],[n:"Subject",t:"string"],[n:"Message body",t:"string"]], ], wolRequest : [ n: "Wake a LAN device", a: true, i: "", d: "Wake LAN device at address {0}{1}", p: [[n:"MAC address",t:"string"],[n:"Secure code",t:"string",d:" with secure code {v}"]], ], adjustLevel : [ n: "Adjust level...", r: ["setLevel"], i: "toggle-on", d: "Adjust level by {0}%{1}", p: [[n:"Adjustment",t:"integer",r:[-100,100]], [n:"Only if switch is...", t:"enum",o:["on","off"], d:" if already {v}"]], ], @@ -3052,7 +3052,7 @@ private Map getLocationModeOptions(updateCache = false) { } return result } -private static Map getAlarmSystemStatusOptions() { +private static Map getAlarmSystemStatusActions() { return [ armAll: "Arm All", armRules: "Arm Monitor Rules", @@ -3065,6 +3065,15 @@ private static Map getAlarmSystemStatusOptions() { ] } +private static Map getAlarmSystemStatusOptions() { + return [ + armedAway: "Armed Away", + armedHome: "Armed Home", + disarmed: "Disarmed", + allDisarmed: "All Disarmed" + ] +} + private static Map getAlarmSystemAlertOptions() { return [ intrusion: "Intrusion", @@ -3106,8 +3115,10 @@ 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: 'Hubitat Safety Monitor status', t: 'enum', o: getAlarmSystemStatusOptions(), x: true], - alarmSystemAlert: [ n: 'Hubitat Safety Monitor alert',t: 'enum', o: getAlarmSystemAlertOptions(), m: true] + alarmSystemStatus: [ n: 'Hubitat Safety Monitor status',t: 'enum', o: getAlarmSystemStatusOptions(), ac: getAlarmSystemStatusActions(), x: true], + //this one can be confusing to users so it's been commented out. It can subscribe to hsmSetArm, but the safety monitor doesn't actually send these events themselves, only other apps + //alarmSystemEvent: [ n: 'Hubitat Safety Monitor event',t: 'enum', o: getAlarmSystemStatusActions(), m: true], + alarmSystemAlert: [ n: 'Hubitat Safety Monitor alert',t: 'enum', o: getAlarmSystemAlertOptions(), m: true] ] } public Map getColorByName(name){ From e35ca9bb60bf4f992e182b8e581689745d6062bb Mon Sep 17 00:00:00 2001 From: jp0550 Date: Sun, 5 Aug 2018 19:47:10 -0500 Subject: [PATCH 27/55] Add support for hsm rule arm/disarming --- .../ady624/webcore-piston.src/webcore-piston.groovy | 7 +++++++ smartapps/ady624/webcore.src/webcore.groovy | 10 +++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/smartapps/ady624/webcore-piston.src/webcore-piston.groovy b/smartapps/ady624/webcore-piston.src/webcore-piston.groovy index 78787225..d78d11f3 100644 --- a/smartapps/ady624/webcore-piston.src/webcore-piston.groovy +++ b/smartapps/ady624/webcore-piston.src/webcore-piston.groovy @@ -3707,6 +3707,9 @@ private evaluateOperand(rtData, node, operand, index = null, trigger = false, ne 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; @@ -4456,6 +4459,10 @@ private void subscribeAll(rtData) { case 'alarmSystemEvent': subscriptionId = "$deviceId${operand.v}" attribute = "hsmSetArm" + break; + case 'alarmSystemRule': + subscriptionId = "$deviceId${operand.v}" + attribute = "hsmRules" break; case 'time': case 'date': diff --git a/smartapps/ady624/webcore.src/webcore.groovy b/smartapps/ady624/webcore.src/webcore.groovy index b1db5776..63266d44 100644 --- a/smartapps/ady624/webcore.src/webcore.groovy +++ b/smartapps/ady624/webcore.src/webcore.groovy @@ -3083,6 +3083,13 @@ private static Map getAlarmSystemAlertOptions() { ] } +private static Map getAlarmSystemRuleOptions() { + return [ + armedRule: "Armed Rule", + disarmedRule: "Disarmed Rule" + ] +} + private Map getRoutineOptions(updateCache = false) { def routines = location.helloHome?.getPhrases()?.sort{ it?.label ?: '' } @@ -3118,7 +3125,8 @@ private Map virtualDevices(updateCache = false) { alarmSystemStatus: [ n: 'Hubitat Safety Monitor status',t: 'enum', o: getAlarmSystemStatusOptions(), ac: getAlarmSystemStatusActions(), x: true], //this one can be confusing to users so it's been commented out. It can subscribe to hsmSetArm, but the safety monitor doesn't actually send these events themselves, only other apps //alarmSystemEvent: [ n: 'Hubitat Safety Monitor event',t: 'enum', o: getAlarmSystemStatusActions(), m: true], - alarmSystemAlert: [ n: 'Hubitat Safety Monitor alert',t: 'enum', o: getAlarmSystemAlertOptions(), 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 Map getColorByName(name){ From 5fefc65c8591e5622c15c1180980fe38687a5f86 Mon Sep 17 00:00:00 2001 From: jp0550 Date: Wed, 8 Aug 2018 13:40:16 -0500 Subject: [PATCH 28/55] Null check on stats --- smartapps/ady624/webcore.src/webcore.groovy | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/smartapps/ady624/webcore.src/webcore.groovy b/smartapps/ady624/webcore.src/webcore.groovy index fe75b511..673982a9 100644 --- a/smartapps/ady624/webcore.src/webcore.groovy +++ b/smartapps/ady624/webcore.src/webcore.groovy @@ -1110,8 +1110,8 @@ private api_intf_dashboard_piston_get() { 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 = [] + 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) From f504c057dfa90f43b9812ee126866cb229927635 Mon Sep 17 00:00:00 2001 From: jp0550 Date: Sun, 12 Aug 2018 19:25:31 -0500 Subject: [PATCH 29/55] Common codebase --- dashboard/js/app.js | 119 +++++-- dashboard/js/modules/dashboard.module.js | 2 +- dashboard/js/modules/piston.module.js | 4 +- .../webcore-dashboard.groovy | 22 +- .../webcore-piston.src/webcore-piston.groovy | 232 ++++++++----- .../webcore-storage.groovy | 9 +- smartapps/ady624/webcore.src/webcore.groovy | 310 +++++++++++++----- 7 files changed, 474 insertions(+), 224 deletions(-) diff --git a/dashboard/js/app.js b/dashboard/js/app.js index 9fc1e795..9a42a79f 100644 --- a/dashboard/js/app.js +++ b/dashboard/js/app.js @@ -1333,21 +1333,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; - }); } } @@ -1396,21 +1426,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 62aa7589..0ef6444e 100644 --- a/dashboard/js/modules/dashboard.module.js +++ b/dashboard/js/modules/dashboard.module.js @@ -34,7 +34,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:' + '?access_token=' + data.accessToken; + $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 fec3a143..afdea8cd 100644 --- a/dashboard/js/modules/piston.module.js +++ b/dashboard/js/modules/piston.module.js @@ -205,7 +205,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 + '?access_token=' + si.accessToken; + $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) { @@ -787,7 +787,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 + '?access_token=' + si.accessToken; + return uri + 'ifttt/' + eventName + (si.accessToken ? '?access_token=' + si.accessToken : ''); } $scope.toggleAdvancedOptions = function() { diff --git a/smartapps/ady624/webcore-dashboard.src/webcore-dashboard.groovy b/smartapps/ady624/webcore-dashboard.src/webcore-dashboard.groovy index f6e1698e..ff4bc14d 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.107.20180806" } /*** webCoRE DEFINITION ***/ /******************************************************************************/ private static String handle() { return "webCoRE" } -//include 'asynchttp_v1' +if(!hubUID)include 'asynchttp_v1' definition( name: "${handle()} Dashboard", namespace: "ady624", @@ -156,20 +156,12 @@ private void broadcastEvent(deviceId, eventName, eventValue, eventTime) { body: [d: deviceId, n: eventName, v: eventValue, t: eventTime] ] - asynchttpPut((String)null, params) - - /* - 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 - ] - ])*/ + if(asynchttp_v1){ + asynchttp_v1.put(null, params) + } + else { + asynchttpPut((String)null, params) + } } /******************************************************************************/ diff --git a/smartapps/ady624/webcore-piston.src/webcore-piston.groovy b/smartapps/ady624/webcore-piston.src/webcore-piston.groovy index df45d1fc..197748af 100644 --- a/smartapps/ady624/webcore-piston.src/webcore-piston.groovy +++ b/smartapps/ady624/webcore-piston.src/webcore-piston.groovy @@ -288,7 +288,13 @@ public static String version() { return "v0.3.107.20180806" } /*** webCoRE DEFINITION ***/ /******************************************************************************/ private static String handle() { return "webCoRE" } -//include 'asynchttp_v1' +import hubitat.device.HubAction +import hubitat.device.Protocol +//import physicalgraph.device.HubAction +//import physicalgraph.device.Protocol + +if(!hubUID)include 'asynchttp_v1' + definition( name: "${handle()} Piston", namespace: "ady624", @@ -726,7 +732,7 @@ 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 - def atomState = getCachedAtomicState() + def atomState = hubUID ? getCachedAtomicState() : atomicState rtData.cache = atomState.cache ?: [:] rtData.newCache = [:] @@ -749,6 +755,8 @@ private getRunTimeData(rtData = null, semaphore = null, fetchWrappers = false) { rtData.fastForwardTo = null rtData.break = false rtData.updateDevices = false + rtData.timeLimits = getTimeLimits() + state.schedules = atomState.schedules if (!fetchWrappers) { rtData.devices = (settings.dev && (settings.dev instanceof List) ? settings.dev.collectEntries{[(hashId(it.id)): it]} : [:]) @@ -796,6 +804,11 @@ try { } } +//new and improved timeout recovery management +def timeoutRecoveryHandler_webCoRE(event) { + timeHandler([t:now()], true) +} + def timeRecoveryHandler(event) { timeHandler(event, true) } @@ -804,10 +817,26 @@ def executeHandler(event) { handleEvents([date: event.date, device: location, name: 'execute', value: event.value, jsonData: event.jsonData]) } +def getTimeLimits(){ + return hubUID ? [ + schedule: 20000, + scheduleVariance: 3000, + executionTime: 30000, + taskRemaining: 3000, + taskDelayMax: 5000, + recovery: 45000 + ] : [ + schedule: 5000, + scheduleVariance: 2000, + executionTime: 20000, + taskRemaining: 10000, + taskDelayMax: 5000 + ] +} //entry point for all events def handleEvents(event) { //cancel all pending jobs, we'll handle them later - unschedule(timeHandler) + if(hubUID) unschedule(timeHandler) if (!state.active) return def startTime = now() state.lastExecuted = startTime @@ -826,7 +855,13 @@ def handleEvents(event) { return; } checkVersion(rtData) - runIn(45.toInteger(), timeRecoveryHandler) + if(hubUID) { + runIn(rtData.timeLimits.recovery.toInteger(), timeRecoveryHandler) + } + else { + setTimeoutRecoveryHandler('timeoutRecoveryHandler_webCoRE') + } + if (rtData.semaphoreDelay) { warn "Piston waited at a semaphore for ${rtData.semaphoreDelay}ms", rtData } @@ -849,8 +884,7 @@ def handleEvents(event) { //process all time schedules in order def t = now() - while (success && (30000 + rtData.timestamp - now() > 10000)) { //allocate 30 seconds total execution time with max of 20 for schedule loop - //we only keep doing stuff if we haven't passed the 20s execution time mark + while (success && (rtData.timeLimits.executionTime + rtData.timestamp - now() > rtData.timeLimits.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... @@ -858,7 +892,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() + 3000 }] + event = [date: event.date, device: location, name: 'time', value: now(), schedule: schedules.sort{ it.t }.find{ it.t < now() + rtData.timeLimits.scheduleVariance }] } if (!event.schedule) break long threshold = now() > event.schedule.t ? now() : event.schedule.t @@ -898,7 +932,7 @@ def handleEvents(event) { def delay = event.schedule.t - now() if (syncTime && (delay > 0)) { if (rtData.logging > 2) debug "Fast executing schedules, waiting for ${delay}ms to sync up", rtData - pauseExecution delay + pause delay } success = executeEvent(rtData, event) syncTime = true @@ -909,15 +943,15 @@ 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: [ + sendLocationEvent(name: 'webCoRE', value: 'pistonExecuted', isStateChange: true, displayed: false, linkText: desc, descriptionText: desc, data: [ id: hashId(app.id), name: app.label, event: [date: rtData.currentEvent.date, delay: rtData.currentEvent.delay, duration: now() - rtData.currentEvent.date, device: "$rtData.event.device", name: rtData.currentEvent.name, value: rtData.currentEvent.value, physical: rtData.currentEvent.physical, index: rtData.currentEvent.index], state: [old: rtData.state.old, new: rtData.state.new] - ]) */ + ]) } catch (all) { } } @@ -1156,11 +1190,15 @@ private processSchedules(rtData, scheduleJob = false) { 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.toInteger(), timeHandler, [data: next]) - runIn((t+45).toInteger(), timeRecoveryHandler, [data: next]) + if(hubUID){ + runIn((t + rtData.timeLimits.recovery).toInteger(), timeRecoveryHandler, [data: next]) + } } else { rtData.stats.nextSchedule = 0 //remove the recovery - unschedule(timeRecoveryHandler) + if(hubUID){ + unschedule(timeRecoveryHandler) + } } } if (rtData.piston.o?.pep) atomicState.schedules = schedules @@ -1218,7 +1256,6 @@ 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 - //if (rtData.logging > 2) debug "Execute Statement ${statement.$}", rtData if (!rtData.fastForwardTo) { switch (statement.tep) { case 'c': @@ -1666,13 +1703,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 = 30000 + rtData.timestamp - now() + def timeLeft = rtData.timeLimits.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 3s left //keep executing until we hit 3 seconds before the total execution time limit - if (reschedule || (timeLeft - delay < 3000) || (delay >= 5000) || async) { + if (reschedule || (timeLeft - delay < rtData.timeLimits.taskRemaining) || (delay >= rtData.timeLimits.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) @@ -1680,7 +1717,7 @@ private Boolean executeTask(rtData, devices, statement, task, async) { return false } else { if (rtData.logging > 1) trace "Waiting for ${delay}ms", rtData - pauseExecution(delay) + pause(delay) } } tracePoint(rtData, "t:${task.$}", now() - t, delay) @@ -1703,7 +1740,7 @@ private long executeVirtualCommand(rtData, devices, command, params) } private executePhysicalCommand(rtData, device, command, params = [], delay = null, scheduleDevice = null, disableCommandOptimization = false) { - if(!!delay && !scheduleDevice){ + if(hubUID && (!!delay && !scheduleDevice)){ //delay without schedules is not supported in hubitat scheduleDevice = hashId(device.id) } @@ -1753,7 +1790,7 @@ private executePhysicalCommand(rtData, device, command, params = [], delay = nul 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) { //not supported + if (delay) { //not supported in hubitat device."$command"((params as Object[]) + [delay: delay]) msg.m = "Executed physical command [${device.label ?: device.name}].$command($params, [delay: $delay])" } else { @@ -1761,7 +1798,7 @@ private executePhysicalCommand(rtData, device, command, params = [], delay = nul msg.m = "Executed physical command [${device.label ?: device.name}].$command($params)" } } else { - if (delay) { //not supported + if (delay) { //not supported in hubitat device."$command"([delay: delay]) msg.m = "Executed physical command [${device.label ?: device.name}].$command([delay: $delay])" } else { @@ -1775,7 +1812,7 @@ private executePhysicalCommand(rtData, device, command, params = [], delay = nul error "Error while executing physical command $device.$command($params):", rtData, null, all } if (rtData.piston.o?.ced) { - pauseExecution(rtData.piston.o.ced) + pause(rtData.piston.o.ced) if (rtData.logging > 2) debug "Injected a ${rtData.piston.o.ced}ms delay after [$device].$command(${params ? "$params" : ''})", rtData } } @@ -1836,6 +1873,7 @@ private scheduleTimer(rtData, timer, long lastRun = 0) { //switch to local date/times + //hubitat timezone is already local time = hubUID ? time : utcToLocalTime(time) long rightNow = hubUID ? now() : utcToLocalTime(now()) lastRun = lastRun ? (hubUID ? lastRun : utcToLocalTime(lastRun)) : rightNow @@ -2389,9 +2427,12 @@ private long vcmd_setLocationMode(rtData, device, params) { private long vcmd_setAlarmSystemStatus(rtData, device, params) { def statusIdOrName = params[0] - def status = rtData.virtualDevices['alarmSystemStatus']?.ac?.find{ (it.key == statusIdOrName) || (it.value == statusIdOrName)}.collect{ [id: it.key, name: it.value] } + def dev = rtData.virtualDevices['alarmSystemStatus']; + def options = hubUID ? dev?.ac : dev?.o + options?.find{ (it.key == statusIdOrName) || (it.value == statusIdOrName)}.collect{ [id: it.key, name: it.value] } + if (status && status.size()) { - sendLocationEvent(name: 'hsmSetArm', value: status[0].id) + sendLocationEvent(name: (hubUID ? 'hsmSetArm' : 'alarmSystemStatus'), value: status[0].id) } else { error "Error setting SmartThings Home Monitor status. Status '$statusIdOrName' does not exist.", rtData } @@ -2681,7 +2722,11 @@ private long vcmd_internal_fade(Map rtData, device, String command, int startLev return duration + 100 } -private long vcmd_emulatedFlash(rtData, device, params) { +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') int cycles = cast(rtData, params[2], 'integer') @@ -2910,12 +2955,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 hubitat.device.HubAction( - "wake on lan $mac", - hubitat.device.Protocol.LAN, - null, - secureCode ? [secureCode: secureCode] : [:] - )) + + sendHubCommand(new HubAction( + "wake on lan $mac", + Protocol.LAN, + null, + secureCode ? [secureCode: secureCode] : [:] + )) return 0 } @@ -3175,7 +3221,7 @@ private long vcmd_lifxPulse(rtData, device, params) { } -public localHttpRequestHandler(hubitat.device.HubResponse hubResponse) { +public localHttpRequestHandler(hubResponse) { def responseCode = '' for (header in hubResponse.headers) { if (header.key.startsWith('http')) { @@ -3284,7 +3330,7 @@ private long vcmd_httpRequest(rtData, device, params) { query: useQueryString ? data : null, //thank you @destructure00 body: !useQueryString ? data : null //thank you @destructure00 ] - sendHubCommand(new hubitat.device.HubAction(requestParams, null, [callback: localHttpRequestHandler])) + sendHubCommand(new HubAction(requestParams, null, [callback: localHttpRequestHandler])) return 20000 } catch (all) { error "Error executing internal web request: ", rtData, null, all @@ -3363,29 +3409,39 @@ 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, + + def fuelStreamApp = parent.getFuelStreamApp() + if(fuelStreamApp){ + fuelStreamApp.updateFuelStream([ + c: canister, + n: name, s: source, - d: data, + d: data, i: rtData.instanceId - ], - requestContentType: "application/json" - ] - if (asynchttp_v1) { - asynchttp_v1.put(null, requestParams) + ]); + } + else if(!hubUID){ + 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 { - //httpPut(requestParams) { - - //} + else { + log.error "Fuel stream app is not installed. Install it to write to local fuel streams" } + return 0 } @@ -3672,12 +3728,15 @@ private evaluateOperand(rtData, node, operand, index = null, trigger = false, ne break; case 'd': //devices def deviceIds = [] - //def systemDeviceIds = getAllDeviceIds() for (d in expandDeviceList(rtData, operand.d)) { - //if (getDevice(rtData, d)) deviceIds.push(d) - //if(systemDeviceIds.any { (d == hashId(it.id)) || (d == it.label) }) { - deviceIds.push(d) - //} + if(hubUID){ + if(rtData.deviceIds.any { (d == hashId(it.id)) || (d == it.label) }) { + deviceIds.push(d) + } + } + else { + if (getDevice(rtData, d)) deviceIds.push(d) + } } /* for (d in rtData, operand.d) { @@ -4349,14 +4408,14 @@ private traverseExpressions(node, closure, param, parentNode = null) { } private getRoutineById(routineId) { - return [ id : routineId ] - /*def routines = location.helloHome?.getPhrases() + if(hubUID) return [ id : routineId ] + def routines = location.helloHome?.getPhrases() for(routine in routines) { if (routine && routine?.label && (hashId(routine.id) == routineId)) { return routine } } - return null */ + return null } private void updateDeviceList(deviceIdList) { @@ -4451,7 +4510,7 @@ private void subscribeAll(rtData) { switch (operand.v) { case 'alarmSystemStatus': subscriptionId = "$deviceId${operand.v}" - attribute = "hsmStatus" + attribute = hubUID ? "hsmStatus" : operand.v break; case 'alarmSystemAlert': subscriptionId = "$deviceId${operand.v}" @@ -4478,13 +4537,13 @@ private void subscribeAll(rtData) { def routine = getRoutineById(value.c) if (routine) { subscriptionId = "$deviceId${operand.v}${routine.id}" - attribute = "routineExecuted" + attribute = "routineExecuted${hubUID ? "" : ("." + routine.id)}" } } break case 'email': subscriptionId = "$deviceId${operand.v}${hashId(app.id)}" - attribute = "email" + attribute = "email${hubUID ? "" : ("." + hashId(app.id))}" break case 'ifttt': case 'askAlexa': @@ -4494,13 +4553,15 @@ private void subscribeAll(rtData) { def item = options ? options[value.c] : value.c if (item) { subscriptionId = "$deviceId${operand.v}${item}" - attribute = "${operand.v}" + + def attrVal = hubUID ? "" : ".${item}" + attribute = "${operand.v}${attrVal}" switch (operand.v) { case 'askAlexa': - attribute = "askAlexaMacro" + attribute = "askAlexaMacro${attrVal}" break; case 'echoSistant': - attribute = "echoSistantProfile" + attribute = "echoSistantProfile${attrVal}" break; } } @@ -4756,21 +4817,14 @@ private sanitizeVariableName(name) { name = name ? "$name".trim().replace(" ", "_") : null } -/* private getDevice(rtData, idOrName) { + if(hubUID) return getDeviceHubitat(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) { - def start = now() - //rtData.allDevices = getAllDeviceIds().collect{ getDeviceById(it.id) }.flatten().collectEntries{ dev -> [(hashId(dev.id)): dev]} - //log.debug getAllDeviceIds() - rtData.allDevices = parent.listAvailableDevices(true) - if (rtData.logging > 2) debug "Grabbed parent devices in ${now() - start}ms", rtData - } - + if (!rtData.allDevices) rtData.allDevices = parent.listAvailableDevices(true) if (rtData.allDevices) { - def deviceMap = rtData.allDevices.find{ (idOrName == it.key) || (idOrName == it.value.getDisplayName()) } + def deviceMap = rtData.allDevices.find{ (idOrName == it.key) || (idOrName == it.value.getDisplayName()) } if (deviceMap) { rtData.updateDevices = true rtData.devices[deviceMap.key] = deviceMap.value @@ -4781,10 +4835,10 @@ private getDevice(rtData, idOrName) { } } return device -}*/ - -private getDevice(rtData, idOrName) { - def start = now() +} +//parent.listAvailableDevices(true) adds several hundred milliseconds. Get devs only as needed by deviceid +private getDeviceHubitat(rtData, idOrName) { + //def start = now() if (rtData.locationId == idOrName) return location def device = rtData.devices[idOrName] ?: rtData.devices.find{ it.value.getDisplayName() == idOrName }?.value if (!device) { @@ -4794,7 +4848,7 @@ private getDevice(rtData, idOrName) { def deviceMap = rtData.allDevices.find{ (idOrName == it.key) || (idOrName == it.value.getDisplayName()) } if(!deviceMap){ - def minDev = getAllDeviceIds().find { (idOrName == hashId(it.id)) || (idOrName == it.label) } + def minDev = rtData.deviceIds.find { (idOrName == hashId(it.id)) || (idOrName == it.label) } if(minDev){ rtData.allDevices[hashId(minDev.id)] = getDeviceById(minDev.id) deviceMap = rtData.allDevices.find { it.key == hashId(minDev.id) } @@ -4805,7 +4859,10 @@ private getDevice(rtData, idOrName) { rtData.updateDevices = true rtData.devices[deviceMap.key] = deviceMap.value device = deviceMap.value - } + } + else { + error "Device ${idOrName} was not found. Please review your piston.", rtData + } } //if (rtData.logging > 2) debug "Device grabbed in ${now() - start}ms", rtData return device @@ -4844,8 +4901,8 @@ 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 = location.hsmStatus ?: rtData.hsmStatus + case 'alarmSystemStatus': + def v = hubUID ? (location.hsmStatus ?: rtData.hsmStatus) : location.currentState("alarmSystemStatus")?.value def n = rtData.virtualDevices['alarmSystemStatus']?.o[v] return [t: 'string', v: v, n: n] } @@ -7932,7 +7989,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], @@ -8016,7 +8073,7 @@ private static Map getSystemVariables() { "\$iftttStatusCode": [t: "integer", v: null], "\$iftttStatusOk": [t: "boolean", v: null], "\$locationMode": [t: "string", d: true], - "\$hsmStatus": [t: "string", d: true], + "\$${hubUID ? "hsmStatus" : "shmStatus"}": [t: "string", d: true], "\$version": [t: "string", d: true] ].sort{it.key} } @@ -8069,8 +8126,9 @@ private getSystemVariableValue(rtData, name) { 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 "\$hsmStatus": switch (location.hsmStatus ?: rtData.hsmStatus) { case 'allDisarmed' : return 'All Disarmed'; case 'disarmed': return 'Disarmed'; case 'armedHome': return 'Armed/Home'; case 'armedAway': return 'Armed/Away'; }; return null; - case "\$hsmStatus": return location.hsmStatus ?: rtData.hsmStatus + case "\$${hubUID ? "hsmStatus" : "shmStatus"}": + if(hubUID) { return location.hsmStatus ?: rtData.hsmStatus } + else switch (location.currentState("alarmSystemStatus")?.value) { case 'off': return 'Disarmed'; case 'stay': return 'Armed/Stay'; case 'away': return 'Armed/Away'; }; return null; } } diff --git a/smartapps/ady624/webcore-storage.src/webcore-storage.groovy b/smartapps/ady624/webcore-storage.src/webcore-storage.groovy index 54865e5b..d1c04e69 100644 --- a/smartapps/ady624/webcore-storage.src/webcore-storage.groovy +++ b/smartapps/ady624/webcore-storage.src/webcore-storage.groovy @@ -144,7 +144,7 @@ def Map listAvailableDevices(raw = false) { private def transformCommand(command, overrides){ def override = overrides[command.getName()] if(override && override.s == command.getArguments()?.toString()){ - return override.r; + return override.r } return command.getName() } @@ -167,10 +167,11 @@ 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 [ - push : [c: "push", s: null , r: "pushMomentary"] - ] + return (hubUID ? [ + push : [c: "push", s: null , r: "pushMomentary"] //s: command signature + ] : [:]) } /******************************************************************************/ diff --git a/smartapps/ady624/webcore.src/webcore.groovy b/smartapps/ady624/webcore.src/webcore.groovy index fe75b511..1c940c17 100644 --- a/smartapps/ady624/webcore.src/webcore.groovy +++ b/smartapps/ady624/webcore.src/webcore.groovy @@ -288,7 +288,7 @@ public static String version() { return "v0.3.107.20180806" } /******************************************************************************/ private static String handle() { return "webCoRE" } private static String domain() { return "webcore.co" } -//include 'asynchttp_v1' +if(!hubUID) include 'asynchttp_v1' definition( name: "${handle()}", namespace: "ady624", @@ -391,9 +391,9 @@ def pageMain() { input "customEndpoints", "bool", submitOnChange: true, title: "Use custom endpoints?", default: false, required: true if(customEndpoints){ - input "customHubUrl", "string", title: "Custom hub url different from https://cloud.hubitat.com", default: null, required: false + if(hubUID) input "customHubUrl", "string", title: "Custom hub url different from ${hubUID ? "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 - paragraph "If you enter a custom url above you will have to use a different webcore instance from dashboard.webcore.co as they restrict their api to hubitat and smartthing's cloud" + 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" } } } @@ -577,13 +577,20 @@ 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: hubUID ? '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" } } + + def fuelStreamApp = getFuelStreamApp() + if(fuelStreamApp){ + section("Local fuel streams"){ + app([title: hubUID ? 'Do not click' : 'Fuel Streams', multiple: false, install: true, uninstall: false], 'fuelStreams', 'ady624', "${handle()} Fuel Streams") + } + } /* section("Integrations") { href "pageIntegrations", title: "Integrations with other services", description: "Tap here to configure your integrations" }*/ @@ -605,6 +612,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: hubUID ? false : true, required: false } section(title: "Recovery") { @@ -791,14 +799,6 @@ def installed() { } def updated() { - if(state.accessToken){ - if(customEndpoints && (customHubUrl ?: "") != ""){ - state.endpoint = customServerUrl("?access_token=${state.accessToken}") - } - else { - state.endpoint = hubUID ? apiServerUrl("$hubUID/apps/${app.id}/?access_token=${state.accessToken}") : apiServerUrl("/api/token/${accessToken}/smartapps/installations/${app.id}/") - } - } warn "Updating webCoRE ${version()}" unsubscribe() unschedule() @@ -833,20 +833,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 = hubUID ? 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) { - if(customEndpoints && (customHubUrl ?: "") != ""){ - state.endpoint = customServerUrl("?access_token=${state.accessToken}") - } - else { - 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 @@ -870,7 +877,7 @@ private subscribeAll() { subscribe(location, "echoSistant", echoSistantHandler) subscribe(location, "HubUpdated", hubUpdatedHandler, [filterEvents: false]) subscribe(location, "summary", summaryHandler, [filterEvents: false]) - subscribe(location, "hsmStatus", hsmHandler, [filterEvents: false]) + if(hubUID) subscribe(location, "hsmStatus", hsmHandler, [filterEvents: false]) setPowerSource(getHub()?.isBatteryInUse() ? 'battery' : 'mains') } @@ -906,6 +913,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"]} @@ -924,7 +933,7 @@ private api_get_error_result(error) { ] } -private getFirmwareVersion(){ +private getHubitatVersion(){ try{ return location.getHubs().collectEntries {[it.id, it.getFirmwareVersionString()]} } @@ -939,12 +948,15 @@ private api_get_base_result(deviceVersion = 0, updateCache = false) { 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), + id: instanceId, locationId: hashId(location.id, updateCache), name: app.label ?: app.name, uri: state.endpoint, @@ -955,15 +967,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: getFirmwareVersion()[it.id], physical: it.getType().toString().contains('PHYSICAL'), powerSource: it.isBatteryInUse() ? 'battery' : 'mains' ]}, + hubs: location.getHubs().collect{ [id: hashId(it.id, updateCache), name: it.name, firmware: hubUID ? getHubitatVersion()[it.id] : 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), mode: hashId(location.getCurrentMode().id, updateCache), modes: location.getModes().collect{ [id: hashId(it.id, updateCache), name: it.name ]}, - shm: transformHsmStatus(state.hsmStatus), + shm: hubUID ? transformHsmStatus(state.hsmStatus) : location.currentState("alarmSystemStatus")?.value, name: location.name, temperatureScale: location.getTemperatureScale(), timeZone: tz ? [ @@ -977,6 +990,28 @@ private api_get_base_result(deviceVersion = 0, updateCache = false) { ] } +private getFuelStreamUrls(iid){ + if(!hubUID){ + 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("/") : + hubUID ? 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 String transformHsmStatus(status){ switch(status){ case "disarmed": @@ -999,6 +1034,8 @@ private api_intf_dashboard_load() { recoveryHandler() //install storage app def storageApp = getStorageApp(true) + //install fuel stream app + getFuelStreamApp(true) //debug "Dashboard: Request received to initialize instance" if (verifySecurityToken(params.token)) { result = api_get_base_result(params.dev, true) @@ -1052,7 +1089,7 @@ private api_intf_dashboard_piston_create() { def result debug "Dashboard: Request received to generate a new piston name" if (verifySecurityToken(params.token)) { - def piston = addChildApp("ady624", "${handle()} Piston", params.name?:generatePistonName(), [:]) + def piston = addChildApp("ady624", "${handle()} Piston", params.name?:generatePistonName()) if (params.author || params.bin) { piston.config([bin: params.bin, author: params.author, initialVersion: version()]) } @@ -1104,14 +1141,14 @@ private api_intf_dashboard_piston_get() { result.now = now() def jsonData = groovy.json.JsonOutput.toJson(result) - if(!customEndpoints || (customHubUrl ?: "") == ""){ + if(hubUID && (!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 = [] + 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) @@ -1533,6 +1570,43 @@ private api_intf_variable_set() { render contentType: "application/javascript;charset=utf-8", data: "${params.callback}(${groovy.json.JsonOutput.toJson(result)})" } +private api_intf_fuelstreams_list() { + def result = [] + debug "Fuel Streams: Request to list fuel streams" + + def fuelStreamApp = getFuelStreamApp() + + if(fuelStreamApp){ + result = fuelStreamApp.listFuelStreams().values().collect { + it.c = it.c ?: "" + it + } + } + else { + debug "Fuel stream app not installed. Install for local fuel streams" + } + + render contentType: "application/javascript;charset=utf-8", data: "${params.callback}(${groovy.json.JsonOutput.toJson(["fuelStreams" : result])})" +} + +private api_intf_fuelstreams_get() { + def result = [] + debug "Fuel Streams: Request to list fuel stream data" + + def id = params.id + + def fuelStreamApp = getFuelStreamApp() + + if(fuelStreamApp){ + result = fuelStreamApp.listFuelStreamData(id) + } + else { + debug "Fuel stream app not installed. Install for local fuel streams" + } + + 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" @@ -1583,7 +1657,7 @@ private api_intf_dashboard_piston_activity() { def api_ifttt() { def data = [:] - def remoteAddr = "UNKNOWN" /*request.getHeader("X-FORWARDED-FOR") ?: request.getRemoteAddr() */ + def remoteAddr = hubUID ? "UNKNOWN" : request.getHeader("X-FORWARDED-FOR") ?: request.getRemoteAddr() if (params) { data.params = [:] for(param in params) { @@ -1615,7 +1689,7 @@ def api_email() { private api_execute() { def result = [:] def data = [:] - def remoteAddr = "UNKOWN" /*request.getHeader("X-FORWARDED-FOR") ?: request.getRemoteAddr()*/ + def remoteAddr = hubUID ? "UNKNOWN" : request.getHeader("X-FORWARDED-FOR") ?: request.getRemoteAddr() debug "Dashboard: Request received to execute a piston from IP $remoteAddr" if (params) { data = [:] @@ -1700,15 +1774,16 @@ private cleanUp() { private getStorageApp(install = false) { 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 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 @@ -1726,6 +1801,26 @@ private getStorageApp(install = false) { return storageApp } +public getFuelStreamApp(install = false){ + def name = handle() + ' Fuel Streams' + def fuelStreamApp = getChildApps().find{ it.name == name } + def label = "${app.label} Fuel Streams" + if(fuelStreamApp){ + if (label != fuelStreamApp.label) { + fuelStreamApp.updateLabel(label) + } + return fuelStreamApp + } + if (!install) return null + try { + fuelStreamApp = addChildApp("ady624", name, label) + } catch (all) { + if(hubUID) error "Please install the webCoRE Fuel Streams app for local Fuel Streams" + return null + } + return fuelStreamApp +} + private getDashboardApp(install = false) { def name = handle() + ' Dashboard' def label = app.label + ' (dashboard)' @@ -1749,14 +1844,18 @@ def customServerUrl(path){ if(!path.startsWith("/")){ path = "/" + path } - return customHubUrl + "/apps/api/" + app.id + 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 - if(customEndpoints && (customHubUrl ?: "") != ""){ + if(isCustomEndpoint()){ return url + (register ? "register/" : "init/") + ( customServerUrl('/?access_token=' + state.accessToken) ).bytes.encodeBase64() @@ -1800,7 +1899,7 @@ public Map listAvailableDevices(raw = false, updateCache = false) { private def transformCommand(command, overrides){ def override = overrides[command.getName()] if(override && override.s == command.getArguments()?.toString()){ - return override.r; + return override.r } return command.getName() } @@ -1950,7 +2049,7 @@ private testLifx() { requestContentType: "application/json" ] if (asynchttp_v1) asynchttp_v1.get(lifxHandler, requestParams, [request: 'scenes']) - pauseExecution(250) + pause(250) requestParams.path = "/v1/lights/all" if (asynchttp_v1) asynchttp_v1.get(lifxHandler, requestParams, [request: 'lights']) return true @@ -1991,8 +2090,8 @@ private registerInstance() { asynchttp_v1.put(instanceRegistrationHandler, params) } else { - //params << [contentType: 'text/plain'] - //httpPut(params) { res -> } + params << [contentType: 'application/json', requestContentType: 'application/json'] + httpPut(params) { res -> } } } @@ -2074,7 +2173,7 @@ public Map getRunTimeData(semaphore = null, fetchWrappers = false) { break } waited = true - pauseExecution(250) + pause(250) } } def storageApp = !!fetchWrappers ? getStorageApp() : null @@ -2098,7 +2197,6 @@ public Map getRunTimeData(semaphore = null, fetchWrappers = false) { globalStore: state.store ?: [:], settings: state.settings ?: [:], lifx: state.lifx ?: [:], - hsmStatus: state.hsmStatus, powerSource: state.powerSource ?: 'mains', region: state.endpoint.contains('graph-eu') ? 'eu' : 'us', instanceId: hashId(app.id), @@ -2106,8 +2204,12 @@ public Map getRunTimeData(semaphore = null, fetchWrappers = false) { started: startTime, ended: now(), generatedIn: now() - startTime, - redirectContactBook: settings.redirectContactBook - ] + redirectContactBook: settings.redirectContactBook, + logPistonExecutions: settings.logPistonExecutions + ] + (hubUID ? [ + hsmStatus: state.hsmStatus, + deviceIds: getAllDeviceIds() + ] : [:]) } public void updateRunTimeData(data) { @@ -2223,7 +2325,7 @@ def webCoREHandler(event) { switch (event.value) { case 'poll': int delay = (int) Math.round(2000 * Math.random()) - pauseExecution(delay) + pause(delay) broadcastPistonList() break; /* case 'ping': @@ -2430,19 +2532,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 @@ -2450,14 +2548,15 @@ 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"], ], audioNotification : [ n: "Audio Notification", d: "audio notification devices", c: ["playText", "playTextAndResume", "playTextAndRestore", "playTrack", "playTrackAndResume", "playTrackAndRestore"], ], battery : [ n: "Battery", d: "battery powered devices", a: "battery", ], beacon : [ n: "Beacon", d: "beacons", a: "presence", ], - bulb : [ n: "Bulb", d: "bulbs", a: "switch", c: ["off", "on"], ], + bulb : [ n: "Bulb", d: "bulbs", a: "switch", c: ["off", "on"], ], + button : [ n: "Button", d: "buttons", a: "button", m: true, s: "numberOfButtons,numButtons", i: "buttonNumber", ], carbonDioxideMeasurement : [ n: "Carbon Dioxide Measurement", d: "carbon dioxide sensors", a: "carbonDioxide", ], carbonMonoxideDetector : [ n: "Carbon Monoxide Detector", d: "carbon monoxide detectors", a: "carbonMonoxide", ], colorControl : [ n: "Color Control", d: "adjustable color lights", a: "color", c: ["setColor", "setHue", "setSaturation"], ], @@ -2466,11 +2565,10 @@ private static Map capabilities() { consumable : [ n: "Consumable", d: "consumables", a: "consumableStatus", c: ["setConsumableStatus"], ], contactSensor : [ n: "Contact Sensor", d: "contact sensors", a: "contact", ], doorControl : [ n: "Door Control", d: "automatic doors", a: "door", c: ["close", "open"], ], - doubleTapableButton : [ n: "Double Tapable Button", d: "double tapable buttons", a: "doubleTapped", c: ["doubleTap"], ], energyMeter : [ n: "Energy Meter", d: "energy meters", a: "energy", ], estimatedTimeOfArrival : [ n: "Estimated Time of Arrival", d: "moving devices (ETA)", a: "eta", ], garageDoorControl : [ n: "Garage Door Control", d: "automatic garage doors", a: "door", c: ["close", "open"], ], - holdableButton : [ n: "Holdable Button", d: "holdable buttons", a: "held", c: ["hold"] ], + holdableButton : [ n: "Holdable Button", d: "holdable buttons", a: "button", m: true, s: "numberOfButtons,numButtons", i: "buttonNumber", ], illuminanceMeasurement : [ n: "Illuminance Measurement", d: "illuminance sensors", a: "illuminance", ], imageCapture : [ n: "Image Capture", d: "cameras, imaging devices", a: "image", c: ["take"], ], indicator : [ n: "Indicator", d: "indicator devices", a: "indicatorStatus", c: ["indicatorNever", "indicatorWhenOn", "indicatorWhenOff"], ], @@ -2479,7 +2577,7 @@ private static Map capabilities() { lock : [ n: "Lock", d: "electronic locks", a: "lock", c: ["lock", "unlock"], s:"numberOfCodes,numCodes", i: "usedCode", ], lockOnly : [ n: "Lock Only", d: "electronic locks (lock only)", a: "lock", c: ["lock"], ], mediaController : [ n: "Media Controller", d: "media controllers", a: "currentActivity", c: ["startActivity", "getAllActivities", "getCurrentActivity"], ], - momentary : [ n: "Momentary", d: "momentary switches", c: ["pushMomentary"], ], + momentary : [ n: "Momentary", d: "momentary switches", c: ["push"], ], motionSensor : [ n: "Motion Sensor", d: "motion sensors", a: "motion", ], musicPlayer : [ n: "Music Player", d: "music players", a: "status", c: ["mute", "nextTrack", "pause", "play", "playTrack", "previousTrack", "restoreTrack", "resumeTrack", "setLevel", "setTrack", "stop", "unmute"], ], notification : [ n: "Notification", d: "notification devices", c: ["deviceNotification"], ], @@ -2489,7 +2587,6 @@ private static Map capabilities() { powerMeter : [ n: "Power Meter", d: "power meters", a: "power", ], powerSource : [ n: "Power Source", d: "multisource powered devices", a: "powerSource", ], presenceSensor : [ n: "Presence Sensor", d: "presence sensors", a: "presence", ], - pushableButton : [ n: "Pushable Button", d: "pushable buttons", a: "pushed", c: ["push"], ], refresh : [ n: "Refresh", d: "refreshable devices", c: ["refresh"], ], relativeHumidityMeasurement : [ n: "Relative Humidity Measurement", d: "humidity sensors", a: "humidity", ], relaySwitch : [ n: "Relay Switch", d: "relay switches", a: "switch", c: ["off", "on"], ], @@ -2522,19 +2619,32 @@ 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"], ], - ] + windowShade : [ n: "Window Shade", d: "automatic window shades", a: "windowShade", c: ["close", "open", "presetPosition"], ] + ] + (hubUID ? [ + 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(hubUID){ + capabilities.remove('button') + } + + return capabilities } -private static Map attributes() { - return [ +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"], ], axisX : [ n: "X axis", t: "integer", r: [-1024, 1024], s: "threeAxis", ], axisY : [ n: "Y axis", t: "integer", r: [-1024, 1024], s: "threeAxis", ], axisZ : [ n: "Z axis", t: "integer", r: [-1024, 1024], s: "threeAxis", ], - battery : [ n: "battery", t: "integer", r: [0, 100], u: "%", ], + battery : [ n: "battery", t: "integer", r: [0, 100], u: "%", ], + button : [ n: "button", t: "enum", o: ["pushed", "held"], c: "button", m: true, s: "numberOfButtons,numButtons", i: "buttonNumber" ], carbonDioxide : [ n: "carbon dioxide", t: "decimal", r: [0, null], ], carbonMonoxide : [ n: "carbon monoxide", t: "enum", o: ["clear", "detected", "tested"], ], color : [ n: "color", t: "color", ], @@ -2544,13 +2654,12 @@ private static Map attributes() { coolingSetpoint : [ n: "cooling setpoint", t: "decimal", r: [-127, 127], u: '°?', ], currentActivity : [ n: "current activity", t: "string", ], door : [ n: "door", t: "enum", o: ["closed", "closing", "open", "opening", "unknown"], p: true, ], - doubleTapped : [ n: "double tapped button", t: "integer", c: "doubleTapableButton" ], energy : [ n: "energy", t: "decimal", r: [0, null], u: "kWh", ], eta : [ n: "ETA", t: "datetime", ], goal : [ n: "goal", t: "integer", r: [0, null], ], heatingSetpoint : [ n: "heating setpoint", t: "decimal", r: [-127, 127], u: '°?', ], - held : [ n: "held button", t: "integer", c: "holdableButton" ], - hex : [ n: "hexadecimal code", t: "hexcolor", ], + hex : [ n: "hexadecimal code", t: "hexcolor", ], + holdableButton : [ n: "holdable button", t: "enum", o: ["held", "pushed"], c: "holdableButton", m: true, ], hue : [ n: "hue", t: "integer", r: [0, 360], u: "°", ], humidity : [ n: "relative humidity", t: "integer", r: [0, 100], u: "%", ], illuminance : [ n: "illuminance", t: "integer", r: [0, null], u: "lux", ], @@ -2571,7 +2680,6 @@ private static Map attributes() { power : [ n: "power", t: "decimal", u: "W", ], powerSource : [ n: "power source", t: "enum", o: ["battery", "dc", "mains", "unknown"], ], presence : [ n: "presence", t: "enum", o: ["not present", "present"], ], - pushed : [ n: "pushed button", t: "integer", c: "pushableButton" ], rssi : [ n: "signal strength", t: "integer", r: [0, 100], u: "%", ], saturation : [ n: "saturation", t: "integer", r: [0, 100], u: "%", ], schedule : [ n: "schedule", t: "object", ], @@ -2623,16 +2731,28 @@ 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: "°", ], - ] + ] + (hubUID ? [ + 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(hubUID){ + attrs.remove('button') + attrs.remove('holdableButton') + } + + return attrs } +/* Push command has multiple overloads in hubitat */ public Map commandOverrides(){ - return [ - push : [c: "push", s: null , r: "pushMomentary"] - ] + return (hubUID ? [ + push : [c: "push", s: null , r: "pushMomentary"] //s: command signature + ] : [:]) } -private static Map commands() { +private Map commands() { return [ auto : [ n: "Set to Auto", a: "thermostatMode", v: "auto", ], beep : [ n: "Beep", ], @@ -2642,16 +2762,13 @@ private static Map commands() { configure : [ n: "Configure", i: 'cog', ], cool : [ n: "Set to Cool", i: 'snowflake', is: 'l', a: "thermostatMode", v: "cool", ], deviceNotification : [ n: "Send device notification...", d: "Send device notification \"{0}\"", p: [[n:"Message",t:"string"]], ], - doubleTap : [ n: "Double Tap", d: "Double tap button {0}", a: "doubleTapped", p:[[n: "Button #", t: "integer"]] ], emergencyHeat : [ n: "Set to Emergency Heat", a: "thermostatMode", v: "emergency heat", ], fanAuto : [ n: "Set fan to Auto", a: "thermostatFanMode", v: "auto", ], fanCirculate : [ n: "Set fan to Circulate", a: "thermostatFanMode", v: "circulate", ], fanOn : [ n: "Set fan to On", a: "thermostatFanMode", v: "on", ], - flash : [ n: "Flash", ], getAllActivities : [ n: "Get all activities", ], getCurrentActivity : [ n: "Get current activity", ], heat : [ n: "Set to Heat", i: 'fire', a: "thermostatMode", v: "heat", ], - hold : [ n: "Hold", d: "Hold Button {0}", a: "held", p: [[n:"Button #", t: "integer"]] ], indicatorNever : [ n: "Disable indicator", ], indicatorWhenOff : [ n: "Enable indicator when off", ], indicatorWhenOn : [ n: "Enable indicator when on", ], @@ -2672,8 +2789,7 @@ private static Map commands() { poll : [ n: "Poll", i: 'question', ], presetPosition : [ n: "Move to preset position", a: "windowShade", v: "partially open", ], previousTrack : [ n: "Previous track", ], - push : [ n: "Push", d: "Push button {0}", a: "pushed", p:[[n: "Button #", t: "integer"]] ], - pushMomentary : [ n: "Push" ], + push : [ n: "Push", ], refresh : [ n: "Refresh", i: 'sync', ], restoreTrack : [ n: "Restore track...", d: "Restore track {0}", p: [[n:"Track URL",t:"url"]], ], resumeTrack : [ n: "Resume track...", d: "Resume track {0}", p: [[n:"Track URL",t:"url"]], ], @@ -2754,10 +2870,16 @@ private static Map commands() { low : [ n: "Set to Low", ], med : [ n: "Set to Medium", ], high : [ n: "Set to High", ], - ] + ] + (hubUID ? [ + doubleTap : [ n: "Double Tap", d: "Double tap button {0}", a: "doubleTapped", p:[[n: "Button #", t: "integer"]] ], + flash : [ 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 @@ -2793,7 +2915,7 @@ private static Map virtualCommands() { setTile : [ n: "Set piston tile...", a: true, i: "info-square", is:"l", d: "Set piston tile #{0} title to \"{1}\", text to \"{2}\", footer to \"{3}\", and colors to {4} over {5}{6}", p: [[n:"Tile Index",t:"enum",o:tileIndexes],[n:"Title",t:"string"],[n:"Text",t:"string"],[n:"Footer",t:"string"],[n:"Text Color",t:"color"],[n:"Background Color",t:"color"],[n:"Flash mode",t:"boolean",d:" (flashing)"]], ], clearTile : [ n: "Clear piston tile...", a: true, i: "info-square", is:"l", d: "Clear piston tile #{0}", p: [[n:"Tile Index",t:"enum",o:tileIndexes]], ], setLocationMode : [ n: "Set location mode...", a: true, i: "", d: "Set location mode to {0}", p: [[n:"Mode",t:"mode"]], ], - 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]}]], ], + setAlarmSystemStatus : [ n: "Set Smart Home Monitor status...", a: true, i: "", d: "Set Smart Home Monitor status to {0}", p: [[n:"Status", t:"alarmSystemStatus"]], ], sendEmail : [ n: "Send email...", a: true, i: "envelope", d: "Send email with subject \"{1}\" to {0}", p: [[n:"Recipient",t:"email"],[n:"Subject",t:"string"],[n:"Message body",t:"string"]], ], wolRequest : [ n: "Wake a LAN device", a: true, i: "", d: "Wake LAN device at address {0}{1}", p: [[n:"MAC address",t:"string"],[n:"Secure code",t:"string",d:" with secure code {v}"]], ], adjustLevel : [ n: "Adjust level...", r: ["setLevel"], i: "toggle-on", d: "Adjust level by {0}%{1}", p: [[n:"Adjustment",t:"integer",r:[-100,100]], [n:"Only if switch is...", t:"enum",o:["on","off"], d:" if already {v}"]], ], @@ -2806,7 +2928,7 @@ private static Map virtualCommands() { fadeSaturation : [ n: "Fade saturation...", r: ["setSaturation"], i: "toggle-on", d: "Fade saturation{0} to {1}% in {2}{3}", p: [[n:"Starting saturation",t:"level",d:" from {v}%"],[n:"Final saturation",t:"level"],[n:"Duration",t:"duration"], [n:"Only if switch is...", t:"enum",o:["on","off"], d:" if already {v}"]], ], fadeHue : [ n: "Fade hue...", r: ["setHue"], i: "toggle-on", d: "Fade hue{0} to {1}° in {2}{3}", p: [[n:"Starting hue",t:"hue",d:" from {v}°"],[n:"Final hue",t:"hue"],[n:"Duration",t:"duration"], [n:"Only if switch is...", t:"enum",o:["on","off"], d:" if already {v}"]], ], fadeColorTemperature : [ n: "Fade color temperature...", r: ["setColorTemperature"], i: "toggle-on", d: "Fade color temperature{0} to {1}°K in {2}{3}", p: [[n:"Starting color temperature",t:"colorTemperature",d:" from {v}°K"],[n:"Final color temperature",t:"colorTemperature"],[n:"Duration",t:"duration"], [n:"Only if switch is...", t:"enum",o:["on","off"], d:" if already {v}"]], ], - emulatedFlash : [ 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}"]], ], + 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}"]], ], flashLevel : [ n: "Flash (level)...", r: ["setLevel"], i: "toggle-on", d: "Flash {0}% {1} / {2}% {3} for {4} times{5}", p: [[n:"Level 1", t:"level"],[n:"Duration 1",t:"duration"],[n:"Level 2", t:"level"],[n:"Duration 2",t:"duration"],[n:"Number of flashes",t:"integer"], [n:"Only if switch is...", t:"enum",o:["on","off"], d:" if already {v}"]], ], flashColor : [ n: "Flash (color)...", r: ["setColor"], i: "toggle-on", d: "Flash {0} {1} / {2} {3} for {4} times{5}", p: [[n:"Color 1", t:"color"],[n:"Duration 1",t:"duration"],[n:"Color 2", t:"color"],[n:"Duration 2",t:"duration"],[n:"Number of flashes",t:"integer"], [n:"Only if switch is...", t:"enum",o:["on","off"], d:" if already {v}"]], ], iftttMaker : [ n: "Send an IFTTT Maker event...", a: true, d: "Send the {0} IFTTT Maker event{1}{2}{3}", p: [[n:"Event", t:"text"], [n:"Value 1", t:"string", d:", passing value1 = '{v}'"], [n:"Value 2", t:"string", d:", passing value2 = '{v}'"], [n:"Value 3", t:"string", d:", passing value3 = '{v}'"]], ], @@ -2845,7 +2967,11 @@ private static Map virtualCommands() { ] : [:]) + (getLifxToken() ? [ lifxScene: [n: "Activate LIFX scene", p: ["Scene:lifxScenes"], l: true, dd: "Activate LIFX Scene '{0}'", aggregated: true], - ] : [:])*/ + ] : [:])*/ + + (hubUID ? [ + 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]}]], ], + flash : [ n: "Emulated 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}"]], ] + ] : [:]) } @@ -3067,12 +3193,20 @@ private static Map getAlarmSystemStatusActions() { } private static Map getAlarmSystemStatusOptions() { - return [ + 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" - ] + allDisarmed: "All Disarmed" + ] } private static Map getAlarmSystemAlertOptions() { @@ -3123,12 +3257,14 @@ 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: 'Hubitat Safety Monitor status',t: 'enum', o: getAlarmSystemStatusOptions(), ac: getAlarmSystemStatusActions(), x: true], + alarmSystemStatus: [ n: 'Smart Home Monitor status', t: 'enum', o: getAlarmSystemStatusOptions(), x: true] + ] + (hubUID ? [ + alarmSystemStatus: [ n: 'Hubitat Safety Monitor status',t: 'enum', o: getHubitatAlarmSystemStatusOptions(), ac: getAlarmSystemStatusActions(), x: true], //this one can be confusing to users so it's been commented out. It can subscribe to hsmSetArm, but the safety monitor doesn't actually send these events themselves, only other apps //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] - ] + alarmSystemRule: [ n: 'Hubitat Safety Monitor rule',t: 'enum', o: getAlarmSystemRuleOptions(), m: true] + ] : [:]) } public Map getColorByName(name){ return getColors().find{ it.name == name } From df2eec6f10d4f41f6bcbd80a37233d0593f46265 Mon Sep 17 00:00:00 2001 From: jp0550 Date: Mon, 13 Aug 2018 01:32:19 -0500 Subject: [PATCH 30/55] Use app per fuelstream --- .../webcore-fuelstream.groovy | 99 +++++++++++++++++ .../webcore-piston.src/webcore-piston.groovy | 21 ++-- smartapps/ady624/webcore.src/webcore.groovy | 100 ++++++++---------- 3 files changed, 156 insertions(+), 64 deletions(-) create mode 100644 smartapps/ady624/webcore-fuelstream.src/webcore-fuelstream.groovy diff --git a/smartapps/ady624/webcore-fuelstream.src/webcore-fuelstream.groovy b/smartapps/ady624/webcore-fuelstream.src/webcore-fuelstream.groovy new file mode 100644 index 00000000..fb760395 --- /dev/null +++ b/smartapps/ady624/webcore-fuelstream.src/webcore-fuelstream.groovy @@ -0,0 +1,99 @@ +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(!state.fuelStreamData){ + state.fuelStreamData = [] + } + + cleanFuelStreams() +} + +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 = state.fuelStreamData.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 = state.fuelStreamData.sort { it.i }.take(pointsToRemove) + state.fuelStreamData.each { + it.removeAll(toBeRemoved) + } + } +} + +def updateFuelStream(req){ + def canister = req.c ?: "" + def name = req.n + def data = req.d + def instance = req.i + def source = req.s + + state.fuelStreamData.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(){ + state.fuelStreamData.collect{ it << [t: getFormattedDate(new Date(it.i))]} +} \ 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 197748af..7aa7a24f 100644 --- a/smartapps/ady624/webcore-piston.src/webcore-piston.groovy +++ b/smartapps/ady624/webcore-piston.src/webcore-piston.groovy @@ -3409,16 +3409,17 @@ private long vcmd_writeToFuelStream(rtData, device, params) { def name = params[1] def data = params[2] def source = params[3] + + def req = [ + c: canister, + n: name, + s: source, + d: data, + i: rtData.instanceId + ] - def fuelStreamApp = parent.getFuelStreamApp() - if(fuelStreamApp){ - fuelStreamApp.updateFuelStream([ - c: canister, - n: name, - s: source, - d: data, - i: rtData.instanceId - ]); + if(rtData.useLocalFuelStreams){ + parent.writeToFuelStream(req) } else if(!hubUID){ def requestParams = [ @@ -3440,7 +3441,7 @@ private long vcmd_writeToFuelStream(rtData, device, params) { } else { log.error "Fuel stream app is not installed. Install it to write to local fuel streams" - } + } return 0 } diff --git a/smartapps/ady624/webcore.src/webcore.groovy b/smartapps/ady624/webcore.src/webcore.groovy index 1c940c17..02546e08 100644 --- a/smartapps/ady624/webcore.src/webcore.groovy +++ b/smartapps/ady624/webcore.src/webcore.groovy @@ -311,6 +311,7 @@ preferences { page(name: "pageInitializeDashboard") page(name: "pageFinishInstall") page(name: "pageSelectDevices") + page(name: "pageFuelStreams") page(name: "pageSettings") page(name: "pageChangePassword") page(name: "pageSavePassword") @@ -585,12 +586,13 @@ def pageSettings() { } } - def fuelStreamApp = getFuelStreamApp() - if(fuelStreamApp){ - section("Local fuel streams"){ - app([title: hubUID ? 'Do not click' : 'Fuel Streams', multiple: false, install: true, uninstall: false], 'fuelStreams', 'ady624', "${handle()} Fuel Streams") - } + section("Fuel Streams"){ + input "localFuelStreams", "bool", title: "Use local fuel streams?", defaultValue: hubUID ? 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" }*/ @@ -627,6 +629,14 @@ def pageSettings() { } } +private pageFuelStreams(){ + dynamicPage(name: "pageFuelStreams", title: "", uninstall: false, install: false){ + section(){ + app([title: hubUID ? '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() { @@ -991,7 +1001,7 @@ private api_get_base_result(deviceVersion = 0, updateCache = false) { } private getFuelStreamUrls(iid){ - if(!hubUID){ + if(!settings.localFuelStreams){ 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 ] @@ -1005,6 +1015,7 @@ private getFuelStreamUrls(iid){ def baseUrl = isCustomEndpoint() ? customServerUrl("/") : hubUID ? 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}"], @@ -1034,8 +1045,6 @@ private api_intf_dashboard_load() { recoveryHandler() //install storage app def storageApp = getStorageApp(true) - //install fuel stream app - getFuelStreamApp(true) //debug "Dashboard: Request received to initialize instance" if (verifySecurityToken(params.token)) { result = api_get_base_result(params.dev, true) @@ -1494,7 +1503,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.id); + app.deleteChildApp(hubUID ? piston.id : piston) result = [status: "ST_SUCCESS"] state.remove(params.id) state.remove('sph${params.id}') @@ -1570,39 +1579,41 @@ private api_intf_variable_set() { render contentType: "application/javascript;charset=utf-8", data: "${params.callback}(${groovy.json.JsonOutput.toJson(result)})" } -private api_intf_fuelstreams_list() { - def result = [] - debug "Fuel Streams: Request to list fuel streams" - - def fuelStreamApp = getFuelStreamApp() +public writeToFuelStream(req){ + def name = "${handle()} Fuel Stream" + def streamName = "${(req.c ?: "")}||${req.n}" - if(fuelStreamApp){ - result = fuelStreamApp.listFuelStreams().values().collect { - it.c = it.c ?: "" - it + def result = getChildApps().find{ it.name == name && it.label.contains(streamName)} + if(!result){ + def id = (getChildApps().findAll{ it.name == name }.collect{ it.label.split(' - ')[0].toInteger()}.max() ?: 0) + 1 + try { + result = addChildApp('ady624', name, "$id - $streamName") + result.createStream([id: id, name: req.n, canister: req.c ?: ""]) } - } - else { - debug "Fuel stream app not installed. Install for local fuel streams" + catch(e){ + error "Please install the webCoRE Fuel Streams app for local Fuel Streams" + return + } } - render contentType: "application/javascript;charset=utf-8", data: "${params.callback}(${groovy.json.JsonOutput.toJson(["fuelStreams" : result])})" + result.updateFuelStream(req) } -private api_intf_fuelstreams_get() { - def result = [] - debug "Fuel Streams: Request to list fuel stream data" +private api_intf_fuelstreams_list() { + def result = [] + def name = "${handle()} Fuel Stream" + result = getChildApps().findAll{ it.name == name }*.getFuelStream() - def id = params.id + render contentType: "application/javascript;charset=utf-8", data: "${params.callback}(${groovy.json.JsonOutput.toJson(["fuelStreams" : result])})" +} - def fuelStreamApp = getFuelStreamApp() +private api_intf_fuelstreams_get() { + def result = [] + def id = params.id - if(fuelStreamApp){ - result = fuelStreamApp.listFuelStreamData(id) - } - else { - debug "Fuel stream app not installed. Install for local fuel streams" - } + 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])})" } @@ -1801,26 +1812,6 @@ private getStorageApp(install = false) { return storageApp } -public getFuelStreamApp(install = false){ - def name = handle() + ' Fuel Streams' - def fuelStreamApp = getChildApps().find{ it.name == name } - def label = "${app.label} Fuel Streams" - if(fuelStreamApp){ - if (label != fuelStreamApp.label) { - fuelStreamApp.updateLabel(label) - } - return fuelStreamApp - } - if (!install) return null - try { - fuelStreamApp = addChildApp("ady624", name, label) - } catch (all) { - if(hubUID) error "Please install the webCoRE Fuel Streams app for local Fuel Streams" - return null - } - return fuelStreamApp -} - private getDashboardApp(install = false) { def name = handle() + ' Dashboard' def label = app.label + ' (dashboard)' @@ -2205,7 +2196,8 @@ public Map getRunTimeData(semaphore = null, fetchWrappers = false) { ended: now(), generatedIn: now() - startTime, redirectContactBook: settings.redirectContactBook, - logPistonExecutions: settings.logPistonExecutions + logPistonExecutions: settings.logPistonExecutions, + useLocalFuelStreams : settings.localFuelStreams ] + (hubUID ? [ hsmStatus: state.hsmStatus, deviceIds: getAllDeviceIds() From c0259e7fc9b0ee2bd8d482cbec0e503216f5edec Mon Sep 17 00:00:00 2001 From: jp0550 Date: Mon, 13 Aug 2018 02:13:27 -0500 Subject: [PATCH 31/55] Work around ST parent state/app bug --- .../webcore-fuelstream.groovy | 4 ++++ smartapps/ady624/webcore.src/webcore.groovy | 19 +++++++++++++++++-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/smartapps/ady624/webcore-fuelstream.src/webcore-fuelstream.groovy b/smartapps/ady624/webcore-fuelstream.src/webcore-fuelstream.groovy index fb760395..9bc239b8 100644 --- a/smartapps/ady624/webcore-fuelstream.src/webcore-fuelstream.groovy +++ b/smartapps/ady624/webcore-fuelstream.src/webcore-fuelstream.groovy @@ -96,4 +96,8 @@ def getFuelStream(){ def listFuelStreamData(){ state.fuelStreamData.collect{ it << [t: getFormattedDate(new Date(it.i))]} +} + +def uninstalled(){ + parent.resetFuelStreamList() } \ No newline at end of file diff --git a/smartapps/ady624/webcore.src/webcore.groovy b/smartapps/ady624/webcore.src/webcore.groovy index 02546e08..54b7fe5f 100644 --- a/smartapps/ady624/webcore.src/webcore.groovy +++ b/smartapps/ady624/webcore.src/webcore.groovy @@ -1579,15 +1579,30 @@ 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 = hubUID ? [] : atomicState.fuelStreams ?: [] + if(!result){ - def id = (getChildApps().findAll{ it.name == name }.collect{ it.label.split(' - ')[0].toInteger()}.max() ?: 0) + 1 + 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") + result = addChildApp('ady624', name, "$id - $streamName") + if(!hubUID){ + 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){ From f4114f2c2e0e61fc5c5939c3e1a4cd8d024e9ac6 Mon Sep 17 00:00:00 2001 From: jp0550 Date: Mon, 13 Aug 2018 18:51:12 -0500 Subject: [PATCH 32/55] Fix location id = 1 for Hubitat --- .../webcore-piston.src/webcore-piston.groovy | 25 ++++++++++++------- smartapps/ady624/webcore.src/webcore.groovy | 8 +++--- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/smartapps/ady624/webcore-piston.src/webcore-piston.groovy b/smartapps/ady624/webcore-piston.src/webcore-piston.groovy index 7aa7a24f..cf8216f5 100644 --- a/smartapps/ady624/webcore-piston.src/webcore-piston.groovy +++ b/smartapps/ady624/webcore-piston.src/webcore-piston.groovy @@ -742,7 +742,7 @@ 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 + (hubUID ? '-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 @@ -984,7 +984,7 @@ 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 + (hubUID ? !isDeviceLocation(device) ? '' : '-L' : '')), name: srcEvent ? srcEvent.name : event.name, value: srcEvent ? srcEvent.value : event.value, descriptionText: srcEvent ? srcEvent.descriptionText : event.descriptionText, @@ -2427,9 +2427,9 @@ private long vcmd_setLocationMode(rtData, device, params) { private long vcmd_setAlarmSystemStatus(rtData, device, params) { def statusIdOrName = params[0] - def dev = rtData.virtualDevices['alarmSystemStatus']; + def dev = rtData.virtualDevices['alarmSystemStatus'] def options = hubUID ? dev?.ac : dev?.o - options?.find{ (it.key == statusIdOrName) || (it.value == statusIdOrName)}.collect{ [id: it.key, name: it.value] } + def status = options?.find{ (it.key == statusIdOrName) || (it.value == statusIdOrName)}.collect{ [id: it.key, name: it.value] } if (status && status.size()) { sendLocationEvent(name: (hubUID ? 'hsmSetArm' : 'alarmSystemStatus'), value: status[0].id) @@ -3730,7 +3730,7 @@ private evaluateOperand(rtData, node, operand, index = null, trigger = false, ne case 'd': //devices def deviceIds = [] for (d in expandDeviceList(rtData, operand.d)) { - if(hubUID){ + if(hubUID && !!rtData.deviceIds){ if(rtData.deviceIds.any { (d == hashId(it.id)) || (d == it.label) }) { deviceIds.push(d) } @@ -4819,7 +4819,7 @@ private sanitizeVariableName(name) { } private getDevice(rtData, idOrName) { - if(hubUID) return getDeviceHubitat(rtData, idOrName) + if(hubUID && !!rtData.deviceIds) return getDeviceHubitat(rtData, idOrName) if (rtData.locationId == idOrName) return location def device = rtData.devices[idOrName] ?: rtData.devices.find{ it.value.getDisplayName() == idOrName }?.value if (!device) { @@ -4920,7 +4920,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"] } @@ -6675,7 +6677,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() @@ -6707,7 +6709,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() @@ -7787,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() && (hubUID ? ((device?.hubs?.size() ?: 0) > 0) : true) +} + /******************************************************************************/ /*** DEBUG FUNCTIONS ***/ /******************************************************************************/ diff --git a/smartapps/ady624/webcore.src/webcore.groovy b/smartapps/ady624/webcore.src/webcore.groovy index 54b7fe5f..f98eade4 100644 --- a/smartapps/ady624/webcore.src/webcore.groovy +++ b/smartapps/ady624/webcore.src/webcore.groovy @@ -967,7 +967,7 @@ private api_get_base_result(deviceVersion = 0, updateCache = false) { 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: instanceId, - locationId: hashId(location.id, updateCache), + locationId: hashId(location.id + (hubUID ? '-L' : ''), updateCache), name: app.label ?: app.name, uri: state.endpoint, deviceVersion: currentDeviceVersion, @@ -983,7 +983,7 @@ private api_get_base_result(deviceVersion = 0, updateCache = false) { contactBookEnabled: location.getContactBookEnabled(), hubs: location.getHubs().collect{ [id: hashId(it.id, updateCache), name: it.name, firmware: hubUID ? getHubitatVersion()[it.id] : 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), + id: hashId(location.id + (hubUID ? '-L' : ''), updateCache), mode: hashId(location.getCurrentMode().id, updateCache), modes: location.getModes().collect{ [id: hashId(it.id, updateCache), name: it.name ]}, shm: hubUID ? transformHsmStatus(state.hsmStatus) : location.currentState("alarmSystemStatus")?.value, @@ -2063,7 +2063,7 @@ private testLifx() { private registerInstance() { def accountId = hashId(hubUID ?: app.getAccountId()) - def locationId = hashId(location.id) + def locationId = hashId(location.id + (hubUID ? '-L' : '')) def instanceId = hashId(app.id) def endpoint = state.endpoint def region = endpoint.contains('graph-eu') ? 'eu' : 'us'; @@ -2215,7 +2215,7 @@ public Map getRunTimeData(semaphore = null, fetchWrappers = false) { useLocalFuelStreams : settings.localFuelStreams ] + (hubUID ? [ hsmStatus: state.hsmStatus, - deviceIds: getAllDeviceIds() + deviceIds: allDeviceIds ] : [:]) } From 02cf0ccad1073bda061342d4f66d739f8aaef3f4 Mon Sep 17 00:00:00 2001 From: jp0550 Date: Tue, 14 Aug 2018 12:44:40 -0500 Subject: [PATCH 33/55] Fix flash commands --- .../webcore-piston.src/webcore-piston.groovy | 4 +-- .../webcore-storage.groovy | 3 ++- smartapps/ady624/webcore.src/webcore.groovy | 27 ++++++++++++------- 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/smartapps/ady624/webcore-piston.src/webcore-piston.groovy b/smartapps/ady624/webcore-piston.src/webcore-piston.groovy index cf8216f5..9e23d39c 100644 --- a/smartapps/ady624/webcore-piston.src/webcore-piston.groovy +++ b/smartapps/ady624/webcore-piston.src/webcore-piston.groovy @@ -1684,7 +1684,7 @@ private Boolean executeTask(rtData, devices, statement, task, async) { def vcmd = rtData.commands.virtual[command] long delay = 0 for (device in (virtualDevice ? [virtualDevice] : devices)) { - if (!virtualDevice && device.hasCommand(command)) { + if (!virtualDevice && device.hasCommand(command) && !(vcmd && vcmd.o /*virutal command overrides physical command*/)) { def msg = timer "Executed [$device].${command}" try { delay = "cmd_${command}"(rtData, device, params) @@ -2722,7 +2722,7 @@ private long vcmd_internal_fade(Map rtData, device, String command, int startLev return duration + 100 } -private long vcmd_emulatedflash(rtData, device, params) { +private long vcmd_emulatedFlash(rtData, device, params) { vcmd_flash(rtData, device, params) } diff --git a/smartapps/ady624/webcore-storage.src/webcore-storage.groovy b/smartapps/ady624/webcore-storage.src/webcore-storage.groovy index d1c04e69..19e0530d 100644 --- a/smartapps/ady624/webcore-storage.src/webcore-storage.groovy +++ b/smartapps/ady624/webcore-storage.src/webcore-storage.groovy @@ -170,7 +170,8 @@ public String mem(showBytes = true) { /* Push command has multiple overloads in hubitat */ public Map commandOverrides(){ return (hubUID ? [ - push : [c: "push", s: null , r: "pushMomentary"] //s: command signature + push : [c: "push", s: null , r: "pushMomentary"], + flash : [c: "flash", s: null , r: "flashNative"],//s: command signature ] : [:]) } diff --git a/smartapps/ady624/webcore.src/webcore.groovy b/smartapps/ady624/webcore.src/webcore.groovy index f98eade4..cee326e0 100644 --- a/smartapps/ady624/webcore.src/webcore.groovy +++ b/smartapps/ady624/webcore.src/webcore.groovy @@ -2754,8 +2754,9 @@ private Map attributes() { /* Push command has multiple overloads in hubitat */ public Map commandOverrides(){ - return (hubUID ? [ - push : [c: "push", s: null , r: "pushMomentary"] //s: command signature + return (hubUID ? [ //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 ] : [:]) } @@ -2879,7 +2880,7 @@ private Map commands() { high : [ n: "Set to High", ], ] + (hubUID ? [ doubleTap : [ n: "Double Tap", d: "Double tap button {0}", a: "doubleTapped", p:[[n: "Button #", t: "integer"]] ], - flash : [ n: "Flash", ], + 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" ] @@ -2892,7 +2893,7 @@ private Map virtualCommands() { //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"]], ], @@ -2974,11 +2975,19 @@ private Map virtualCommands() { ] : [:]) + (getLifxToken() ? [ lifxScene: [n: "Activate LIFX scene", p: ["Scene:lifxScenes"], l: true, dd: "Activate LIFX Scene '{0}'", aggregated: true], - ] : [:])*/ - + (hubUID ? [ + ] : [:])*/ + + if(hubUID){ + 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]}]], ], - flash : [ n: "Emulated 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}"]], ] - ] : [:]) + //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 } @@ -3266,7 +3275,7 @@ private Map virtualDevices(updateCache = false) { routine: [ n: 'Routine', t: 'enum', o: getRoutineOptions(updateCache), m: true], alarmSystemStatus: [ n: 'Smart Home Monitor status', t: 'enum', o: getAlarmSystemStatusOptions(), x: true] ] + (hubUID ? [ - alarmSystemStatus: [ n: 'Hubitat Safety Monitor status',t: 'enum', o: getHubitatAlarmSystemStatusOptions(), ac: getAlarmSystemStatusActions(), x: true], + alarmSystemStatus: [ n: 'Hubitat Safety Monitor status',t: 'enum', o: getHubitatAlarmSystemStatusOptions(), ac: getAlarmSystemStatusActions(), x: true], //ac - actions. hubitat doesn't reuse the status for actions //this one can be confusing to users so it's been commented out. It can subscribe to hsmSetArm, but the safety monitor doesn't actually send these events themselves, only other apps //alarmSystemEvent: [ n: 'Hubitat Safety Monitor event',t: 'enum', o: getAlarmSystemStatusActions(), m: true], alarmSystemAlert: [ n: 'Hubitat Safety Monitor alert',t: 'enum', o: getAlarmSystemAlertOptions(), m: true], From d2bb739cf793c24afbac0bf4f73427d850dfbd6e Mon Sep 17 00:00:00 2001 From: jp0550 Date: Tue, 14 Aug 2018 13:08:12 -0500 Subject: [PATCH 34/55] Fix fuel stream trimming --- .../ady624/webcore-fuelstream.src/webcore-fuelstream.groovy | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/smartapps/ady624/webcore-fuelstream.src/webcore-fuelstream.groovy b/smartapps/ady624/webcore-fuelstream.src/webcore-fuelstream.groovy index 9bc239b8..1511dc50 100644 --- a/smartapps/ady624/webcore-fuelstream.src/webcore-fuelstream.groovy +++ b/smartapps/ady624/webcore-fuelstream.src/webcore-fuelstream.groovy @@ -66,9 +66,7 @@ def cleanFuelStreams(){ log.debug "Size ${storageSize}KB Points ${points} Avg $averageSize Remove $pointsToRemove" def toBeRemoved = state.fuelStreamData.sort { it.i }.take(pointsToRemove) - state.fuelStreamData.each { - it.removeAll(toBeRemoved) - } + state.fuelStreamData.removeAll(toBeRemoved) } } From 6dc1ffd9026711de80cc20e4bf19d1b9806a9ccf Mon Sep 17 00:00:00 2001 From: jp0550 Date: Tue, 14 Aug 2018 13:21:56 -0500 Subject: [PATCH 35/55] Disable dashboard --- .../ady624/webcore-dashboard.src/webcore-dashboard.groovy | 6 +++--- smartapps/ady624/webcore.src/webcore.groovy | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/smartapps/ady624/webcore-dashboard.src/webcore-dashboard.groovy b/smartapps/ady624/webcore-dashboard.src/webcore-dashboard.groovy index ff4bc14d..52cda2e5 100644 --- a/smartapps/ady624/webcore-dashboard.src/webcore-dashboard.groovy +++ b/smartapps/ady624/webcore-dashboard.src/webcore-dashboard.groovy @@ -159,9 +159,9 @@ private void broadcastEvent(deviceId, eventName, eventValue, eventTime) { if(asynchttp_v1){ asynchttp_v1.put(null, params) } - else { - asynchttpPut((String)null, params) - } + //else { + // asynchttpPut((String)null, params) + //} } /******************************************************************************/ diff --git a/smartapps/ady624/webcore.src/webcore.groovy b/smartapps/ady624/webcore.src/webcore.groovy index cee326e0..b0836d2b 100644 --- a/smartapps/ady624/webcore.src/webcore.groovy +++ b/smartapps/ady624/webcore.src/webcore.groovy @@ -1828,6 +1828,7 @@ private getStorageApp(install = false) { } private getDashboardApp(install = false) { + if(hubUID) return null def name = handle() + ' Dashboard' def label = app.label + ' (dashboard)' def dashboardApp = getChildApps().find{ it.name == name } From 1f507ab84a9d485780f9affdc72f527428027eee Mon Sep 17 00:00:00 2001 From: jp0550 Date: Tue, 14 Aug 2018 13:54:15 -0500 Subject: [PATCH 36/55] Hide instance url message --- smartapps/ady624/webcore.src/webcore.groovy | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/smartapps/ady624/webcore.src/webcore.groovy b/smartapps/ady624/webcore.src/webcore.groovy index b0836d2b..dc53eb72 100644 --- a/smartapps/ady624/webcore.src/webcore.groovy +++ b/smartapps/ady624/webcore.src/webcore.groovy @@ -394,7 +394,7 @@ def pageMain() { if(customEndpoints){ if(hubUID) input "customHubUrl", "string", title: "Custom hub url different from ${hubUID ? "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 - 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" + if(hubUID) 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" } } } @@ -986,7 +986,7 @@ private api_get_base_result(deviceVersion = 0, updateCache = false) { id: hashId(location.id + (hubUID ? '-L' : ''), updateCache), mode: hashId(location.getCurrentMode().id, updateCache), modes: location.getModes().collect{ [id: hashId(it.id, updateCache), name: it.name ]}, - shm: hubUID ? transformHsmStatus(state.hsmStatus) : location.currentState("alarmSystemStatus")?.value, + shm: hubUID ? transformHsmStatus(location.hsmStatus ?: state.hsmStatus) : location.currentState("alarmSystemStatus")?.value, name: location.name, temperatureScale: location.getTemperatureScale(), timeZone: tz ? [ From c029cc8a0f4c7aee645edd95ef3e4d06d5e68044 Mon Sep 17 00:00:00 2001 From: jp0550 Date: Tue, 14 Aug 2018 14:06:50 -0500 Subject: [PATCH 37/55] Enable smartthings compatibility --- smartapps/ady624/webcore-piston.src/webcore-piston.groovy | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/smartapps/ady624/webcore-piston.src/webcore-piston.groovy b/smartapps/ady624/webcore-piston.src/webcore-piston.groovy index 9e23d39c..0212337d 100644 --- a/smartapps/ady624/webcore-piston.src/webcore-piston.groovy +++ b/smartapps/ady624/webcore-piston.src/webcore-piston.groovy @@ -288,10 +288,10 @@ public static String version() { return "v0.3.107.20180806" } /*** webCoRE DEFINITION ***/ /******************************************************************************/ private static String handle() { return "webCoRE" } -import hubitat.device.HubAction -import hubitat.device.Protocol -//import physicalgraph.device.HubAction -//import physicalgraph.device.Protocol +//import hubitat.device.HubAction +//import hubitat.device.Protocol +import physicalgraph.device.HubAction +import physicalgraph.device.Protocol if(!hubUID)include 'asynchttp_v1' From ac1ddb7f80b9573bf4b405e4c342158a3b483206 Mon Sep 17 00:00:00 2001 From: jp0550 Date: Sat, 18 Aug 2018 23:15:47 -0500 Subject: [PATCH 38/55] Fix null pointer on first write for Hubitat --- .../webcore-fuelstream.groovy | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/smartapps/ady624/webcore-fuelstream.src/webcore-fuelstream.groovy b/smartapps/ady624/webcore-fuelstream.src/webcore-fuelstream.groovy index 1511dc50..0778683f 100644 --- a/smartapps/ady624/webcore-fuelstream.src/webcore-fuelstream.groovy +++ b/smartapps/ady624/webcore-fuelstream.src/webcore-fuelstream.groovy @@ -44,13 +44,18 @@ def initialize(){ unsubscribe() unschedule() - if(!state.fuelStreamData){ - state.fuelStreamData = [] - } + 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) @@ -58,15 +63,15 @@ def cleanFuelStreams(){ if(storageSize > max){ log.debug "Trim down fuel stream" - def points = state.fuelStreamData.size() + 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 = state.fuelStreamData.sort { it.i }.take(pointsToRemove) - state.fuelStreamData.removeAll(toBeRemoved) + def toBeRemoved = getFuelStreamData().sort { it.i }.take(pointsToRemove) + getFuelStreamData().removeAll(toBeRemoved) } } @@ -77,7 +82,7 @@ def updateFuelStream(req){ def instance = req.i def source = req.s - state.fuelStreamData.add([d: data, i: (new Date()).getTime()]) + getFuelStreamData().add([d: data, i: (new Date()).getTime()]) cleanFuelStreams() } @@ -93,9 +98,9 @@ def getFuelStream(){ } def listFuelStreamData(){ - state.fuelStreamData.collect{ it << [t: getFormattedDate(new Date(it.i))]} + getFuelStreamData().collect{ it << [t: getFormattedDate(new Date(it.i))]} } def uninstalled(){ parent.resetFuelStreamList() -} \ No newline at end of file +} From c806d5ed49dbb1e239110092811e3065b9a0603b Mon Sep 17 00:00:00 2001 From: jp0550 Date: Mon, 20 Aug 2018 19:58:37 -0500 Subject: [PATCH 39/55] Remove unsupported methods --- .../webcore-piston.src/webcore-piston.groovy | 41 +------------------ smartapps/ady624/webcore.src/webcore.groovy | 3 +- 2 files changed, 2 insertions(+), 42 deletions(-) diff --git a/smartapps/ady624/webcore-piston.src/webcore-piston.groovy b/smartapps/ady624/webcore-piston.src/webcore-piston.groovy index 9e23d39c..c59b17c1 100644 --- a/smartapps/ady624/webcore-piston.src/webcore-piston.groovy +++ b/smartapps/ady624/webcore-piston.src/webcore-piston.groovy @@ -3730,14 +3730,7 @@ private evaluateOperand(rtData, node, operand, index = null, trigger = false, ne case 'd': //devices def deviceIds = [] for (d in expandDeviceList(rtData, operand.d)) { - if(hubUID && !!rtData.deviceIds){ - if(rtData.deviceIds.any { (d == hashId(it.id)) || (d == it.label) }) { - deviceIds.push(d) - } - } - else { - if (getDevice(rtData, d)) deviceIds.push(d) - } + if (getDevice(rtData, d)) deviceIds.push(d) } /* for (d in rtData, operand.d) { @@ -4819,7 +4812,6 @@ private sanitizeVariableName(name) { } private getDevice(rtData, idOrName) { - if(hubUID && !!rtData.deviceIds) return getDeviceHubitat(rtData, idOrName) if (rtData.locationId == idOrName) return location def device = rtData.devices[idOrName] ?: rtData.devices.find{ it.value.getDisplayName() == idOrName }?.value if (!device) { @@ -4837,37 +4829,6 @@ private getDevice(rtData, idOrName) { } return device } -//parent.listAvailableDevices(true) adds several hundred milliseconds. Get devs only as needed by deviceid -private getDeviceHubitat(rtData, idOrName) { - //def start = now() - 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 = [:] - } - - def deviceMap = rtData.allDevices.find{ (idOrName == it.key) || (idOrName == it.value.getDisplayName()) } - if(!deviceMap){ - def minDev = rtData.deviceIds.find { (idOrName == hashId(it.id)) || (idOrName == it.label) } - if(minDev){ - rtData.allDevices[hashId(minDev.id)] = getDeviceById(minDev.id) - deviceMap = rtData.allDevices.find { it.key == hashId(minDev.id) } - } - } - - if (deviceMap) { - rtData.updateDevices = true - rtData.devices[deviceMap.key] = deviceMap.value - device = deviceMap.value - } - else { - error "Device ${idOrName} was not found. Please review your piston.", rtData - } - } - //if (rtData.logging > 2) debug "Device grabbed in ${now() - start}ms", rtData - return device -} private getDeviceAttributeValue(rtData, device, attributeName) { if (rtData.event && (rtData.event.name == attributeName) && (rtData.event.device?.id == device.id)) { diff --git a/smartapps/ady624/webcore.src/webcore.groovy b/smartapps/ady624/webcore.src/webcore.groovy index dc53eb72..f81b368b 100644 --- a/smartapps/ady624/webcore.src/webcore.groovy +++ b/smartapps/ady624/webcore.src/webcore.groovy @@ -2215,8 +2215,7 @@ public Map getRunTimeData(semaphore = null, fetchWrappers = false) { logPistonExecutions: settings.logPistonExecutions, useLocalFuelStreams : settings.localFuelStreams ] + (hubUID ? [ - hsmStatus: state.hsmStatus, - deviceIds: allDeviceIds + hsmStatus: state.hsmStatus ] : [:]) } From eddbb68339a7f2a96ac62a28c1d0abcb27cf6746 Mon Sep 17 00:00:00 2001 From: jp0550 Date: Mon, 20 Aug 2018 22:53:43 -0500 Subject: [PATCH 40/55] Reduce fuel stream size --- .../webcore-fuelstream.groovy | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/smartapps/ady624/webcore-fuelstream.src/webcore-fuelstream.groovy b/smartapps/ady624/webcore-fuelstream.src/webcore-fuelstream.groovy index 0778683f..9de098f3 100644 --- a/smartapps/ady624/webcore-fuelstream.src/webcore-fuelstream.groovy +++ b/smartapps/ady624/webcore-fuelstream.src/webcore-fuelstream.groovy @@ -44,9 +44,10 @@ def initialize(){ unsubscribe() unschedule() - getFuelStreamData() - - cleanFuelStreams() + if(app.id){ + getFuelStreamData() + cleanFuelStreams() + } } def getFuelStreamData(){ @@ -73,6 +74,10 @@ def cleanFuelStreams(){ def toBeRemoved = getFuelStreamData().sort { it.i }.take(pointsToRemove) getFuelStreamData().removeAll(toBeRemoved) } + + getFuelStreamData().each { + it.keySet().remove('t') + } } def updateFuelStream(req){ @@ -98,9 +103,9 @@ def getFuelStream(){ } def listFuelStreamData(){ - getFuelStreamData().collect{ it << [t: getFormattedDate(new Date(it.i))]} + getFuelStreamData().collect{ it + [t: getFormattedDate(new Date(it.i))]} } def uninstalled(){ parent.resetFuelStreamList() -} +} \ No newline at end of file From 05aea0dcfee0b36c917772aca5a558273fb5d3b3 Mon Sep 17 00:00:00 2001 From: jp0550 Date: Mon, 20 Aug 2018 23:49:10 -0500 Subject: [PATCH 41/55] Add logs for missing setting --- .../webcore-piston.src/webcore-piston.groovy | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/smartapps/ady624/webcore-piston.src/webcore-piston.groovy b/smartapps/ady624/webcore-piston.src/webcore-piston.groovy index c59b17c1..75227c2b 100644 --- a/smartapps/ady624/webcore-piston.src/webcore-piston.groovy +++ b/smartapps/ady624/webcore-piston.src/webcore-piston.groovy @@ -324,7 +324,7 @@ preferences { /******************************************************************************/ def pageMain() { //webCoRE Piston main page - return dynamicPage(name: "pageMain", title: "", uninstall: !!state.build) { + return dynamicPage(name: "pageMain", title: "", install: hubUID ? 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." @@ -363,6 +363,12 @@ 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(hubUID){ + section(){ + input "dev", "capability.*", title: "Devices", description: "Piston devices", multiple: true + } + } } } } @@ -414,6 +420,7 @@ def isInstalled(){ } def installed() { + if(hubUID && !app.id) return state.created = now() state.modified = now() state.build = 0 @@ -1079,7 +1086,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) { @@ -4412,8 +4419,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(hubUID && deviceIdList && !settings.dev) debug "Unable to update setting 'dev' from child app. Open piston '$app.label' and click 'Done' for faster operation", rtData + app.updateSetting('dev', [type: hubUID ? 'capability' : 'capability.device', value: deviceIdList.unique()]) } private void updateContactList(contactIdList) { @@ -4743,7 +4751,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 } @@ -4815,7 +4823,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) { From 74d417fa04587948d74d0b76f787ec1c53444fd5 Mon Sep 17 00:00:00 2001 From: jp0550 Date: Tue, 21 Aug 2018 00:23:06 -0500 Subject: [PATCH 42/55] Add HSM api changes --- smartapps/ady624/webcore.src/webcore.groovy | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/smartapps/ady624/webcore.src/webcore.groovy b/smartapps/ady624/webcore.src/webcore.groovy index f81b368b..574b108d 100644 --- a/smartapps/ady624/webcore.src/webcore.groovy +++ b/smartapps/ady624/webcore.src/webcore.groovy @@ -3227,10 +3227,11 @@ private static Map getHubitatAlarmSystemStatusOptions() { private static Map getAlarmSystemAlertOptions() { return [ - intrusion: "Intrusion", - smoke: "Smoke", - water: "Water", - rule: "Rule" + intrusion: "Intrusion Away", + "intrusion-home": "Intrusion Home", + smoke: "Smoke", + water: "Water", + rule: "Rule" ] } @@ -3276,8 +3277,7 @@ private Map virtualDevices(updateCache = false) { alarmSystemStatus: [ n: 'Smart Home Monitor status', t: 'enum', o: getAlarmSystemStatusOptions(), x: true] ] + (hubUID ? [ alarmSystemStatus: [ n: 'Hubitat Safety Monitor status',t: 'enum', o: getHubitatAlarmSystemStatusOptions(), ac: getAlarmSystemStatusActions(), x: true], //ac - actions. hubitat doesn't reuse the status for actions - //this one can be confusing to users so it's been commented out. It can subscribe to hsmSetArm, but the safety monitor doesn't actually send these events themselves, only other apps - //alarmSystemEvent: [ n: 'Hubitat Safety Monitor event',t: 'enum', o: getAlarmSystemStatusActions(), m: true], + 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] ] : [:]) From c94d11e27e8e119bf4a6fae15e24b8803acadbac Mon Sep 17 00:00:00 2001 From: jp0550 Date: Tue, 21 Aug 2018 23:08:28 -0500 Subject: [PATCH 43/55] Fix hsmStatus evaluation --- smartapps/ady624/webcore-piston.src/webcore-piston.groovy | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/smartapps/ady624/webcore-piston.src/webcore-piston.groovy b/smartapps/ady624/webcore-piston.src/webcore-piston.groovy index 75227c2b..4785c656 100644 --- a/smartapps/ady624/webcore-piston.src/webcore-piston.groovy +++ b/smartapps/ady624/webcore-piston.src/webcore-piston.groovy @@ -5292,6 +5292,7 @@ def setLocalVariable(name, value) { /******************************************************************************/ def Map proxyEvaluateExpression(rtData, expression, dataType = null) { + log.debug expression resetRandomValues() rtData = getRunTimeData(rtData) def result = evaluateExpression(rtData, expression, dataType) @@ -8054,7 +8055,7 @@ private Map getSystemVariables() { "\$iftttStatusCode": [t: "integer", v: null], "\$iftttStatusOk": [t: "boolean", v: null], "\$locationMode": [t: "string", d: true], - "\$${hubUID ? "hsmStatus" : "shmStatus"}": [t: "string", d: true], + (hubUID ? "\$hsmStatus" : "\$shmStatus"): [t: "string", d: true], "\$version": [t: "string", d: true] ].sort{it.key} } @@ -8107,7 +8108,7 @@ private getSystemVariableValue(rtData, name) { 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 "\$${hubUID ? "hsmStatus" : "shmStatus"}": + case (hubUID ? "\$hsmStatus" : "\$shmStatus"): if(hubUID) { return location.hsmStatus ?: rtData.hsmStatus } else switch (location.currentState("alarmSystemStatus")?.value) { case 'off': return 'Disarmed'; case 'stay': return 'Armed/Stay'; case 'away': return 'Armed/Away'; }; return null; } From be3135928eebbdbf4202b7c75dd0b1594f1bf472 Mon Sep 17 00:00:00 2001 From: jp0550 Date: Wed, 22 Aug 2018 20:05:17 -0500 Subject: [PATCH 44/55] Remove debug statement --- smartapps/ady624/webcore-piston.src/webcore-piston.groovy | 1 - 1 file changed, 1 deletion(-) diff --git a/smartapps/ady624/webcore-piston.src/webcore-piston.groovy b/smartapps/ady624/webcore-piston.src/webcore-piston.groovy index 4785c656..4be1b485 100644 --- a/smartapps/ady624/webcore-piston.src/webcore-piston.groovy +++ b/smartapps/ady624/webcore-piston.src/webcore-piston.groovy @@ -5292,7 +5292,6 @@ def setLocalVariable(name, value) { /******************************************************************************/ def Map proxyEvaluateExpression(rtData, expression, dataType = null) { - log.debug expression resetRandomValues() rtData = getRunTimeData(rtData) def result = evaluateExpression(rtData, expression, dataType) From 91173cd4e3125e42ecee118dccf6050e965aa4ae Mon Sep 17 00:00:00 2001 From: jp0550 Date: Sat, 25 Aug 2018 22:31:11 -0500 Subject: [PATCH 45/55] Performance improvements --- .../webcore-piston.src/webcore-piston.groovy | 176 ++++++++++-------- .../webcore-storage.groovy | 3 + smartapps/ady624/webcore.src/webcore.groovy | 30 +-- 3 files changed, 124 insertions(+), 85 deletions(-) diff --git a/smartapps/ady624/webcore-piston.src/webcore-piston.groovy b/smartapps/ady624/webcore-piston.src/webcore-piston.groovy index 4be1b485..4e48d6d8 100644 --- a/smartapps/ady624/webcore-piston.src/webcore-piston.groovy +++ b/smartapps/ady624/webcore-piston.src/webcore-piston.groovy @@ -288,12 +288,8 @@ public static String version() { return "v0.3.107.20180806" } /*** webCoRE DEFINITION ***/ /******************************************************************************/ private static String handle() { return "webCoRE" } -import hubitat.device.HubAction -import hubitat.device.Protocol -//import physicalgraph.device.HubAction -//import physicalgraph.device.Protocol -if(!hubUID)include 'asynchttp_v1' +if(!isHubitat())include 'asynchttp_v1' definition( name: "${handle()} Piston", @@ -324,7 +320,7 @@ preferences { /******************************************************************************/ def pageMain() { //webCoRE Piston main page - return dynamicPage(name: "pageMain", title: "", install: hubUID ? true : false, 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." @@ -364,9 +360,11 @@ def pageMain() { href "pageClearAll", title: "Clear all data", description: "You will lose all data stored in any variables" } - if(hubUID){ + 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 } } } @@ -420,7 +418,7 @@ def isInstalled(){ } def installed() { - if(hubUID && !app.id) return + if(isHubitat() && !app.id) return state.created = now() state.modified = now() state.build = 0 @@ -435,6 +433,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 } @@ -739,7 +743,7 @@ 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 - def atomState = hubUID ? getCachedAtomicState() : atomicState + def atomState = (rtData.waitedAtSemaphore ?: true) ? (isHubitat() ? getCachedAtomicState() : atomicState) : state rtData.cache = atomState.cache ?: [:] rtData.newCache = [:] @@ -749,7 +753,7 @@ 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 + (hubUID ? '-L' : '')) + 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 @@ -762,7 +766,7 @@ private getRunTimeData(rtData = null, semaphore = null, fetchWrappers = false) { rtData.fastForwardTo = null rtData.break = false rtData.updateDevices = false - rtData.timeLimits = getTimeLimits() + rtData.pistonLimits = getPistonLimits() state.schedules = atomState.schedules if (!fetchWrappers) { @@ -824,26 +828,30 @@ def executeHandler(event) { handleEvents([date: event.date, device: location, name: 'execute', value: event.value, jsonData: event.jsonData]) } -def getTimeLimits(){ - return hubUID ? [ +def getPistonLimits(){ + return isHubitat() ? [ schedule: 20000, scheduleVariance: 3000, executionTime: 30000, taskRemaining: 3000, taskDelayMax: 5000, - recovery: 45000 + maxStats: settings.maxStats ?: 1, + maxLogs: settings.maxLogs ?: 1, + recovery: 45 ] : [ schedule: 5000, scheduleVariance: 2000, executionTime: 20000, taskRemaining: 10000, - taskDelayMax: 5000 + taskDelayMax: 5000, + maxStats: 500, + maxLogs: 500 ] } //entry point for all events def handleEvents(event) { //cancel all pending jobs, we'll handle them later - if(hubUID) unschedule(timeHandler) + if(isHubitat()) unschedule(timeHandler) if (!state.active) return def startTime = now() state.lastExecuted = startTime @@ -862,8 +870,8 @@ def handleEvents(event) { return; } checkVersion(rtData) - if(hubUID) { - runIn(rtData.timeLimits.recovery.toInteger(), timeRecoveryHandler) + if(isHubitat()) { + runIn(rtData.pistonLimits.recovery.toInteger(), timeRecoveryHandler) } else { setTimeoutRecoveryHandler('timeoutRecoveryHandler_webCoRE') @@ -891,7 +899,7 @@ def handleEvents(event) { //process all time schedules in order def t = now() - while (success && (rtData.timeLimits.executionTime + rtData.timestamp - now() > rtData.timeLimits.schedule)) { + 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... @@ -899,7 +907,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() + rtData.timeLimits.scheduleVariance }] + 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 @@ -991,7 +999,7 @@ 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 + (hubUID ? !isDeviceLocation(device) ? '' : '-L' : '')), + 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, @@ -1102,7 +1110,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) { @@ -1197,13 +1205,13 @@ private processSchedules(rtData, scheduleJob = false) { 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.toInteger(), timeHandler, [data: next]) - if(hubUID){ - runIn((t + rtData.timeLimits.recovery).toInteger(), timeRecoveryHandler, [data: next]) + if(isHubitat()){ + runIn((t + rtData.pistonLimits.recovery).toInteger(), timeRecoveryHandler, [data: next]) } } else { rtData.stats.nextSchedule = 0 //remove the recovery - if(hubUID){ + if(isHubitat()){ unschedule(timeRecoveryHandler) } } @@ -1219,7 +1227,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) { @@ -1710,13 +1718,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 = rtData.timeLimits.executionTime + 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 3s left //keep executing until we hit 3 seconds before the total execution time limit - if (reschedule || (timeLeft - delay < rtData.timeLimits.taskRemaining) || (delay >= rtData.timeLimits.taskMaxDelay) || async) { + 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) @@ -1747,7 +1755,7 @@ private long executeVirtualCommand(rtData, devices, command, params) } private executePhysicalCommand(rtData, device, command, params = [], delay = null, scheduleDevice = null, disableCommandOptimization = false) { - if(hubUID && (!!delay && !scheduleDevice)){ + if(isHubitat() && (!!delay && !scheduleDevice)){ //delay without schedules is not supported in hubitat scheduleDevice = hashId(device.id) } @@ -1881,9 +1889,9 @@ private scheduleTimer(rtData, timer, long lastRun = 0) { //switch to local date/times //hubitat timezone is already local - time = hubUID ? time : utcToLocalTime(time) - long rightNow = hubUID ? now() : utcToLocalTime(now()) - lastRun = lastRun ? (hubUID ? lastRun : utcToLocalTime(lastRun)) : rightNow + time = isHubitat() ? time : utcToLocalTime(time) + long rightNow = isHubitat() ? now() : utcToLocalTime(now()) + lastRun = lastRun ? (isHubitat() ? lastRun : utcToLocalTime(lastRun)) : rightNow long nextSchedule = lastRun if (lastRun > rightNow) { @@ -1999,7 +2007,7 @@ private scheduleTimer(rtData, timer, long lastRun = 0) { if (nextSchedule > lastRun) { //convert back to UTC - nextSchedule = hubUID ? nextSchedule : localToUtcTime(nextSchedule) + nextSchedule = isHubitat() ? nextSchedule : localToUtcTime(nextSchedule) rtData.schedules.removeAll{ it.s == timer.$ } requestWakeUp(rtData, timer, [$: -1], nextSchedule) } @@ -2257,8 +2265,8 @@ private long cmd_setColorTemperature(rtData, device, params) { return 0 } -private getColor(colorValue) { - def color = (colorValue == 'Random') ? (colorUtil?.RANDOM ?: parent.getRandomColor()) : (colorUtil?.findByName(colorValue) ?: parent.getColorByName(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, @@ -2281,7 +2289,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 @@ -2296,7 +2304,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 @@ -2369,8 +2377,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 } @@ -2402,8 +2410,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 } @@ -2435,11 +2443,11 @@ private long vcmd_setLocationMode(rtData, device, params) { private long vcmd_setAlarmSystemStatus(rtData, device, params) { def statusIdOrName = params[0] def dev = rtData.virtualDevices['alarmSystemStatus'] - def options = hubUID ? dev?.ac : dev?.o + 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: (hubUID ? 'hsmSetArm' : '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 } @@ -2800,9 +2808,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] : "" @@ -2963,7 +2971,7 @@ private long vcmd_wolRequest(rtData, device, params) { def secureCode = params[1] mac = mac.replace(":", "").replace("-", "").replace(".", "").replace(" ", "").toLowerCase() - sendHubCommand(new HubAction( + sendHubCommand(HubActionClass.newInstance( "wake on lan $mac", Protocol.LAN, null, @@ -3080,7 +3088,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 @@ -3155,8 +3163,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] @@ -3197,8 +3205,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') @@ -3323,7 +3331,7 @@ private long vcmd_httpRequest(rtData, device, params) { data[variable] = getVariable(rtData, variable).v } } - if (internal && !hubUID) { + 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) @@ -3337,7 +3345,7 @@ private long vcmd_httpRequest(rtData, device, params) { query: useQueryString ? data : null, //thank you @destructure00 body: !useQueryString ? data : null //thank you @destructure00 ] - sendHubCommand(new 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 @@ -3428,7 +3436,7 @@ private long vcmd_writeToFuelStream(rtData, device, params) { if(rtData.useLocalFuelStreams){ parent.writeToFuelStream(req) } - else if(!hubUID){ + else if(!isHubitat()){ def requestParams = [ uri: "https://api-${rtData.region}-${rtData.instanceId[32]}.webcore.co:9247", path: "/fuelStream/write", @@ -4068,7 +4076,7 @@ private Boolean evaluateComparison(rtData, comparison, lo, ro = null, ro2 = null case 'time': case 'date': case 'datetime': - boolean pass = checkTimeRestrictions(rtData, lo.operand, hubUID ? now() : 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; } @@ -4409,7 +4417,7 @@ private traverseExpressions(node, closure, param, parentNode = null) { } private getRoutineById(routineId) { - if(hubUID) return [ id : routineId ] + if(isHubitat()) return [ id : routineId ] def routines = location.helloHome?.getPhrases() for(routine in routines) { if (routine && routine?.label && (hashId(routine.id) == routineId)) { @@ -4420,8 +4428,8 @@ private getRoutineById(routineId) { } private void updateDeviceList(rtData, deviceIdList) { - if(hubUID && deviceIdList && !settings.dev) debug "Unable to update setting 'dev' from child app. Open piston '$app.label' and click 'Done' for faster operation", rtData - app.updateSetting('dev', [type: hubUID ? 'capability' : 'capability.device', value: deviceIdList.unique()]) + 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) { @@ -4512,7 +4520,7 @@ private void subscribeAll(rtData) { switch (operand.v) { case 'alarmSystemStatus': subscriptionId = "$deviceId${operand.v}" - attribute = hubUID ? "hsmStatus" : operand.v + attribute = isHubitat() ? "hsmStatus" : operand.v break; case 'alarmSystemAlert': subscriptionId = "$deviceId${operand.v}" @@ -4539,13 +4547,13 @@ private void subscribeAll(rtData) { def routine = getRoutineById(value.c) if (routine) { subscriptionId = "$deviceId${operand.v}${routine.id}" - attribute = "routineExecuted${hubUID ? "" : ("." + routine.id)}" + attribute = "routineExecuted${isHubitat() ? "" : ("." + routine.id)}" } } break case 'email': subscriptionId = "$deviceId${operand.v}${hashId(app.id)}" - attribute = "email${hubUID ? "" : ("." + hashId(app.id))}" + attribute = "email${isHubitat() ? "" : ("." + hashId(app.id))}" break case 'ifttt': case 'askAlexa': @@ -4556,7 +4564,7 @@ private void subscribeAll(rtData) { if (item) { subscriptionId = "$deviceId${operand.v}${item}" - def attrVal = hubUID ? "" : ".${item}" + def attrVal = isHubitat() ? "" : ".${item}" attribute = "${operand.v}${attrVal}" switch (operand.v) { case 'askAlexa': @@ -4876,7 +4884,7 @@ private Map getDeviceAttribute(rtData, deviceId, attributeName, subDeviceIndex = def mode = location.getCurrentMode(); return [t: 'string', v: hashId(mode.getId()), n: mode.getName()] case 'alarmSystemStatus': - def v = hubUID ? (location.hsmStatus ?: rtData.hsmStatus) : location.currentState("alarmSystemStatus")?.value + def v = isHubitat() ? (rtData.hsmStatus) : location.currentState("alarmSystemStatus")?.value def n = rtData.virtualDevices['alarmSystemStatus']?.o[v] return [t: 'string', v: v, n: n] } @@ -5145,7 +5153,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) { @@ -6278,9 +6286,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 @@ -7561,7 +7569,7 @@ private utcToLocalDate(dateOrTimeOrString = null) { } if (dateOrTimeOrString instanceof Long) { //ST the system time is UTC, hubitat is user's local timezone. No need to convert - return new Date(dateOrTimeOrString + ( (!hubUID && location.timeZone) ? location.timeZone.getOffset(dateOrTimeOrString) : 0)) + return new Date(dateOrTimeOrString + ( (!isHubitat() && location.timeZone) ? location.timeZone.getOffset(dateOrTimeOrString) : 0)) } return null } @@ -7764,7 +7772,7 @@ private List hexToRgbArray(hex) { //hubitat device ids can be the same as the location id private isDeviceLocation(device){ - return device?.id.toString() == location.id.toString() && (hubUID ? ((device?.hubs?.size() ?: 0) > 0) : true) + return device?.id.toString() == location.id.toString() && (isHubitat() ? ((device?.hubs?.size() ?: 0) > 0) : true) } /******************************************************************************/ @@ -7830,7 +7838,7 @@ 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) { + if (isHubitat()) { if(err){ log."$cmd" "$prefix $message $err" } @@ -8054,7 +8062,7 @@ private Map getSystemVariables() { "\$iftttStatusCode": [t: "integer", v: null], "\$iftttStatusOk": [t: "boolean", v: null], "\$locationMode": [t: "string", d: true], - (hubUID ? "\$hsmStatus" : "\$shmStatus"): [t: "string", d: true], + (isHubitat() ? "\$hsmStatus" : "\$shmStatus"): [t: "string", d: true], "\$version": [t: "string", d: true] ].sort{it.key} } @@ -8101,14 +8109,14 @@ 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 ?: parent.getRandomColor())?.rgb; setRandomValue("\$randomColor", result); return result - case "\$randomColorName": def result = getRandomValue("\$randomColorName") ?: (colorUtil?.RANDOM ?: parent.getRandomColor())?.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 (hubUID ? "\$hsmStatus" : "\$shmStatus"): - if(hubUID) { return location.hsmStatus ?: rtData.hsmStatus } + 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; } } @@ -8135,4 +8143,24 @@ private void setRandomValue(name, value) { 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 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 19e0530d..619dbe5c 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) { diff --git a/smartapps/ady624/webcore.src/webcore.groovy b/smartapps/ady624/webcore.src/webcore.groovy index 574b108d..3a7a8c14 100644 --- a/smartapps/ady624/webcore.src/webcore.groovy +++ b/smartapps/ady624/webcore.src/webcore.groovy @@ -1045,6 +1045,13 @@ private api_intf_dashboard_load() { recoveryHandler() //install storage app def storageApp = getStorageApp(true) + if(storageApp && hubUID){ //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) @@ -1798,6 +1805,7 @@ private cleanUp() { } private getStorageApp(install = false) { + if(hubUID && state.migratedStorage) return null def name = handle() + ' Storage' def storageApp = getChildApps().find{ it.name == name } def label = "${app.label} Devices" @@ -1808,6 +1816,10 @@ private getStorageApp(install = false) { return storageApp } if (!install) return null + if(hubUID){ + state.migratedStorage = true + return null + } try { storageApp = addChildApp("ady624", name, label) } catch (all) { @@ -2167,8 +2179,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) { @@ -2213,9 +2226,11 @@ public Map getRunTimeData(semaphore = null, fetchWrappers = false) { generatedIn: now() - startTime, redirectContactBook: settings.redirectContactBook, logPistonExecutions: settings.logPistonExecutions, - useLocalFuelStreams : settings.localFuelStreams + useLocalFuelStreams : settings.localFuelStreams, + waitedAtSemaphore : waited ] + (hubUID ? [ - hsmStatus: state.hsmStatus + hsmStatus: state.hsmStatus ?: location.hsmStatus, + colors: getColors() ] : [:]) } @@ -3282,13 +3297,6 @@ private Map virtualDevices(updateCache = false) { alarmSystemRule: [ n: 'Hubitat Safety Monitor rule',t: 'enum', o: getAlarmSystemRuleOptions(), m: true] ] : [:]) } -public Map getColorByName(name){ - return getColors().find{ it.name == name } -} -public Map getRandomColor(){ - def random = (int)(Math.random() * getColors().size()) - return getColors()[random] -} public List getColors(){ return [ From 970035dece318d7bfbc664ed5b2316992451a511 Mon Sep 17 00:00:00 2001 From: jp0550 Date: Sat, 25 Aug 2018 22:39:08 -0500 Subject: [PATCH 46/55] Remove hubUID references --- .../webcore-dashboard.groovy | 6 +- .../webcore-storage.groovy | 6 +- smartapps/ady624/webcore.src/webcore.groovy | 80 ++++++++++--------- 3 files changed, 52 insertions(+), 40 deletions(-) diff --git a/smartapps/ady624/webcore-dashboard.src/webcore-dashboard.groovy b/smartapps/ady624/webcore-dashboard.src/webcore-dashboard.groovy index 52cda2e5..f3c4883b 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.107.20180806" } /*** webCoRE DEFINITION ***/ /******************************************************************************/ private static String handle() { return "webCoRE" } -if(!hubUID)include 'asynchttp_v1' +if(!isHubitat())include 'asynchttp_v1' definition( name: "${handle()} Dashboard", namespace: "ady624", @@ -195,6 +195,10 @@ def String hashId(id) { return result } +private isHubitat(){ + return hubUID != null +} + /******************************************************************************/ /*** ***/ /*** END OF CODE ***/ diff --git a/smartapps/ady624/webcore-storage.src/webcore-storage.groovy b/smartapps/ady624/webcore-storage.src/webcore-storage.groovy index 619dbe5c..5d3f4980 100644 --- a/smartapps/ady624/webcore-storage.src/webcore-storage.groovy +++ b/smartapps/ady624/webcore-storage.src/webcore-storage.groovy @@ -172,7 +172,7 @@ public String mem(showBytes = true) { /* Push command has multiple overloads in hubitat */ public Map commandOverrides(){ - return (hubUID ? [ + return (isHubitat() ? [ push : [c: "push", s: null , r: "pushMomentary"], flash : [c: "flash", s: null , r: "flashNative"],//s: command signature ] : [:]) @@ -209,6 +209,10 @@ def String hashId(id) { return result } +private isHubitat(){ + return hubUID != null +} + /******************************************************************************/ /*** ***/ /*** END OF CODE ***/ diff --git a/smartapps/ady624/webcore.src/webcore.groovy b/smartapps/ady624/webcore.src/webcore.groovy index 3a7a8c14..ad7b989a 100644 --- a/smartapps/ady624/webcore.src/webcore.groovy +++ b/smartapps/ady624/webcore.src/webcore.groovy @@ -288,7 +288,7 @@ public static String version() { return "v0.3.107.20180806" } /******************************************************************************/ private static String handle() { return "webCoRE" } private static String domain() { return "webcore.co" } -if(!hubUID) include 'asynchttp_v1' +if(!isHubitat()) include 'asynchttp_v1' definition( name: "${handle()}", namespace: "ady624", @@ -392,9 +392,9 @@ def pageMain() { input "customEndpoints", "bool", submitOnChange: true, title: "Use custom endpoints?", default: false, required: true if(customEndpoints){ - if(hubUID) input "customHubUrl", "string", title: "Custom hub url different from ${hubUID ? "https://cloud.hubitat.com" : "https://graph.smartthings.com"}", default: null, required: false + 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(hubUID) 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" + 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" } } } @@ -578,7 +578,7 @@ def pageSettings() { def storageApp = getStorageApp() if (storageApp) { section("Available devices") { - app([title: hubUID ? 'Do not click' : '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") { @@ -587,7 +587,7 @@ def pageSettings() { } section("Fuel Streams"){ - input "localFuelStreams", "bool", title: "Use local fuel streams?", defaultValue: hubUID ? true : false, submitOnChange: true + 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" } @@ -614,7 +614,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: hubUID ? false : true, required: false + 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") { @@ -632,7 +632,7 @@ def pageSettings() { private pageFuelStreams(){ dynamicPage(name: "pageFuelStreams", title: "", uninstall: false, install: false){ section(){ - app([title: hubUID ? 'Do not click' : 'Fuel Streams', multiple: true, install: true, uninstall: false], 'fuelStreams', 'ady624', "${handle()} Fuel Stream") + app([title: isHubitat() ? 'Do not click' : 'Fuel Streams', multiple: true, install: true, uninstall: false], 'fuelStreams', 'ady624', "${handle()} Fuel Stream") } } } @@ -854,7 +854,7 @@ private updateEndpoint(accessToken){ state.endpoint = customServerUrl("?access_token=${accessToken}") } else { - state.endpoint = hubUID ? apiServerUrl("$hubUID/apps/${app.id}/?access_token=${accessToken}") : apiServerUrl("/api/token/${accessToken}/smartapps/installations/${app.id}/") + state.endpoint = isHubitat() ? apiServerUrl("$hubUID/apps/${app.id}/?access_token=${accessToken}") : apiServerUrl("/api/token/${accessToken}/smartapps/installations/${app.id}/") } } private initializeWebCoREEndpoint() { @@ -887,7 +887,7 @@ private subscribeAll() { subscribe(location, "echoSistant", echoSistantHandler) subscribe(location, "HubUpdated", hubUpdatedHandler, [filterEvents: false]) subscribe(location, "summary", summaryHandler, [filterEvents: false]) - if(hubUID) subscribe(location, "hsmStatus", hsmHandler, [filterEvents: false]) + if(isHubitat()) subscribe(location, "hsmStatus", hsmHandler, [filterEvents: false]) setPowerSource(getHub()?.isBatteryInUse() ? 'battery' : 'mains') } @@ -967,7 +967,7 @@ private api_get_base_result(deviceVersion = 0, updateCache = false) { 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: instanceId, - locationId: hashId(location.id + (hubUID ? '-L' : ''), updateCache), + locationId: hashId(location.id + (isHubitat() ? '-L' : ''), updateCache), name: app.label ?: app.name, uri: state.endpoint, deviceVersion: currentDeviceVersion, @@ -981,12 +981,12 @@ private api_get_base_result(deviceVersion = 0, updateCache = false) { ] + (sendDevices ? [contacts: [:], devices: listAvailableDevices(false, updateCache)] : [:]), location: [ contactBookEnabled: location.getContactBookEnabled(), - hubs: location.getHubs().collect{ [id: hashId(it.id, updateCache), name: it.name, firmware: hubUID ? getHubitatVersion()[it.id] : 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 + (hubUID ? '-L' : ''), 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 ? transformHsmStatus(location.hsmStatus ?: state.hsmStatus) : location.currentState("alarmSystemStatus")?.value, + shm: isHubitat() ? transformHsmStatus(location.hsmStatus ?: state.hsmStatus) : location.currentState("alarmSystemStatus")?.value, name: location.name, temperatureScale: location.getTemperatureScale(), timeZone: tz ? [ @@ -1013,7 +1013,7 @@ private getFuelStreamUrls(iid){ } def baseUrl = isCustomEndpoint() ? customServerUrl("/") : - hubUID ? apiServerUrl("$hubUID/apps/${app.id}/") + 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}" @@ -1045,7 +1045,7 @@ private api_intf_dashboard_load() { recoveryHandler() //install storage app def storageApp = getStorageApp(true) - if(storageApp && hubUID){ //migrate off of storage app + if(storageApp && isHubitat()){ //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 }]) } @@ -1109,7 +1109,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.isInstalled()) 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") @@ -1157,7 +1157,7 @@ private api_intf_dashboard_piston_get() { result.now = now() def jsonData = groovy.json.JsonOutput.toJson(result) - if(hubUID && (!isCustomEndpoint() || customHubUrl.contains(hubUID))){ + 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 @@ -1510,7 +1510,7 @@ private api_intf_dashboard_piston_delete() { if (verifySecurityToken(params.token)) { def piston = getChildApps().find{ hashId(it.id) == params.id }; if (piston) { - app.deleteChildApp(hubUID ? piston.id : piston) + app.deleteChildApp(isHubitat() ? piston.id : piston) result = [status: "ST_SUCCESS"] state.remove(params.id) state.remove('sph${params.id}') @@ -1595,7 +1595,7 @@ public writeToFuelStream(req){ def streamName = "${(req.c ?: "")}||${req.n}" def result = getChildApps().find{ it.name == name && it.label.contains(streamName)} - def fuelStreams = hubUID ? [] : atomicState.fuelStreams ?: [] + 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 @@ -1605,7 +1605,7 @@ public writeToFuelStream(req){ def id = (getChildApps().findAll{ it.name == name }.collect{ it.label.split(' - ')[0].toInteger()}.max() ?: 0) + 1 try { result = addChildApp('ady624', name, "$id - $streamName") - if(!hubUID){ + if(!isHubitat()){ fuelStreams = getChildApps().find{ it.name == name }.collect { it.label } fuelStreams << result.label atomicState.fuelStreams = fuelStreams @@ -1690,7 +1690,7 @@ private api_intf_dashboard_piston_activity() { def api_ifttt() { def data = [:] - def remoteAddr = hubUID ? "UNKNOWN" : 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) { @@ -1722,7 +1722,7 @@ def api_email() { private api_execute() { def result = [:] def data = [:] - def remoteAddr = hubUID ? "UNKNOWN" : 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 = [:] @@ -1805,7 +1805,7 @@ private cleanUp() { } private getStorageApp(install = false) { - if(hubUID && state.migratedStorage) return null + if(isHubitat() && state.migratedStorage) return null def name = handle() + ' Storage' def storageApp = getChildApps().find{ it.name == name } def label = "${app.label} Devices" @@ -1816,7 +1816,7 @@ private getStorageApp(install = false) { return storageApp } if (!install) return null - if(hubUID){ + if(isHubitat()){ state.migratedStorage = true return null } @@ -1840,7 +1840,7 @@ private getStorageApp(install = false) { } private getDashboardApp(install = false) { - if(hubUID) return null + if(isHubitat()) return null def name = handle() + ' Dashboard' def label = app.label + ' (dashboard)' def dashboardApp = getChildApps().find{ it.name == name } @@ -1882,7 +1882,7 @@ private String getDashboardInitUrl(register = false) { else { 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() + ((hubUID ?: state.accessToken) + app.id).replace("-", "") + (isHubitat() ? '/?access_token=' + state.accessToken : '')).bytes.encodeBase64() } } @@ -2076,7 +2076,7 @@ private testLifx() { private registerInstance() { def accountId = hashId(hubUID ?: app.getAccountId()) - def locationId = hashId(location.id + (hubUID ? '-L' : '')) + def locationId = hashId(location.id + (isHubitat() ? '-L' : '')) def instanceId = hashId(app.id) def endpoint = state.endpoint def region = endpoint.contains('graph-eu') ? 'eu' : 'us'; @@ -2228,7 +2228,7 @@ public Map getRunTimeData(semaphore = null, fetchWrappers = false) { logPistonExecutions: settings.logPistonExecutions, useLocalFuelStreams : settings.localFuelStreams, waitedAtSemaphore : waited - ] + (hubUID ? [ + ] + (isHubitat() ? [ hsmStatus: state.hsmStatus ?: location.hsmStatus, colors: getColors() ] : [:]) @@ -2543,7 +2543,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 $err" } 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 } @@ -2642,7 +2642,7 @@ private Map capabilities() { 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"], ] - ] + (hubUID ? [ + ] + (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"], ], @@ -2650,7 +2650,7 @@ private Map capabilities() { ] : [:]) - if(hubUID){ + if(isHubitat()){ capabilities.remove('button') } @@ -2753,13 +2753,13 @@ private 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: "°", ], - ] + (hubUID ? [ + ] + (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(hubUID){ + if(isHubitat()){ attrs.remove('button') attrs.remove('holdableButton') } @@ -2769,7 +2769,7 @@ private Map attributes() { /* Push command has multiple overloads in hubitat */ public Map commandOverrides(){ - return (hubUID ? [ //s: command signature + 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 ] : [:]) @@ -2893,7 +2893,7 @@ private Map commands() { low : [ n: "Set to Low", ], med : [ n: "Set to Medium", ], high : [ n: "Set to High", ], - ] + (hubUID ? [ + ] + (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"]] ], @@ -2992,7 +2992,7 @@ private Map virtualCommands() { lifxScene: [n: "Activate LIFX scene", p: ["Scene:lifxScenes"], l: true, dd: "Activate LIFX Scene '{0}'", aggregated: true], ] : [:])*/ - if(hubUID){ + 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 @@ -3290,7 +3290,7 @@ private Map virtualDevices(updateCache = false) { 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] - ] + (hubUID ? [ + ] + (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], @@ -3444,3 +3444,7 @@ public List getColors(){ [name:"Yellow Green", rgb:"#9ACD32", h:80, s:61, l:50] ] } + +private isHubitat(){ + return hubUID != null +} From 2ef00db9be1b57adffceff8c323744fdfbfbca1c Mon Sep 17 00:00:00 2001 From: jp0550 Date: Sun, 26 Aug 2018 09:52:10 -0500 Subject: [PATCH 47/55] Fix ST Import format --- .../webcore-fuel-stream.groovy} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename smartapps/ady624/{webcore-fuelstream.src/webcore-fuelstream.groovy => webcore-fuel-stream.src/webcore-fuel-stream.groovy} (100%) diff --git a/smartapps/ady624/webcore-fuelstream.src/webcore-fuelstream.groovy b/smartapps/ady624/webcore-fuel-stream.src/webcore-fuel-stream.groovy similarity index 100% rename from smartapps/ady624/webcore-fuelstream.src/webcore-fuelstream.groovy rename to smartapps/ady624/webcore-fuel-stream.src/webcore-fuel-stream.groovy From f64c7e5676489cafc6e3346e940611f5114f61a2 Mon Sep 17 00:00:00 2001 From: jp0550 Date: Sun, 26 Aug 2018 14:51:53 -0500 Subject: [PATCH 48/55] Fix global variable subscriptions and add higher log limit --- smartapps/ady624/webcore-piston.src/webcore-piston.groovy | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/smartapps/ady624/webcore-piston.src/webcore-piston.groovy b/smartapps/ady624/webcore-piston.src/webcore-piston.groovy index 4e48d6d8..92075ae1 100644 --- a/smartapps/ady624/webcore-piston.src/webcore-piston.groovy +++ b/smartapps/ady624/webcore-piston.src/webcore-piston.groovy @@ -835,8 +835,8 @@ def getPistonLimits(){ executionTime: 30000, taskRemaining: 3000, taskDelayMax: 5000, - maxStats: settings.maxStats ?: 1, - maxLogs: settings.maxLogs ?: 1, + maxStats: settings.maxStats ?: 50, + maxLogs: settings.maxLogs ?: 50, recovery: 45 ] : [ schedule: 5000, @@ -4476,7 +4476,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 @@ -4592,7 +4592,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' From d043dd0042e14a8d7079c4e3d9003d3e4ed1e4c8 Mon Sep 17 00:00:00 2001 From: jp0550 Date: Sun, 26 Aug 2018 15:14:23 -0500 Subject: [PATCH 49/55] Fix method call --- smartapps/ady624/webcore-piston.src/webcore-piston.groovy | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/smartapps/ady624/webcore-piston.src/webcore-piston.groovy b/smartapps/ady624/webcore-piston.src/webcore-piston.groovy index 92075ae1..0ee6ac56 100644 --- a/smartapps/ady624/webcore-piston.src/webcore-piston.groovy +++ b/smartapps/ady624/webcore-piston.src/webcore-piston.groovy @@ -2971,7 +2971,7 @@ private long vcmd_wolRequest(rtData, device, params) { def secureCode = params[1] mac = mac.replace(":", "").replace("-", "").replace(".", "").replace(" ", "").toLowerCase() - sendHubCommand(HubActionClass.newInstance( + sendHubCommand(HubActionClass().newInstance( "wake on lan $mac", Protocol.LAN, null, @@ -3345,7 +3345,7 @@ private long vcmd_httpRequest(rtData, device, params) { query: useQueryString ? data : null, //thank you @destructure00 body: !useQueryString ? data : null //thank you @destructure00 ] - sendHubCommand(HubActionClass.newInstance(requestParams, null, [callback: localHttpRequestHandler])) + sendHubCommand(HubActionClass().newInstance(requestParams, null, [callback: localHttpRequestHandler])) return 20000 } catch (all) { error "Error executing internal web request: ", rtData, null, all From 3fea6a202abcfc8d4384d9e99793146d847cb7fa Mon Sep 17 00:00:00 2001 From: jp0550 Date: Sun, 26 Aug 2018 15:18:50 -0500 Subject: [PATCH 50/55] Fix NPE on Protocol --- .../ady624/webcore-piston.src/webcore-piston.groovy | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/smartapps/ady624/webcore-piston.src/webcore-piston.groovy b/smartapps/ady624/webcore-piston.src/webcore-piston.groovy index 0ee6ac56..ca66ff25 100644 --- a/smartapps/ady624/webcore-piston.src/webcore-piston.groovy +++ b/smartapps/ady624/webcore-piston.src/webcore-piston.groovy @@ -2973,7 +2973,7 @@ private long vcmd_wolRequest(rtData, device, params) { sendHubCommand(HubActionClass().newInstance( "wake on lan $mac", - Protocol.LAN, + HubProtocolClass().LAN, null, secureCode ? [secureCode: secureCode] : [:] )) @@ -8161,6 +8161,13 @@ private static Class HubActionClass() { 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 From c65cf36a2a4fe18cfa8b04712a3658564b6a23c1 Mon Sep 17 00:00:00 2001 From: jp0550 Date: Sun, 26 Aug 2018 23:12:49 -0500 Subject: [PATCH 51/55] Fix default fuel streams --- smartapps/ady624/webcore.src/webcore.groovy | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/smartapps/ady624/webcore.src/webcore.groovy b/smartapps/ady624/webcore.src/webcore.groovy index ad7b989a..277cdb42 100644 --- a/smartapps/ady624/webcore.src/webcore.groovy +++ b/smartapps/ady624/webcore.src/webcore.groovy @@ -1001,7 +1001,7 @@ private api_get_base_result(deviceVersion = 0, updateCache = false) { } private getFuelStreamUrls(iid){ - if(!settings.localFuelStreams){ + 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 ] @@ -1023,6 +1023,10 @@ private getFuelStreamUrls(iid){ ] } +private boolean useLocalFuelStreams(){ + return settings.localFuelStreams != null ? settings.localFuelStreams : (isHubitat() ? true : false) +} + private String transformHsmStatus(status){ switch(status){ case "disarmed": @@ -2226,7 +2230,7 @@ public Map getRunTimeData(semaphore = null, fetchWrappers = false) { generatedIn: now() - startTime, redirectContactBook: settings.redirectContactBook, logPistonExecutions: settings.logPistonExecutions, - useLocalFuelStreams : settings.localFuelStreams, + useLocalFuelStreams : useLocalFuelStreams(), waitedAtSemaphore : waited ] + (isHubitat() ? [ hsmStatus: state.hsmStatus ?: location.hsmStatus, From a02305b5e52be7022b327947b9ce1e100597eed2 Mon Sep 17 00:00:00 2001 From: jp0550 Date: Mon, 27 Aug 2018 22:09:32 -0500 Subject: [PATCH 52/55] Check null settings before migrating --- smartapps/ady624/webcore.src/webcore.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/smartapps/ady624/webcore.src/webcore.groovy b/smartapps/ady624/webcore.src/webcore.groovy index 277cdb42..6da3e7c1 100644 --- a/smartapps/ady624/webcore.src/webcore.groovy +++ b/smartapps/ady624/webcore.src/webcore.groovy @@ -1049,7 +1049,7 @@ private api_intf_dashboard_load() { recoveryHandler() //install storage app def storageApp = getStorageApp(true) - if(storageApp && isHubitat()){ //migrate off of storage app + 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 }]) } From c310be5180c0a1ea4e580d4cb4cb684bfd607e32 Mon Sep 17 00:00:00 2001 From: jp0550 Date: Mon, 27 Aug 2018 22:15:21 -0500 Subject: [PATCH 53/55] Add select all devices category --- smartapps/ady624/webcore.src/webcore.groovy | 1 + 1 file changed, 1 insertion(+) diff --git a/smartapps/ady624/webcore.src/webcore.groovy b/smartapps/ady624/webcore.src/webcore.groovy index 6da3e7c1..61531015 100644 --- a/smartapps/ady624/webcore.src/webcore.groovy +++ b/smartapps/ady624/webcore.src/webcore.groovy @@ -537,6 +537,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 } From af532c22fac461709b9cbd84f85188e9d9f5ca98 Mon Sep 17 00:00:00 2001 From: jp0550 Date: Tue, 28 Aug 2018 20:48:52 -0500 Subject: [PATCH 54/55] Add safer timeToday method for Hubitat --- .../ady624/webcore-piston.src/webcore-piston.groovy | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/smartapps/ady624/webcore-piston.src/webcore-piston.groovy b/smartapps/ady624/webcore-piston.src/webcore-piston.groovy index ca66ff25..a37ab429 100644 --- a/smartapps/ady624/webcore-piston.src/webcore-piston.groovy +++ b/smartapps/ady624/webcore-piston.src/webcore-piston.groovy @@ -7606,6 +7606,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 @@ -7632,7 +7642,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') From 1d7a94f1aeeb94730ca356660a4a730506c25d08 Mon Sep 17 00:00:00 2001 From: jp0550 Date: Tue, 4 Sep 2018 17:01:24 -0500 Subject: [PATCH 55/55] Don't fail on http exception --- smartapps/ady624/webcore.src/webcore.groovy | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/smartapps/ady624/webcore.src/webcore.groovy b/smartapps/ady624/webcore.src/webcore.groovy index 61531015..7ded48f3 100644 --- a/smartapps/ady624/webcore.src/webcore.groovy +++ b/smartapps/ady624/webcore.src/webcore.groovy @@ -2115,7 +2115,10 @@ private registerInstance() { } else { params << [contentType: 'application/json', requestContentType: 'application/json'] - httpPut(params) { res -> } + try{ + httpPut(params) { res -> } + } + catch(e) {} } }