Last active
November 19, 2019 22:28
-
-
Save MorningZ/608af549118a30079b471eb1372ba524 to your computer and use it in GitHub Desktop.
Hubitat "myQ Lite" app code (install 1st)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* ----------------------- | |
* ------ SMART APP ------ | |
* ----------------------- | |
* | |
* MyQ Lite | |
* | |
* Copyright 2019 Jason Mok/Brian Beaird/Barry Burke/RBoy Apps | |
* | |
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except | |
* in compliance with the License. You may obtain a copy of the License at: | |
* | |
* http://www.apache.org/licenses/LICENSE-2.0 | |
* | |
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed | |
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License | |
* for the specific language governing permissions and limitations under the License. | |
* | |
*/ | |
String appVersion() { return "3.1.0" } | |
String appModified() { return "2019-10-18"} | |
String appAuthor() { return "Brian Beaird" } | |
String gitBranch() { return "brbeaird" } | |
String getAppImg(imgName) { return "https://raw.githubusercontent.com/${gitBranch()}/SmartThings_MyQ/master/icons/$imgName" } | |
definition( | |
name: "MyQ Lite", | |
namespace: "brbeaird", | |
author: "Jason Mok/Brian Beaird/Barry Burke", | |
description: "Integrate MyQ with Smartthings", | |
category: "SmartThings Labs", | |
iconUrl: "https://raw.githubusercontent.com/brbeaird/SmartThings_MyQ/master/icons/myq.png", | |
iconX2Url: "https://raw.githubusercontent.com/brbeaird/SmartThings_MyQ/master/icons/[email protected]", | |
iconX3Url: "https://raw.githubusercontent.com/brbeaird/SmartThings_MyQ/master/icons/[email protected]" | |
) | |
preferences { | |
page(name: "mainPage", title: "MyQ Lite") | |
page(name: "prefLogIn", title: "MyQ") | |
page(name: "loginResultPage", title: "MyQ") | |
page(name: "prefListDevices", title: "MyQ") | |
page(name: "sensorPage", title: "MyQ") | |
page(name: "noDoorsSelected", title: "MyQ") | |
page(name: "summary", title: "MyQ") | |
page(name: "prefUninstall", title: "MyQ") | |
} | |
def appInfoSect(sect=true) { | |
def str = "" | |
str += "${app?.name}" | |
str += "\nAuthor: ${appAuthor()}" | |
section() { paragraph str, image: getAppImg("[email protected]") } | |
} | |
def mainPage() { | |
if (state.previousVersion == null){ | |
state.previousVersion = 0; | |
} | |
//Brand new install (need to grab version info) | |
if (!state.latestVersion){ | |
getVersionInfo(0, 0) | |
state.currentVersion = [:] | |
state.currentVersion['SmartApp'] = appVersion() | |
} | |
//Version updated | |
else if (appVersion() != state.previousVersion){ | |
state.previousVersion = appVersion() | |
getVersionInfo(state.previousVersion, appVersion()); | |
} | |
//If fresh install, go straight to login page | |
if (!settings.username){ | |
state.lastPage = "prefListDevices" | |
return prefLogIn() | |
} | |
state.lastPage = "mainPage" | |
dynamicPage(name: "mainPage", nextPage: "", uninstall: false, install: true) { | |
getVersionInfo(0,0) | |
appInfoSect() | |
def devs = refreshChildren() | |
section("MyQ Account"){ | |
paragraph title: "", "Email: ${settings.username}\n" | |
href "prefLogIn", title: "", description: "Tap to modify account", params: [nextPageName: "mainPage"] | |
} | |
section("Connected Devices") { | |
paragraph title: "", "${devs?.size() ? devs?.join("\n") : "No MyQ Devices Connected"}" | |
href "prefListDevices", title: "", description: "Tap to modify devices" | |
} | |
section("App and Handler Versions"){ | |
state.currentVersion.each { device, version -> | |
paragraph title: "", "${device} ${version} (${versionCompare(device)})" | |
} | |
input "prefUpdateNotify", "bool", required: false, title: "Notify when new version is available" | |
} | |
section("Uninstall") { | |
paragraph "Tap below to completely uninstall this SmartApp and devices (doors and lamp control devices will be force-removed from automations and SmartApps)" | |
href(name: "", title: "", description: "Tap to Uninstall", required: false, page: "prefUninstall") | |
} | |
} | |
} | |
def versionCompare(deviceName){ | |
if (!state.currentVersion || !state.latestVersion){return 'checking...'} | |
if (state.currentVersion[deviceName] == state.latestVersion[deviceName]){ | |
return 'latest' | |
} | |
else{ | |
return "${state.latestVersion[deviceName]} available" | |
} | |
} | |
def refreshChildren(){ | |
state.currentVersion = [:] | |
state.currentVersion['SmartApp'] = appVersion() | |
def devices = [] | |
childDevices.each { child -> | |
def myQId = child.getMyQDeviceId() ? "ID: ${child.getMyQDeviceId()}" : 'Missing MyQ ID' | |
def devName = child.name | |
if (child.typeName == "MyQ Garage Door Opener"){ | |
devName = devName + " (${child.currentContact}) ${myQId}" | |
state.currentVersion['DoorDevice'] = child.showVersion() | |
} | |
else if (child.typeName == "MyQ Garage Door Opener-NoSensor"){ | |
devName = devName + " (No sensor) ${myQId}" | |
state.currentVersion['DoorDeviceNoSensor'] = child.showVersion() | |
} | |
else if (child.typeName == "MyQ Light Controller"){ | |
devName = devName + " (${child.currentSwitch}) ${myQId}" | |
state.currentVersion['LightDevice'] = child.showVersion() | |
} | |
else{ | |
return //Ignore push-button devices | |
} | |
devices.push(devName) | |
} | |
return devices | |
} | |
/* Preferences */ | |
def prefLogIn(params) { | |
state.installMsg = "" | |
def showUninstall = username != null && password != null | |
return dynamicPage(name: "prefLogIn", title: "Connect to MyQ", nextPage:"loginResultPage", uninstall:false, install: false, submitOnChange: true) { | |
section("Login Credentials"){ | |
input("username", "email", title: "Username", description: "MyQ Username (email address)") | |
input("password", "password", title: "Password", description: "MyQ password") | |
} | |
} | |
} | |
def loginResultPage(){ | |
log.debug "login result next page: ${state.lastPage}" | |
if (forceLogin()) { | |
if (state.lastPage == "prefListDevices") | |
return prefListDevices() | |
else | |
return mainPage() | |
} | |
else{ | |
return dynamicPage(name: "loginResultPage", title: "Login Error", install:false, uninstall:false) { | |
section(""){ | |
paragraph "The username or password you entered is incorrect. Go back and try again. " | |
} | |
} | |
} | |
} | |
def prefUninstall() { | |
log.debug "Removing MyQ Devices..." | |
def msg = "" | |
childDevices.each { | |
try{ | |
deleteChildDevice(it.deviceNetworkId, true) | |
msg = "Devices have been removed. Tap remove to complete the process." | |
} | |
catch (e) { | |
log.debug "Error deleting ${it.deviceNetworkId}: ${e}" | |
msg = "There was a problem removing your device(s). Check the IDE logs for details." | |
} | |
} | |
return dynamicPage(name: "prefUninstall", title: "Uninstall", install:false, uninstall:true) { | |
section("Uninstallation"){ | |
paragraph msg | |
} | |
} | |
} | |
def getDeviceSelectionList(deviceType){ | |
def testing | |
} | |
def prefListDevices() { | |
state.lastPage = "prefListDevices" | |
if (login()) { | |
getMyQDevices() | |
state.doorList = [:] | |
state.lightList = [:] | |
state.MyQDataPending.each { id, device -> | |
if (device.typeName == 'door'){ | |
state.doorList[id] = device.name | |
} | |
else if (device.typeName == 'light'){ | |
state.lightList[id] = device.name | |
} | |
} | |
if ((state.doorList) || (state.lightList)){ | |
def nextPage = "sensorPage" | |
if (!state.doorList){nextPage = "summary"} //Skip to summary if there are no doors to handle | |
return dynamicPage(name: "prefListDevices", title: "Devices", nextPage:nextPage, install:false, uninstall:false) { | |
if (state.doorList) { | |
section("Select which garage door/gate to use"){ | |
input(name: "doors", type: "enum", required:false, multiple:true, options:state.doorList) | |
} | |
} | |
if (state.lightList) { | |
section("Select which lights to use"){ | |
input(name: "lights", type: "enum", required:false, multiple:true, options:state.lightList) | |
} | |
} | |
section("Advanced (optional)", hideable: true, hidden:true){ | |
paragraph "BETA: Enable the below option if you would like to force the Garage Doors to behave as Door Locks (sensor required)." + | |
"This may be desirable if you only want doors to open up via PIN with Alexa voice commands. " + | |
"Note this is still considered highly experimental and may break many other automations/apps that need the garage door capability." | |
input "prefUseLockType", "bool", required: false, title: "Create garage doors as door locks?" | |
} | |
} | |
}else { | |
return dynamicPage(name: "prefListDevices", title: "Error!", install:false, uninstall:true) { | |
section(""){ | |
paragraph "Could not find any supported device(s). Please report to author about these devices: " + state.unsupportedList | |
} | |
} | |
} | |
} else { | |
return prefLogIn([nextPageName: "prefListDevices"]) | |
} | |
} | |
def sensorPage() { | |
//If MyQ ID changes, the old stale ID will still be listed in the settings array. Let's get a clean count of valid doors selected | |
state.validatedDoors = [] | |
if (doors instanceof List && doors.size() > 1){ | |
doors.each { | |
if (state.MyQDataPending[it] != null){ | |
state.validatedDoors.add(it) | |
} | |
} | |
} | |
else{ | |
state.validatedDoors = doors //Handle single door | |
} | |
return dynamicPage(name: "sensorPage", title: "Optional Sensors and Push Buttons", nextPage:"summary", install:false, uninstall:false) { | |
def sensorCounter = 1 | |
state.validatedDoors.each{ door -> | |
section("Setup options for " + state.MyQDataPending[door].name){ | |
input "door${sensorCounter}Sensor", "capability.contactSensor", required: false, multiple: false, title: state.MyQDataPending[door].name + " Contact Sensor" | |
input "prefDoor${sensorCounter}PushButtons", "bool", required: false, title: "Create on/off push buttons?" | |
} | |
sensorCounter++ | |
} | |
section("Sensor setup"){ | |
paragraph "For each door above, you can specify an optional sensor that allows the device type to know whether the door is open or closed. This helps the device function as a switch " + | |
"you can turn on (to open) and off (to close) in other automations and SmartApps." | |
paragraph "Alternatively, you can choose the other option below to have separate additional On and Off push button devices created. This is recommened if you have no sensors but still want a way to open/close the " + | |
"garage from SmartTiles and other interfaces like Google Home that can't function with the built-in open/close capability. See wiki for more details" | |
} | |
} | |
} | |
def summary() { | |
state.installMsg = "" | |
try{ | |
initialize() | |
} | |
//If error thrown during initialize, try to get the line number and display on installation summary page | |
catch (e){ | |
def errorLine = "unknown" | |
try{ | |
log.debug e.stackTrace | |
def pattern = ( e.stackTrace =~ /groovy.(\d+)./ ) | |
errorLine = pattern[0][1] | |
} | |
catch(lineError){} | |
log.debug "Error at line number ${errorLine}: ${e}" | |
state.installMsg = "There was a problem updating devices:\n ${e}.\nLine number: ${errorLine}\nLast successful step: ${state.lastSuccessfulStep}" | |
} | |
return dynamicPage(name: "summary", title: "Summary", install:true, uninstall:true) { | |
section("Installation Details:"){ | |
paragraph state.installMsg | |
} | |
} | |
} | |
/* Initialization */ | |
def installed() { | |
} | |
def updated() { | |
log.debug "MyQ Lite changes saved." | |
unschedule() | |
runEvery3Hours(updateVersionInfo) //Check for new version every 3 hours | |
if (door1Sensor && state.validatedDoors){ | |
refreshAll() | |
runEvery30Minutes(refreshAll) | |
} | |
stateCleanup() | |
} | |
/* Version Checking */ | |
//Called from scheduler every 3 hours | |
def updateVersionInfo(){ | |
getVersionInfo('versionCheck', '0') | |
} | |
//Get latest versions for SmartApp and Device Handlers | |
def getVersionInfo(oldVersion, newVersion){ | |
//Don't check for updates more 5 minutes | |
if (state.lastVersionCheck && (now() - state.lastVersionCheck) / 1000/60 < 5 ){ | |
return | |
} | |
state.lastVersionCheck = now() | |
log.info "Checking for latest version..." | |
def params = [ | |
uri: 'http://www.brbeaird.com/getVersion/myq/' + oldVersion + '/' + newVersion, | |
contentType: 'application/json' | |
] | |
def callbackMethod = oldVersion == 'versionCheck' ? 'updateCheck' : 'handleVersionUpdateResponse' | |
asynchttpGet(callbackMethod, params) | |
} | |
//When version response received (async), update state with the data | |
def handleVersionUpdateResponse(response, data) { | |
if (response.hasError()) { | |
log.error "Error getting version info: ${response.errorMessage}" | |
} | |
else {state.latestVersion = response.json} | |
} | |
//In case of periodic update check, also refresh installed versions and update the version warning message | |
def updateCheck(response, data) { | |
handleVersionUpdateResponse(response,data) | |
refreshChildren() | |
updateVersionMessage() | |
} | |
def updateVersionMessage(){ | |
state.versionMsg = "" | |
state.currentVersion.each { device, version -> | |
if (versionCompare(device) != 'latest'){ | |
state.versionMsg = "MyQ Lite Updates are available." | |
} | |
} | |
//Notify if updates are available | |
if (state.versionMsg != ""){ | |
//Send push notification if enabled | |
if (prefUpdateNotify){ | |
//Don't notify if we've sent a notification within the last 1 day | |
if (state.lastVersionNotification){ | |
def timeSinceLastNotification = (now() - state.lastVersionNotification) / 1000 | |
if (timeSinceLastNotification < 60*60*23){ | |
return | |
} | |
} | |
state.lastVersionNotification = now() | |
} | |
} | |
} | |
def uninstall(){ | |
log.debug "Removing MyQ Devices..." | |
childDevices.each { | |
try{ | |
deleteChildDevice(it.deviceNetworkId, true) | |
} | |
catch (e) { | |
log.debug "Error deleting ${it.deviceNetworkId}: ${e}" | |
} | |
} | |
} | |
def uninstalled() { | |
log.debug "MyQ removal complete." | |
getVersionInfo(state.previousVersion, 0); | |
} | |
def initialize() { | |
log.debug "Initializing..." | |
state.data = state.MyQDataPending | |
state.lastSuccessfulStep = "" | |
unsubscribe() | |
//Check existing installed devices against MyQ data | |
verifyChildDeviceIds() | |
//Mark sensors onto state door data | |
def doorSensorCounter = 1 | |
state.validatedDoors.each{ door -> | |
if (settings["door${doorSensorCounter}Sensor"]){ | |
state.data[door].sensor = "door${doorSensorCounter}Sensor" | |
doorSensorCounter++ | |
} | |
} | |
state.lastSuccessfulStep = "Sensor Indexing" | |
//Create door devices | |
def doorCounter = 1 | |
state.validatedDoors.each{ door -> | |
createChilDevices(door, settings[state.data[door].sensor], state.data[door].name, settings["prefDoor${doorCounter}PushButtons"]) | |
doorCounter++ | |
} | |
state.lastSuccessfulStep = "Door device creation" | |
//Create light devices | |
if (lights){ | |
state.validatedLights = [] | |
if (lights instanceof List && lights.size() > 1){ | |
lights.each { lightId -> | |
if (state.data[lightId] != null){ | |
state.validatedLights.add(lightId) | |
} | |
} | |
} | |
else{ | |
state.validatedLights = lights | |
} | |
state.validatedLights.each { light -> | |
if (light){ | |
def myQDeviceId = state.data[light].myQDeviceId | |
def DNI = [ app.id, "LightController", myQDeviceId ].join('|') | |
def lightName = state.data[light].name | |
def childLight = getChildDevice(state.data[light].child) | |
if (!childLight) { | |
log.debug "Creating child light device: " + light | |
try{ | |
childLight = addChildDevice("brbeaird", "MyQ Light Controller", DNI, getHubID(), ["name": lightName]) | |
state.data[myQDeviceId].child = DNI | |
state.installMsg = state.installMsg + lightName + ": created light device. \r\n\r\n" | |
} | |
catch(com.hubitat.app.exception.UnknownDeviceTypeException e) | |
{ | |
log.debug "Error! " + e | |
state.installMsg = state.installMsg + lightName + ": problem creating light device. Check your IDE to make sure the brbeaird : MyQ Light Controller device handler is installed and published. \r\n\r\n" | |
} | |
} | |
else{ | |
log.debug "Light device already exists: " + lightName | |
state.installMsg = state.installMsg + lightName + ": light device already exists. \r\n\r\n" | |
} | |
log.debug "Setting ${lightName} status to ${state.data[light].status}" | |
childLight.updateDeviceStatus(state.data[light].status) | |
} | |
} | |
state.lastSuccessfulStep = "Light device creation" | |
} | |
// Remove unselected devices | |
getChildDevices().each{ child -> | |
log.debug "Checking ${child} for deletion" | |
def myQDeviceId = child.getMyQDeviceId() | |
if (myQDeviceId){ | |
if (!(myQDeviceId in state.validatedDoors) && !(myQDeviceId in state.validatedLights)){ | |
try{ | |
log.debug "Child ${child} with ID ${myQDeviceId} not found in selected list. Deleting." | |
deleteChildDevice(child.deviceNetworkId, true) | |
log.debug "Removed old device: ${child}" | |
state.installMsg = state.installMsg + "Removed old device: ${child} \r\n\r\n" | |
} | |
catch (e) | |
{ | |
log.debug "Error trying to delete device: ${child} - ${e}" | |
log.debug "Device is likely in use in a Routine, or SmartApp (make sure and check Alexa, ActionTiles, etc.)." | |
} | |
} | |
} | |
} | |
state.lastSuccessfulStep = "Old device removal" | |
//Set initial values | |
if (state.validatedDoors){ | |
syncDoorsWithSensors() | |
} | |
state.lastSuccessfulStep = "Setting initial values" | |
//Subscribe to sensor events | |
settings.each{ key, val-> | |
if (key.contains('Sensor')){ | |
subscribe(val, "contact", sensorHandler) | |
} | |
} | |
} | |
def verifyChildDeviceIds(){ | |
//Try to match existing child devices with latest MyQ data | |
childDevices.each { child -> | |
def matchingId | |
if (child.typeName != 'Momentary Button Tile'){ | |
//Look for a matching entry in MyQ | |
state.data.each { myQId, myQData -> | |
if (child.getMyQDeviceId() == myQId){ | |
log.debug "Found matching ID for ${child}" | |
matchingId = myQId | |
} | |
//If no matching ID, try to match on name | |
else if (child.name == myQData.name || child.label == myQData.name){ | |
log.debug "Found matching ID (via name) for ${child}" | |
child.updateMyQDeviceId(myQId) //Update child to new ID | |
matchingId = myQId | |
} | |
} | |
log.debug "final matchingid for ${child.name} ${matchingId}" | |
if (matchingId){ | |
state.data[matchingId].child = child.deviceNetworkId | |
} | |
else{ | |
log.debug "WARNING: Existing child ${child} does not seem to have a valid MyQID" | |
} | |
} | |
} | |
} | |
def createChilDevices(door, sensor, doorName, prefPushButtons){ | |
def sensorTypeName = "MyQ Garage Door Opener" | |
def noSensorTypeName = "MyQ Garage Door Opener-NoSensor" | |
def lockTypeName = "MyQ Lock Door" | |
if (door){ | |
def myQDeviceId = state.data[door].myQDeviceId | |
def DNI = [ app.id, "GarageDoorOpener", myQDeviceId ].join('|') | |
//Has door's child device already been created? | |
def existingDev = getChildDevice(state.data[door].child) | |
def existingType = existingDev?.typeName | |
if (existingDev){ | |
log.debug "Child already exists for " + doorName + ". Sensor name is: " + sensor | |
state.installMsg = state.installMsg + doorName + ": door device already exists. \r\n\r\n" | |
if (prefUseLockType && existingType != lockTypeName){ | |
try{ | |
log.debug "Type needs updating to Lock version" | |
existingDev.deviceType = lockTypeName | |
state.installMsg = state.installMsg + doorName + ": changed door device to lock version." + "\r\n\r\n" | |
} | |
catch(hubitat.exception.NotFoundException e) | |
{ | |
log.debug "Error! " + e | |
state.installMsg = state.installMsg + doorName + ": problem changing door to no-sensor type. Check your IDE to make sure the brbeaird : " + lockTypeName + " device handler is installed and published. \r\n\r\n" | |
} | |
} | |
else if ((!sensor) && existingType != noSensorTypeName){ | |
try{ | |
log.debug "Type needs updating to no-sensor version" | |
existingDev.deviceType = noSensorTypeName | |
state.installMsg = state.installMsg + doorName + ": changed door device to No-sensor version." + "\r\n\r\n" | |
} | |
catch(hubitat.exception.NotFoundException e) | |
{ | |
log.debug "Error! " + e | |
state.installMsg = state.installMsg + doorName + ": problem changing door to no-sensor type. Check your IDE to make sure the brbeaird : " + noSensorTypeName + " device handler is installed and published. \r\n\r\n" | |
} | |
} | |
else if (sensor && existingType != sensorTypeName && !prefUseLockType){ | |
try{ | |
log.debug "Type needs updating to sensor version" | |
existingDev.deviceType = sensorTypeName | |
state.installMsg = state.installMsg + doorName + ": changed door device to sensor version." + "\r\n\r\n" | |
} | |
catch(hubitat.exception.NotFoundException e) | |
{ | |
log.debug "Error! " + e | |
state.installMsg = state.installMsg + doorName + ": problem changing door to sensor type. Check your IDE to make sure the brbeaird : " + sensorTypeName + " device handler is installed and published. \r\n\r\n" | |
} | |
} | |
} | |
else{ | |
log.debug "Creating child door device " + door | |
def childDoor | |
if (prefUseLockType){ | |
try{ | |
log.debug "Creating door with lock type" | |
childDoor = addChildDevice("brbeaird", lockTypeName, DNI, getHubID(), ["name": doorName]) | |
childDoor.updateMyQDeviceId(myQDeviceId) | |
state.installMsg = state.installMsg + doorName + ": created lock device \r\n\r\n" | |
} | |
catch(com.hubitat.app.exception.UnknownDeviceTypeException e) | |
{ | |
log.debug "Error! " + e | |
state.installMsg = state.installMsg + doorName + ": problem creating door device (lock type). Check your IDE to make sure the brbeaird : " + sensorTypeName + " device handler is installed and published. \r\n\r\n" | |
} | |
} | |
else if (sensor){ | |
try{ | |
log.debug "Creating door with sensor" | |
childDoor = addChildDevice("brbeaird", sensorTypeName, DNI, getHubID(), ["name": doorName]) | |
childDoor.updateMyQDeviceId(myQDeviceId) | |
state.installMsg = state.installMsg + doorName + ": created door device (sensor version) \r\n\r\n" | |
} | |
catch(com.hubitat.app.exception.UnknownDeviceTypeException e) | |
{ | |
log.debug "Error! " + e | |
state.installMsg = state.installMsg + doorName + ": problem creating door device (sensor type). Check your IDE to make sure the brbeaird : " + sensorTypeName + " device handler is installed and published. \r\n\r\n" | |
} | |
} | |
else{ | |
try{ | |
log.debug "Creating door with no sensor" | |
childDoor = addChildDevice("brbeaird", noSensorTypeName, DNI, getHubID(), ["name": doorName]) | |
childDoor.updateMyQDeviceId(myQDeviceId) | |
state.installMsg = state.installMsg + doorName + ": created door device (no-sensor version) \r\n\r\n" | |
} | |
catch(com.hubitat.app.exception.UnknownDeviceTypeException e) | |
{ | |
log.debug "Error! " + e | |
state.installMsg = state.installMsg + doorName + ": problem creating door device (no-sensor type). Check your IDE to make sure the brbeaird : " + noSensorTypeName + " device handler is installed and published. \r\n\r\n" | |
} | |
} | |
state.data[door].child = childDoor.deviceNetworkId | |
} | |
//Create push button devices | |
if (prefPushButtons){ | |
def existingOpenButtonDev = getChildDevice(door + " Opener") | |
def existingCloseButtonDev = getChildDevice(door + " Closer") | |
if (!existingOpenButtonDev){ | |
try{ | |
def openButton = addChildDevice("smartthings", "Momentary Button Tile", door + " Opener", getHubID(), [name: doorName + " Opener", label: doorName + " Opener"]) | |
state.installMsg = state.installMsg + doorName + ": created push button device. \r\n\r\n" | |
subscribe(openButton, "momentary.pushed", doorButtonOpenHandler) | |
} | |
catch(com.hubitat.app.exception.UnknownDeviceTypeException e) | |
{ | |
log.debug "Error! " + e | |
state.installMsg = state.installMsg + doorName + ": problem creating push button device. Check your IDE to make sure the smartthings : Momentary Button Tile device handler is installed and published. \r\n\r\n" | |
} | |
} | |
else{ | |
subscribe(existingOpenButtonDev, "momentary.pushed", doorButtonOpenHandler) | |
state.installMsg = state.installMsg + doorName + ": push button device already exists. Subscription recreated. \r\n\r\n" | |
log.debug "subscribed to button: " + existingOpenButtonDev | |
} | |
if (!existingCloseButtonDev){ | |
try{ | |
def closeButton = addChildDevice("smartthings", "Momentary Button Tile", door + " Closer", getHubID(), [name: doorName + " Closer", label: doorName + " Closer"]) | |
subscribe(closeButton, "momentary.pushed", doorButtonCloseHandler) | |
} | |
catch(com.hubitat.app.exception.UnknownDeviceTypeException e) | |
{ | |
log.debug "Error! " + e | |
} | |
} | |
else{ | |
subscribe(existingCloseButtonDev, "momentary.pushed", doorButtonCloseHandler) | |
} | |
} | |
//Cleanup defunct push button devices if no longer wanted | |
else{ | |
def pushButtonIDs = [door + " Opener", door + " Closer"] | |
def devsToDelete = getChildDevices().findAll { pushButtonIDs.contains(it.deviceNetworkId)} | |
log.debug "button devices to delete: " + devsToDelete | |
devsToDelete.each{ | |
log.debug "deleting button: " + it | |
try{ | |
deleteChildDevice(it.deviceNetworkId, true) | |
} catch (e){ | |
state.installMsg = state.installMsg + "Warning: unable to delete virtual on/off push button - you'll need to manually remove it. \r\n\r\n" | |
log.debug "Error trying to delete button " + it + " - " + e | |
log.debug "Button is likely in use in a Routine, or SmartApp (make sure and check SmarTiles!)." | |
} | |
} | |
} | |
} | |
} | |
def syncDoorsWithSensors(child){ | |
// refresh only the requesting door (makes things a bit more efficient if you have more than 1 door | |
/*if (child) { | |
def doorMyQId = child.getMyQDeviceId() | |
updateDoorStatus(child.device.deviceNetworkId, settings[state.data[doorMyQId].sensor], child) | |
} | |
//Otherwise, refresh everything | |
else{*/ | |
state.validatedDoors.each { door -> | |
log.debug "Refreshing ${door} ${state.data[door].child}" | |
if (state.data[door].sensor){ | |
updateDoorStatus(state.data[door].child, settings[state.data[door].sensor], '') | |
} | |
} | |
//} | |
} | |
def updateDoorStatus(doorDNI, sensor, child){ | |
try{ | |
log.debug "Updating door status: ${doorDNI} ${sensor} ${child}" | |
if (!sensor){//If we got here somehow without a sensor, bail out | |
log.debug "Warning: no sensor found for ${doorDNI}" | |
return 0} | |
if (!doorDNI){ | |
log.debug "Invalid doorDNI for sensor ${sensor} ${child}" | |
return 0 | |
} | |
//Get door to update and set the new value | |
def doorToUpdate = getChildDevice(doorDNI) | |
def doorName = "unknown" | |
if (state.data[doorDNI]){doorName = state.data[doorDNI].name} | |
//Get current sensor value | |
def currentSensorValue = "unknown" | |
currentSensorValue = sensor.latestValue("contact") | |
def currentDoorState = doorToUpdate.latestValue("contact") | |
//If sensor and door are out of sync, update the door | |
if (currentDoorState != currentSensorValue){ | |
log.debug "Updating ${doorName} as ${currentSensorValue} from sensor ${sensor}" | |
doorToUpdate.updateDeviceStatus(currentSensorValue) | |
doorToUpdate.updateDeviceSensor("${sensor} is ${currentSensorValue}") | |
//Write to child log if this was initiated from one of the doors | |
if (child){child.log("Updating as ${currentSensorValue} from sensor ${sensor}")} | |
//Get latest activity timestamp for the sensor (data saved for up to a week) | |
def eventsSinceYesterday = sensor.eventsSince(new Date() - 7) | |
def latestEvent = eventsSinceYesterday[0]?.date | |
//Update timestamp | |
if (latestEvent){ | |
doorToUpdate.updateDeviceLastActivity(latestEvent) | |
} | |
else{ //If the door has been inactive for more than a week, timestamp data will be null. Keep current value in that case. | |
timeStampLogText = "Door: " + doorName + ": Null timestamp detected " + " - from sensor " + sensor + " . Keeping current value." | |
} | |
} | |
}catch (e) { | |
log.debug "Error updating door: ${doorDNI}: ${e}" | |
} | |
} | |
def refresh(child){ | |
def door = child.device.deviceNetworkId | |
def doorName = state.data[child.getMyQDeviceId()].name | |
child.log("refresh called from " + doorName + ' (' + door + ')') | |
syncDoorsWithSensors(child) | |
} | |
def refreshAll(){ | |
syncDoorsWithSensors() | |
} | |
def refreshAll(evt){ | |
refreshAll() | |
} | |
def sensorHandler(evt) { | |
log.debug "Sensor change detected: Event name " + evt.name + " value: " + evt.value + " deviceID: " + evt.deviceId | |
state.validatedDoors.each{ door -> | |
if (settings[state.data[door].sensor]?.id == evt.deviceId) | |
updateDoorStatus(state.data[door].child, settings[state.data[door].sensor], null) | |
} | |
} | |
def doorButtonOpenHandler(evt) { | |
try{ | |
log.debug "Door open button push detected: Event name " + evt.name + " value: " + evt.value + " deviceID: " + evt.deviceId + " DNI: " + evt.getDevice().deviceNetworkId | |
def myQDeviceId = evt.getDevice().deviceNetworkId.replace(" Opener", "") | |
def doorDevice = getChildDevice(state.data[myQDeviceId].child) | |
log.debug "Opening door." | |
doorDevice.openPrep() | |
sendCommand(myQDeviceId, "open") | |
}catch(e){ | |
def errMsg = "Warning: MyQ Open button command failed - ${e}" | |
log.error errMsg | |
} | |
} | |
def doorButtonCloseHandler(evt) { | |
try{ | |
log.debug "Door close button push detected: Event name " + evt.name + " value: " + evt.value + " deviceID: " + evt.deviceId + " DNI: " + evt.getDevice().deviceNetworkId | |
def myQDeviceId = evt.getDevice().deviceNetworkId.replace(" Closer", "") | |
def doorDevice = getChildDevice(state.data[myQDeviceId].child) | |
log.debug "Closing door." | |
doorDevice.closePrep() | |
sendCommand(myQDeviceId, "close") | |
}catch(e){ | |
def errMsg = "Warning: MyQ Close button command failed - ${e}" | |
log.error errMsg | |
} | |
} | |
def getSelectedDevices( settingsName ) { | |
def selectedDevices = [] | |
(!settings.get(settingsName))?:((settings.get(settingsName)?.getAt(0)?.size() > 1) ? settings.get(settingsName)?.each { selectedDevices.add(it) } : selectedDevices.add(settings.get(settingsName))) | |
return selectedDevices | |
} | |
/* Access Management */ | |
private forceLogin() { | |
//Reset token and expiry | |
log.warn "forceLogin: Refreshing login token" | |
state.session = [ brandID: 0, brandName: settings.brand, securityToken: null, expiration: 0 ] | |
return doLogin() | |
} | |
private login() { | |
if (now() > state.session.expiration){ | |
log.warn "Token has expired. Logging in again." | |
doLogin() | |
} | |
else{ | |
return true; | |
} | |
} | |
private doLogin() { | |
def result = false | |
apiPostLogin("/api/v5/Login", "{\"username\":\"${settings.username}\",\"password\": \"${settings.password}\"}" ) { response -> | |
if (response.data.SecurityToken != null) { | |
state.session.securityToken = response.data.SecurityToken | |
state.session.expiration = now() + (5*60*1000) // 5 minutes default | |
//Now get account ID | |
return apiGet(getAccountIdURL(), [expand: "account"]) { acctResponse -> | |
if (acctResponse.status == 200) { | |
state.session.accountId = acctResponse.data.Account.Id | |
log.debug "got accountid ${acctResponse.data.Account.Id}" | |
result = true | |
} | |
else{ | |
log.warn "Failed to get AccountId, login unsuccessful" | |
result = false | |
} | |
} | |
result = true | |
} else { | |
log.warn "No security token found, login unsuccessful" | |
state.session = [ brandID: 0, brandName: settings.brand, securityToken: null, expiration: 0 ] // Reset token and expiration | |
result = false | |
} | |
} | |
return result | |
} | |
//Get devices listed on your MyQ account | |
private getMyQDevices() { | |
state.MyQDataPending = [:] | |
state.unsupportedList = [] | |
apiGet(getDevicesURL(), [:]) { response -> | |
if (response.status == 200) { | |
response.data.items.each { device -> | |
// 2 = garage door, 5 = gate, 7 = MyQGarage(no gateway), 9 = commercial door, 17 = Garage Door Opener WGDO | |
//if (device.MyQDeviceTypeId == 2||device.MyQDeviceTypeId == 5||device.MyQDeviceTypeId == 7||device.MyQDeviceTypeId == 17||device.MyQDeviceTypeId == 9) { | |
if (device.device_family == "garagedoor") { | |
log.debug "Found door: ${device.name}" | |
def dni = device.serial_number | |
def description = device.name | |
def doorState = device.state.door_state | |
def updatedTime = device.last_update | |
//def dni = device.MyQDeviceId | |
//def description = '' | |
//def doorState = '' | |
//def updatedTime = '' | |
/*device.Attributes.each { | |
if (it.AttributeDisplayName=="desc") | |
{ | |
description = it.Value | |
} | |
if (it.AttributeDisplayName=="doorstate") { | |
doorState = it.Value | |
updatedTime = it.UpdatedTime | |
} | |
} | |
//Sometimes MyQ has duplicates. Check and see if we've seen this door before | |
def doorToRemove = "" | |
state.MyQDataPending.each { doorDNI, door -> | |
if (door.name == description){ | |
log.debug "Duplicate door detected. Checking to see if this one is newer..." | |
//If this instance is newer than the duplicate, pull the older one back out of the array | |
if (door.lastAction < updatedTime){ | |
log.debug "Yep, this one is newer." | |
doorToRemove = door | |
} | |
//If this door is the older one, clear out the description so it will be ignored | |
else{ | |
log.debug "Nope, this one is older. Stick with what we've got." | |
description = "" | |
} | |
} | |
} | |
if (doorToRemove){ | |
log.debug "Removing older duplicate." | |
state.MyQDataPending.remove(door) | |
}*/ | |
//Ignore any doors with blank descriptions | |
if (description != ''){ | |
log.debug "Got valid door: ${description} type: ${device.device_family} status: ${doorState} type: ${device.device_type}" | |
//log.debug "Storing door info: " + description + "type: " + device.device_family + " status: " + doorState + " type: " + device.device_type | |
state.MyQDataPending[dni] = [ status: doorState, lastAction: updatedTime, name: description, typeId: device.MyQDeviceTypeId, typeName: 'door', sensor: '', myQDeviceId: device.serial_number] | |
} | |
else{ | |
log.debug "Door " + device.MyQDeviceId + " has blank desc field. This is unusual..." | |
} | |
} | |
//Lights | |
else if (device.device_family == "lamp") { | |
def dni = device.serial_number | |
def description = device.name | |
def lightState = device.state.lamp_state | |
def updatedTime = device.state.last_update | |
/* | |
device.Attributes.each { | |
if (it.AttributeDisplayName=="desc") | |
{ | |
description = it.Value | |
} | |
if (it.AttributeDisplayName=="lightstate") { | |
lightState = it.Value | |
updatedTime = it.UpdatedTime | |
} | |
}*/ | |
//Ignore any lights with blank descriptions | |
if (description && description != ''){ | |
log.debug "Got valid light: ${description} type: ${device.device_family} status: ${lightState} type: ${device.device_type}" | |
state.MyQDataPending[dni] = [ status: lightState, lastAction: updatedTime, name: description, typeName: 'light', type: device.MyQDeviceTypeId, myQDeviceId: device.serial_number ] | |
} | |
} | |
//Unsupported devices | |
else{ | |
state.unsupportedList.add([name: device.name, typeId: device.device_family, typeName: device.device_type]) | |
} | |
} | |
} | |
} | |
} | |
def getHubID(){ | |
return 1234 | |
} | |
/* API Methods */ | |
private getDevicesURL(){ | |
return "/api/v5.1/Accounts/${state.session.accountId}/Devices" | |
} | |
private getAccountIdURL(){ | |
return "/api/v5/My" | |
} | |
import groovy.transform.Field | |
@Field final MAX_RETRIES = 1 // Retry count before giving up | |
// get URL | |
private getApiURL() { | |
return "https://api.myqdevice.com" | |
} | |
private getApiAppID() { | |
return "JVM/G9Nwih5BwKgNCjLxiFUQxQijAebyyg8QUHr7JOrP+tuPb8iHfRHKwTmDzHOu" | |
} | |
private getMyQHeaders() { | |
return [ | |
"SecurityToken": state.session.securityToken, | |
"MyQApplicationId": getApiAppID(), | |
"Content-Type": "application/json" | |
] | |
} | |
// HTTP GET call (Get Devices) | |
private apiGet(apiPath, apiQuery = [], callback = {}) { | |
if (!login()){ | |
log.error "Unable to complete GET, login failed" | |
return | |
} | |
try { | |
//log.debug "API Callout: GET ${getApiURL()}${apiPath} headers: ${getMyQHeaders()}" | |
httpGet([ uri: getApiURL(), path: apiPath, headers: getMyQHeaders(), query: apiQuery ]) { response -> | |
log.debug "called" | |
def result = isGoodResponse(response) | |
log.debug "Got result: ${result}" | |
if (result == 0) { | |
callback(response) | |
} | |
/*else if (result == 1){ | |
apiGet(apiPath, apiQuery, callback) // Try again | |
}*/ | |
} | |
} catch (e) { | |
log.error "API GET Error: $e" | |
} | |
} | |
// HTTP PUT call (Send commands) | |
private apiPut(apiPath, apiBody = [], actionText = "") { | |
if (!login()){ | |
log.error "Unable to complete PUT, login failed" | |
return | |
} | |
try { | |
//log.debug "Calling out PUT ${getApiURL()}${apiPath}${apiBody} ${getMyQHeaders()}" | |
httpPut([ uri: getApiURL(), path: apiPath, contentType: "application/json; charset=utf-8", headers: getMyQHeaders(), body: apiBody ]) { response -> | |
def result = isGoodResponse(response) | |
if (result == 0) { | |
return | |
} | |
else if (result == 1){ | |
apiPut(apiPath, apiBody, callback) // Try again | |
} | |
} | |
} catch (e) { | |
log.error "API PUT Error: $e" | |
} | |
} | |
//Check response and retry login if needed | |
def isGoodResponse(response){ | |
log.debug "Got response: STATUS: ${response.status}" | |
//Good response | |
if (response.status == 200 || response.status == 204) { | |
state.retryCount = 0 // Reset it | |
return 0 | |
} | |
//Bad token response | |
else if(response.status == 401){ | |
if (state.retryCount <= MAX_RETRIES) { | |
state.retryCount = (state.retryCount ?: 0) + 1 | |
log.warn "GET: Login expired, logging in again" | |
if (forceLogin()){ | |
returnCode = 1 | |
log.warn "GET: Re-login successful." | |
} | |
else{ | |
returnCode = -1 | |
log.warn "GET: Re-login failed." | |
} | |
} else { | |
log.warn "Too many retries, dropping request" | |
} | |
} | |
//Unknown response | |
else{ | |
log.error "Unknown status: ${response.status} ${response.data}" | |
return -1 | |
} | |
return returnCode | |
} | |
// HTTP POST call (Login) | |
private apiPostLogin(apiPath, apiBody = [], callback = {}) { | |
try { | |
def result = false | |
//log.debug "Logging into ${getApiURL()}/${apiPath} headers: ${getMyQHeaders()}" | |
return httpPost([ uri: getApiURL(), path: apiPath, headers: getMyQHeaders(), body: apiBody ]) { response -> | |
log.debug "Got LOGIN POST response: STATUS: ${response.status}\n\nDATA: ${response.data}" | |
if (response.status == 200) { | |
result = callback(response) | |
} else { | |
log.error "Unknown LOGIN POST status: ${response.status} data: ${response.data}" | |
} | |
result = false | |
} | |
} catch (e) { | |
log.warn "API POST Error: $e" | |
} | |
return result | |
} | |
// Send command to start or stop | |
def sendCommand(myQDeviceId, command) { | |
state.lastCommandSent = now() | |
apiPut("/api/v5.1/Accounts/${state.session.accountId}/Devices/${myQDeviceId}/actions", "{\"action_type\":\"${command}\"}", "${state.data[myQDeviceId].name}(${command})") | |
return true | |
} | |
//Remove old unused pieces of state | |
def stateCleanup(){ | |
if (state.latestDoorNoSensorVersion){state.remove('latestDoorNoSensorVersion')} | |
if (state.latestDoorVersion){state.remove('latestDoorVersion')} | |
if (state.latestLightVersion){state.remove('latestLightVersion')} | |
if (state.latestSmartAppVersion){state.remove('latestSmartAppVersion')} | |
if (state.thisDoorNoSensorVersion){state.remove('thisDoorNoSensorVersion')} | |
if (state.thisDoorVersion){state.remove('thisDoorVersion')} | |
if (state.thisLightVersion){state.remove('thisLightVersion')} | |
if (state.thisSmartAppVersion){state.remove('thisSmartAppVersion')} | |
if (state.versionWarning){state.remove('versionWarning')} | |
if (state.polling){state.remove('polling')} | |
} | |
//Available to be called from child devices for special logging | |
def notify(message){ | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment