Create a gist now

Instantly share code, notes, and snippets.

@fishy /myq.groovy
Last active Nov 20, 2017

What would you like to do?
MyQ Garage SmartThings device handler
/**
* 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")
}
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)
runEvery1Minute(doRefresh)
}
def updated() {
log.debug "Updating MyQ Garage Door"
state.Login.Expiration = 0
state.DeviceID = 0
checkLogin(settings, state)
runEvery1Minute(doRefresh)
}
// 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 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