Skip to content

Instantly share code, notes, and snippets.

@fishy
Last active March 3, 2019 05:06
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save fishy/5d5bdae727d5daa75f00e723ed4fc183 to your computer and use it in GitHub Desktop.
Save fishy/5d5bdae727d5daa75f00e723ed4fc183 to your computer and use it in GitHub Desktop.
MyQ Garage SmartThings device handler
/**
* MyQ Garage Door
*
* Copyright 2018 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
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 "lastDoorAction", "string"
attribute "lastHttpStatus", "enum", ["succeeded", "failed"]
}
preferences {
input(
name: "username",
type: "text",
title: "Username",
description: "MyQ username (email address)",
required: true,
)
input(
name: "password",
type: "password",
title: "Password",
description: "MyQ password",
required: true,
)
input(
name: "base_url",
type: "text",
title: "Base URL",
description: "Base URL of MyQ API endpoint (default: \"https://myqexternal.myqdevice.com/\")",
)
input(
name: "myq_app_id",
type: "text",
title: "MyQ App ID",
description: "appId used in API calls, leave empty to use default one",
)
input(
name: "door_name",
type: "text",
title: "Door Name",
description: "MyQ Garage Door name (default: \"Garage Door\")",
)
input(
name: "refresh_rate",
type: "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,
)
}
tiles {
standardTile("sDoorToggle", "device.door", width: 1, height: 1, canChangeIcon: false) {
state(
"default",
label: "",
)
state(
"unknown",
label: "Unknown",
icon: "st.unknown.unknown.unknown",
action: "refresh.refresh",
backgroundColor: "#afafaf",
)
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: "#00a0dc",
)
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.door", 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.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)
log.debug "Door Status: $status"
}
}
def afterForceRefresh(status, startTime) {
def time = (now() - startTime) / 1000
log.debug "Final Door Status: $status, took $time seconds"
setDoorState(status)
}
def forceRefreshUntil(data) {
def timestamp = now()
def target = data.targetStatus
log.debug "forceRefreshUntil: ${new Date()}, timestamp: $timestamp, stops at ${data.stopAt}, target status: $target"
def scheduleNext = true
if (timestamp >= data.stopAt) {
log.debug "Stopping refreshing..."
getDoorStatus(state) { status ->
afterForceRefresh(status, data.startTime)
}
scheduleNext = false
return
}
getDoorStatus(state) { status ->
log.debug "forceRefreshUntil: get door status: $status"
if (status == target) {
log.debug "Got target status $status, stopping refreshing..."
afterForceRefresh(status, data.startTime)
scheduleNext = false
return
}
}
if (scheduleNext) {
def options = [
overwrite: true,
data: data,
]
runIn(3, forceRefreshUntil, options)
} else {
unschedule(forceRefreshUntil)
}
}
def refreshUntil(target) {
log.debug "refreshUntil: $target"
def maxMin = 5
def timestamp = now() + 60 * 1000 * maxMin
def data = [
startTime: now(),
stopAt: timestamp,
targetStatus: target,
]
forceRefreshUntil(data)
}
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)
refreshUntil("open")
}
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)
refreshUntil("closed")
}
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 ->
log.debug "response: $response.data"
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"
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"
sendEvent(
name: "lastHttpStatus",
value: "failed",
displayed: true,
descriptionText: "Device Discovery found no supported door devices",
)
return
}
state.DeviceID = 0
def doorName = "garage door"
if (settings.door_name != null) {
doorName = settings.door_name.toLowerCase()
}
garageDevices.each { pDevice ->
log.debug "device = $pDevice"
def doorAttrib = pDevice.Attributes.find{ it.AttributeDisplayName == "desc" }
if (doorAttrib.Value.toLowerCase() == doorName) {
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,
displayed: true,
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" || status == "closing") {
sendEvent(
name: "contact",
value: "open",
displayed: true,
descriptionText: "Contact is open",
)
sendEvent(
name: "switch",
value: "on",
displayed: true,
descriptionText: "Switch is on",
)
} else if (status == "closed") {
sendEvent(
name: "contact",
value: "closed",
displayed: true,
descriptionText: "Contact is closed",
)
sendEvent(
name: "switch",
value: "off",
displayed: true,
descriptionText: "Switch is off",
)
}
}
def setDoorState(status) {
sendEvent(
name: "door",
value: status,
displayed: true,
descriptionText: "Door is $status",
)
setContactSensorState(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 getVarParams() {
def base = "https://myqexternal.myqdevice.com/"
if (settings.base_url != null) {
base = settings.base_url
}
def appId = "NWknvuBd7LoFHfXmKNMBcgajXtZEgKUh4V7WNzMidrpUUluDpVYVZx+xT4PCM5Kx"
if (settings.myq_app_id != null) {
appId = settings.myq_app_id
}
def dParams = [
BaseURL: base,
AppId: appId,
BrandId: "2",
UserAgent: "Chamberlain/3.73",
ApiVersion: "4.1",
Culture: "en",
]
return dParams
}
def callApiPut(state, apipath, value, callback = {}) {
def finalHeaders = [
"User-Agent": getVarParams().UserAgent,
"BrandId": getVarParams().BrandId,
"ApiVersion": getVarParams().ApiVersion,
"Culture": getVarParams().Culture,
"MyQApplicationId": getVarParams().AppId,
"SecurityToken": state.Login.SecToken,
]
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"
sendEvent(
name: "lastHttpStatus",
value: "failed",
displayed: true,
descriptionText: "API Error: $response.data",
)
callback(response)
return
}
sendEvent(
name: "lastHttpStatus",
value: "succeeded",
displayed: true,
descriptionText: "HTTP request succeeded",
)
callback(response)
}
} catch (Error e) {
// setDoorState("unknown")
sendEvent(
name: "lastHttpStatus",
value: "failed",
displayed: true,
descriptionText: "HTTP request failed: $e",
)
log.debug "API Error: $e"
}
}
def callApiGet(state, apipath, callback = {}) {
def finalHeaders = [
"User-Agent": getVarParams().UserAgent,
"BrandId": getVarParams().BrandId,
"ApiVersion": getVarParams().ApiVersion,
"Culture": getVarParams().Culture,
"MyQApplicationId": getVarParams().AppId,
"SecurityToken": state.Login.SecToken,
]
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"
sendEvent(
name: "lastHttpStatus",
value: "failed",
displayed: true,
descriptionText: "API Error: $response.data",
)
callback(response)
return
}
sendEvent(
name: "lastHttpStatus",
value: "succeeded",
displayed: true,
descriptionText: "HTTP request succeeded",
)
callback(response)
}
} catch (Error e) {
// setDoorState("unknown")
sendEvent(
name: "lastHttpStatus",
value: "failed",
displayed: true,
descriptionText: "HTTP request failed: $e",
)
log.debug "API Error: $e"
}
}
def callApiPost(state, apipath, body, callback = {}) {
def finalHeaders = [
"User-Agent": getVarParams().UserAgent,
"BrandId": getVarParams().BrandId,
"ApiVersion": getVarParams().ApiVersion,
"Culture": getVarParams().Culture,
"MyQApplicationId": getVarParams().AppId,
]
def finalParams = [
uri: getVarParams().BaseURL,
path: apipath,
headers: finalHeaders,
body: body,
]
log.debug "final params: $finalParams"
try {
httpPostJson(finalParams) { response ->
if (response.data.ErrorMessage) {
log.debug "API Error: $response.data"
sendEvent(
name: "lastHttpStatus",
value: "failed",
displayed: true,
descriptionText: "API Error: $response.data",
)
callback(response)
return
}
log.debug "Login succeed: $response.data"
sendEvent(
name: "lastHttpStatus",
value: "succeeded",
displayed: true,
descriptionText: "HTTP request succeeded",
)
callback(response)
}
} catch (Error e) {
// setDoorState("unknown")
sendEvent(
name: "lastHttpStatus",
value: "failed",
displayed: true,
descriptionText: "HTTP request failed: $e",
)
log.debug "API Error: $e"
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment