/** | |
* MyQ Garage Door | |
* | |
* Copyright 2017 Yuxuan Wang | |
* | |
* 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. | |
* | |
*/ | |
// This is a combination of https://github.com/adamheinmiller/ST_MyQ and https://github.com/Einstein42/myq-garage | |
// adamheinmiller/ST_MyQ by Adam Heinmiller, Apache License | |
// Einstein42/myq-garage by Einstein42, MIT License | |
preferences { | |
input("username", "text", title: "Username", description: "MyQ username (email address)") | |
input("password", "password", title: "Password", description: "MyQ password") | |
input("door_name", "text", title: "Door Name", description: "MyQ Garage Door name or Device ID") | |
input( | |
"refresh_rate", | |
"enum", | |
title: "State Refresh Rate", | |
options: [ | |
"Every minute", | |
"Every 5 minutes", | |
"Every 10 minutes", | |
"Every 15 minutes", | |
"Every 30 minutes", | |
"Every hour", | |
"Disabled"], | |
description: "Only disable it if you have another contact sensor hooked on the garage door", | |
required: true) | |
} | |
metadata { | |
definition (name: "MyQ Garage Door", namespace: "fishy", author: "Yuxuan Wang") { | |
capability "Contact Sensor" | |
capability "Door Control" | |
capability "Garage Door Control" | |
capability "Momentary" | |
capability "Polling" | |
capability "Refresh" | |
capability "Switch" | |
attribute "doorStatus", "string" | |
attribute "vacationStatus", "string" | |
attribute "lastDoorAction", "string" | |
} | |
simulator { | |
// TODO: define status and reply messages here | |
} | |
tiles { | |
standardTile("sDoorToggle", "device.doorStatus", width: 1, height: 1, canChangeIcon: false) { | |
state "default", label:'' | |
state "unknown", label: 'Unknown', icon: "st.unknown.unknown.unknown", action: "refresh.refresh", backgroundColor: "#afafaf" | |
state "door_not_found", label:'Not Found', backgroundColor: "#CC1821" | |
state "stopped", label: 'Stopped', icon: "st.contact.contact.open", action: "close", backgroundColor: "#ffdd00" | |
state "closed", label: 'Closed', icon:"st.doors.garage.garage-closed", action: "open", backgroundColor: "#79b820" | |
state "closing", label: 'Closing', icon:"st.doors.garage.garage-closing", backgroundColor: "#ffdd00" | |
state "open", label: 'Open', icon:"st.doors.garage.garage-open", action: "close", backgroundColor: "#ffdd00" | |
state "opening", label: 'Opening', icon:"st.doors.garage.garage-opening", backgroundColor: "#ffdd00" | |
state "moving", label: 'Moving', icon: "st.motion.motion.active", action: "refresh.refresh", backgroundColor: "#ffdd00" | |
} | |
standardTile("sRefresh", "device.doorStatus", inactiveLabel: false, decoration: "flat") { | |
state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" | |
} | |
standardTile("sContact", "device.contact") { | |
state "open", label: '${name}', icon: "st.contact.contact.open", backgroundColor: "#ffa81e" | |
state "closed", label: '${name}', icon: "st.contact.contact.closed", backgroundColor: "#79b821" | |
} | |
valueTile("vLastDoorAction", "device.lastDoorAction", width: 2, height: 1, decoration: "flat") { | |
state "default", label: '${currentValue}' | |
} | |
main(["sDoorToggle"]) | |
details(["sDoorToggle", "vLastDoorAction", "sRefresh"]) | |
} | |
} | |
def installed() { | |
log.debug "Installing MyQ Garage Door" | |
state.Login = [ Expiration: 0 ] | |
state.DeviceID = 0 | |
checkLogin(settings, state) | |
checkRefresh(settings) | |
} | |
def updated() { | |
log.debug "Updating MyQ Garage Door" | |
state.Login.Expiration = 0 | |
state.DeviceID = 0 | |
checkLogin(settings, state) | |
checkRefresh(settings) | |
} | |
// parse events into attributes | |
def parse(String description) { | |
log.debug "Parsing '${description}'" | |
// TODO: handle 'contact' attribute | |
// TODO: handle 'switch' attribute | |
} | |
// handle commands | |
def push() { | |
log.debug "Executing 'push'" | |
checkLogin(settings, state) | |
def dInitStatus | |
getDoorStatus(state) { status -> dInitStatus = status } | |
if (dInitStatus == "closed" || dInitStatus == "closing" || dInitStatus == "stopped" || dInitStatus == "moving") { | |
log.debug "Door is in a closed status, opening" | |
open() | |
} else if (dInitStatus == "open" || dInitStatus == "opening") { | |
log.debug "Door is in an open status, closing" | |
close() | |
} else if (dInitStatus == "unknown") { | |
log.debug "Door is in an unknown state, doing nothing" | |
} | |
} | |
def poll() { | |
log.debug "MyQ Garage door Polling" | |
refresh() | |
} | |
def refresh() { | |
doRefresh() | |
} | |
def checkRefresh(settings) { | |
switch (settings.refresh_rate.toLowerCase()) { | |
case "disabled": | |
unschedule(doRefresh) | |
doRefresh() | |
break | |
case "every 5 minutes": | |
runEvery5Minutes(doRefresh) | |
break | |
case "every 10 minutes": | |
runEvery10Minutes(doRefresh) | |
break | |
case "every 15 minutes": | |
runEvery15Minutes(doRefresh) | |
break | |
case "every 30 minutes": | |
runEvery30Minutes(doRefresh) | |
break | |
case "every hour": | |
runEvery1Hour(doRefresh) | |
break | |
case "every minute": | |
default: | |
runEvery1Minute(doRefresh) | |
break | |
} | |
} | |
def doRefresh() { | |
log.debug "Refreshing Door State" | |
checkLogin(settings, state) | |
getDoorStatus(state) { status -> | |
setDoorState(status) | |
setContactSensorState(status) | |
log.debug "Door Status: $status" | |
} | |
} | |
def on() { | |
log.debug "Executing 'on'" | |
open() | |
} | |
def off() { | |
log.debug "Executing 'off'" | |
close() | |
} | |
def open() { | |
log.debug "Opening Door" | |
checkLogin(settings, state) | |
def dInitStatus | |
def dCurrentStatus = "opening" | |
getDoorStatus(state) { status -> dInitStatus = status } | |
if (dInitStatus == "opening" || dInitStatus == "open" || dInitStatus == "moving") { return } | |
setDoorState("opening") | |
openDoor(state) | |
while (dCurrentStatus == "opening") { | |
sleepForDuration(1000) { | |
getDoorStatus(state, dInitStatus) { status -> dCurrentStatus = status } | |
} | |
} | |
// Contact Sensor | |
setContactSensorState("open") | |
log.debug "Final Door Status: $dCurrentStatus" | |
setDoorState(dCurrentStatus) | |
} | |
def close() { | |
log.debug "Closing Door" | |
checkLogin(settings, state) | |
def dInitStatus | |
def dCurrentStatus = "closing" | |
def dTotalSleep = 0 | |
getDoorStatus(state) { status -> dInitStatus = status } | |
if (dInitStatus == "closing" || dInitStatus == "closed" || dInitStatus == "moving") { return } | |
setDoorState("closing") | |
closeDoor(state) | |
sleepForDuration(7500) { dTotalSleep += it } | |
while (dCurrentStatus == "closing" && dTotalSleep <= 10000) { | |
sleepForDuration(1000) { | |
dTotalSleep += it | |
getDoorStatus(state, dInitStatus) { status -> dCurrentStatus = status } | |
} | |
} | |
if (dTotalSleep >= 10000) { | |
log.debug "Exceeded Door Close time: $dTotalSleep" | |
dCurrentStatus = "closed" | |
} | |
// Contact Sensor | |
setContactSensorState("closed") | |
log.debug "Final Door Status: $dCurrentStatus" | |
setDoorState(dCurrentStatus) | |
} | |
def checkLogin(settings, state) { | |
//log.debug "Checking Login Credentials" | |
def logins = state.Login | |
log.debug "login: $logins" | |
if (state.Login.Expiration <= new Date().getTime()) { | |
def now = new Date().getTime() | |
def expiration = state.Login.Expiration | |
log.debug "expiration: $expiration, now: $now" | |
login(settings, state) | |
} | |
if (state.DeviceID == 0) { | |
getDevice(settings, state) | |
} | |
} | |
def login(settings, state) { | |
log.debug "Logging In to Webservice" | |
def logins = state.Login | |
log.debug "login: $logins" | |
def body = [ | |
username: settings.username, | |
password: settings.password, | |
] | |
callApiPost(state, "api/v4/User/Validate", body) { response -> | |
state.Login = [ | |
BrandID: response.data.BrandName, | |
UserID: response.data.UserId, | |
SecToken: response.data.SecurityToken, | |
Expiration: (new Date()).getTime() + 600000 | |
] | |
log.debug "Sec Token: $state.Login.SecToken" | |
} | |
} | |
def getDevice(settings, state) { | |
log.debug "Getting MyQ Devices" | |
/* | |
// If we set a door name that looks like a device id, use it as a device id | |
if ((settings.door_name ?: "blank").isLong() == true) { | |
log.debug "Door Name: Assuming Door Name is a Device ID, $settings.door_name" | |
state.DeviceID = settings.door_name | |
return | |
}*/ | |
callApiGet(state, "api/v4/userdevicedetails/get") { response -> | |
def garageDevices = response.getData().Devices.findAll{ it.MyQDeviceTypeId == 2 } | |
if (garageDevices.isEmpty() == true) { | |
log.debug "Device Discovery found no supported door devices" | |
setDoorState("door_not_found") | |
return | |
} | |
state.DeviceID = 0 | |
garageDevices.each { pDevice -> | |
//log.debug "device = $pDevice" | |
def doorAttrib = pDevice.Attributes.find{ it.AttributeDisplayName == "desc" } | |
if (doorAttrib.Value.toLowerCase() == settings.door_name.toLowerCase()) { | |
log.debug "Door ID: $pDevice.MyQDeviceId" | |
state.DeviceID = pDevice.MyQDeviceId | |
} | |
} | |
if (state.DeviceID == 0) { | |
log.debug "Supported door devices were found but none matched name '$settings.door_name'" | |
} | |
} | |
} | |
def getDoorStatus(state, initialStatus = null, callback) { | |
callApiGet(state, "api/v4/userdevicedetails/get") { response -> | |
def garageDevices = response.getData().Devices.findAll{ it.MyQDeviceId == state.DeviceID } | |
if (garageDevices.isEmpty() == true) { | |
log.debug "Cannot find the garage door" | |
return | |
} | |
garageDevices.each { pDevice -> | |
def stateAttrib = pDevice.Attributes.find{ it.AttributeDisplayName == "doorstate" } | |
def doorState = translateDoorStatus( stateAttrib.Value, initialStatus ) | |
calcLastActivityTime(stateAttrib.UpdatedTime.toLong(), doorState) | |
callback(doorState) | |
} | |
} | |
} | |
def calcLastActivityTime(lastActivity, doorState) { | |
def currentTime = new Date().getTime() | |
def diffTotal = currentTime - lastActivity | |
def lastActLabel = "" | |
//diffTotal = (86400000 * 12) + (3600000 * 2) + (60000 * 1) | |
def diffDays = (diffTotal / 86400000) as long | |
def diffHours = (diffTotal % 86400000 / 3600000) as long | |
def diffMinutes = (diffTotal % 86400000 % 3600000 / 60000) as long | |
def diffSeconds = (diffTotal % 86400000 % 3600000 % 60000 / 1000) as long | |
if (diffDays == 1) lastActLabel += "${diffDays} Day" | |
else if (diffDays > 1) lastActLabel += "${diffDays} Days" | |
if (diffDays > 0 && diffHours > 0) lastActLabel += ", " | |
if (diffHours == 1) lastActLabel += "${diffHours} Hour" | |
else if (diffHours > 1) lastActLabel += "${diffHours} Hours" | |
if (diffDays == 0 && diffHours > 0 && diffMinutes > 0) lastActLabel += ", " | |
if (diffDays == 0 && diffMinutes == 1) lastActLabel += "${diffMinutes} Minute" | |
if (diffDays == 0 && diffMinutes > 1) lastActLabel += "${diffMinutes} Minutes" | |
if (diffTotal < 60000) lastActLabel = "${diffSeconds} Seconds" | |
sendEvent( | |
name: "lastDoorAction", | |
value: lastActLabel, | |
descriptionText: "$doorState time is $lastActLabel", | |
) | |
} | |
def openDoor(state) { | |
def value = "1" | |
callApiPut(state, "api/v4/DeviceAttribute/PutDeviceAttribute", value) { response -> | |
// if error, do something? | |
} | |
} | |
def closeDoor(state) { | |
def value = "0" | |
callApiPut(state, "api/v4/DeviceAttribute/PutDeviceAttribute", value) { response -> | |
// if error, do something? | |
} | |
} | |
def setContactSensorState(status) { | |
// Sync contact sensor | |
if (status == "open" || status == "opening" || status == "stopped") { | |
sendEvent(name: "contact", value: "open", display: true, descriptionText: "Contact is open") | |
sendEvent(name: "switch", value: "on", display: true, descriptionText: "Switch is on") | |
} else if (status == "closed" || status == "closing") { | |
sendEvent(name: "contact", value: "closed", display: true, descriptionText: "Contact is closed") | |
sendEvent(name: "switch", value: "off", display: true, descriptionText: "Switch is off") | |
} | |
} | |
def setDoorState(status) { | |
sendEvent(name: "doorStatus", value: status, display: true, descriptionText: "Door is $status") | |
sendEvent(name: "door", value: status, display: true, descriptionText: "Door is $status") | |
} | |
def translateDoorStatus(status, initStatus = null) { | |
def dReturn = "unknown" | |
if (status == "2") dReturn = "closed" | |
else if (status == "1" || status == "9") dReturn = "open" | |
else if (status == "4" || (status == "8" && initStatus == "closed")) dReturn = "opening" | |
else if (status == "5" || (status == "8" && initStatus == "open")) dReturn = "closing" | |
else if (status == "3") dReturn = "stopped" | |
else if (status == "8" && initStatus == null) dReturn = "moving" | |
if (dReturn == "unknown") { log.debug "Unknown Door Status ID: $status" } | |
log.debug "state = $dReturn" | |
return dReturn | |
} | |
def sleepForDuration(duration, callback = {}) { | |
// I'm sorry! | |
def dTotalSleep = 0 | |
def dStart = new Date().getTime() | |
while (dTotalSleep <= duration) { | |
try { httpGet("http://australia.gov.au/404") { } } catch (e) { } | |
dTotalSleep = (new Date().getTime() - dStart) | |
} | |
//log.debug "Slept ${dTotalSleep}ms" | |
callback(dTotalSleep) | |
} | |
def getVarParams() { | |
def dParams = [ | |
BaseURL: "https://myqexternal.myqdevice.com/", | |
AppID: "NWknvuBd7LoFHfXmKNMBcgajXtZEgKUh4V7WNzMidrpUUluDpVYVZx+xT4PCM5Kx" | |
] | |
return dParams | |
} | |
def callApiPut(state, apipath, value, callback = {}) { | |
def finalHeaders = [ | |
"User-Agent": "Chamberlain/3.73", | |
"BrandId": "Chamberlain", | |
"ApiVersion": "4.1", | |
"Culture": "en", | |
"MyQApplicationId": getVarParams().AppID, | |
] | |
def query = [ | |
"appId": getVarParams().AppID, | |
"SecurityToken": state.Login.SecToken, | |
] | |
def body = [ | |
"MyQDeviceId": state.DeviceID, | |
"SecurityToken": state.Login.SecToken, | |
"ApplicationId": getVarParams().AppID, | |
"AttributeValue": value, | |
"AttributeName": "desireddoorstate", | |
"format": "json", | |
"nojsoncallback": "1", | |
] | |
def finalParams = [ | |
uri: getVarParams().BaseURL, | |
path: apipath, | |
query: query, | |
headers: finalHeaders, | |
body: body, | |
] | |
//log.debug finalParams | |
try { | |
httpPutJson(finalParams) { response -> | |
log.debug response.data | |
if (response.data.ErrorMessage) { | |
log.debug "API Error: $response.data" | |
} | |
callback(response) | |
} | |
} catch (Error e) { | |
// setDoorState("unknown") | |
log.debug "API Error: $e" | |
} | |
} | |
def callApiGet(state, apipath, callback = {}) { | |
def finalHeaders = [ | |
"User-Agent": "Chamberlain/3.73", | |
"BrandId": "Chamberlain", | |
"ApiVersion": "4.1", | |
"Culture": "en", | |
"MyQApplicationId": getVarParams().AppID, | |
] | |
def finalQParams = [ | |
"appId": getVarParams().AppID, | |
"securityToken": state.Login.SecToken, | |
"filterOn": "true", | |
"format": "json", | |
"nojsoncallback": "1", | |
] | |
def finalParams = [ | |
uri: getVarParams().BaseURL, | |
path: apipath, | |
headers: finalHeaders, | |
query: finalQParams, | |
] | |
//log.debug finalParams | |
try { | |
httpGet(finalParams) { response -> | |
if (response.data.ErrorMessage) { | |
log.debug "API Error: $response.data" | |
} | |
callback(response) | |
} | |
} catch (Error e) { | |
// setDoorState("unknown") | |
log.debug "API Error: $e" | |
} | |
} | |
def callApiPost(state, apipath, body, callback = {}) { | |
def finalHeaders = [ | |
"User-Agent": "Chamberlain/3.73", | |
"BrandId": "Chamberlain", | |
"ApiVersion": "4.1", | |
"Culture": "en", | |
"MyQApplicationId": getVarParams().AppID, | |
] | |
def finalParams = [ | |
uri: getVarParams().BaseURL, | |
path: apipath, | |
headers: finalHeaders, | |
body: body, | |
] | |
//log.debug finalParams | |
try { | |
httpPostJson(finalParams) { response -> | |
if (response.data.ErrorMessage) { | |
log.debug "API Error: $response.data" | |
} | |
log.debug "Login succeed: $response.data" | |
callback(response) | |
} | |
} catch (Error e) { | |
// setDoorState("unknown") | |
log.debug "API Error: $e" | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment