afschmitt Posted August 29, 2014 Posted August 29, 2014 Anyone here figure out how to backwards engineer the open / close Chamberlain MyQ API for use with ISY programs? There is a plugin for SmarthThings so I am hoping to get it working in ISY. I looked at the unofficial API but it lacks to the command to open / close the garage door. Here is the unofficial API info. http://docs.unofficialliftmastermyq.apiary.io/ Smarthings Plugin http://community.smartthings.com/t/myq-garage-door-device-type/2307/10 Anyone start working on this yet?
Teken Posted August 29, 2014 Posted August 29, 2014 (edited) Do you currently have the Insteon garage I/O kit installed? Edited August 29, 2014 by Teken
MWareman Posted August 30, 2014 Posted August 30, 2014 (edited) The clue you need is at the top of the page you linked. "This is unofficial (and partial) documentation of the Chamberlain/Liftmaster MyQ API". They do not have even close to complete documentation. The API is not really compatible with ISY. You first need to make a request to an authentication endpoint using a username and password. https://myqexternal.myqdevice.com/Membership/ValidateUserWithCulture?appId=Vj8pQggXLhLy0WHahglCD4N1nAkkXQtGYpq2HrHD7H1nvmbT55KqtN6RSF4ILB%2fi&securityToken=null&username=**account_username**&password=**account_password**&culture=en JSON is returned with a securityToken. {"UserId":254586,"SecurityToken":"**security_token**","ReturnCode":"0","ErrorMessage":"","ExecutionTimes":"0;78.001;202.8026;187.2024","BrandId":2,"BrandName":"Chamberlain","RegionId":1} You then use that token to enumerate devices: https://myqexternal.myqdevice.com/api/UserDeviceDetails?appId=Vj8pQggXLhLy0WHahglCD4N1nAkkXQtGYpq2HrHD7H1nvmbT55KqtN6RSF4ILB%2fi&securityToken=**securityToken** The security token expires after - I think - an hour. So if you call /api/UserDeviceDetails and get 'Access Denied' you need to go back to /Membership/ValidateUserWithCulture with the username and password to get a new token. Control of the device is done using the Device/setDeviceAttribute (https://sites.google.com/a/arrayent.com/api/reference/device/setdeviceattribute). Looks like the name to send should be 'desireddoorstate'. Here is some code for another platform that implements this - I'm sure you can find the actual details in here: --Change these to match the authentication used to access your MyQ account local username="" local password = "" --Function found: http://lua-users.org/wiki/StringRecipes --Used to encode username / password for submission function url_encode(str) if (str) then str = string.gsub (str, "\n", "\r\n") str = string.gsub (str, "([^%w %-%_%.%~])", function (c) return string.format ("%%%02X", string.byte(c)) end) str = string.gsub (str, " ", "+") end return str end --Libraries local json = require("json") local https = require("ssl.https") --These should not change but who knows, follow UPPERCASE convention for names local APPID = "Vj8pQggXLhLy0WHahglCD4N1nAkkXQtGYpq2HrHD7H1nvmbT55KqtN6RSF4ILB%2fi" local BASEURL = "https://myqexternal.myqdevice.com/" local VALIDATIONPATH = "Membership/ValidateUserWithCulture" local USERDEVICEDETAILS = "api/UserDeviceDetails" --CONSTANTS for TypeIds in the Devices list local GARAGEDOOROPENER = 47 local GATEWAYDEVICE = 49 --Access URLs local auth_string = VALIDATIONPATH .. "?appId=" .. APPID .. "&username=" .. username .. "&password=" .. password .. "&culture=en" --Statuses for doors local doorStatuses = {["1"] = "open", ["2"] = "closed", ["4"] = "opening", ["5"] = "closing"} local SecurityToken --[[ Connects to the LiftMaster/Chamberlain MyQ API Returns result and resultText ]] local function retrieveSecurityToken(authURL, username, password) local result --Did we successfully connect and get the SecurityToken, is true or false local resultText --If false, the error, if true, the SecurityToken --Table to hold our response from the call auth_response = { } local response, status, header = https.request { url = authURL, method = "GET", headers = { ["Content-Type"] = "application/json", ["Content-Length"] = 0 }, sink = ltn12.sink.table(auth_response) } --Check out the response if( response == 1 ) then --Decode our JSON, we have a response authResponseData = json.decode( auth_response[1] ) --Check our return code if( authResponseData.ReturnCode == "0" ) then result = true resultText = authResponseData.SecurityToken else result = false resultText = "Authentication Error!" end else result = false resultText = "Unsuccessful at connecting with the authorization URL!" end return result, resultText end --[[ Inspect all devices associated with the MyQ ]] local function inspectDevices(deviceURL, SecurityToken) local connectionResult --True if successful, false if not local connectionText --Holds issue with connection local openerInfo = {} --Table to hold info about openers local device_response = { } --Table to hold our response from the call --Fire up our connection local response, status, header = https.request { url = deviceURL, method = "GET", headers = { ["Content-Type"] = "application/json", ["Content-Length"] = 0 }, sink = ltn12.sink.table(device_response) } --Check out our response if( response==1 ) then --Decode our JSON: Am unclear as to why device_response[1] threw an error --local deviceContent = json.decode( device_response[1] ) local deviceContent = json.decode(table.concat(device_response)) --A 0 appears to indicate we have had a success if(deviceContent.ReturnCode == "0" ) then connectionResult = true local numOpeners = 0 --Time to loop over our device collection for i,d in ipairs (deviceContent.Devices) do local DeviceName = d.DeviceName local DeviceId = d.DeviceId local ParentName --TypeId of 49 appears to be the gateway device --Useful here might be the desc which is the name in MyQ (e.g. Home) --Perhaps that should be the name of the parent device? if( d.TypeId == GATEWAYDEVICE ) then for k, attr in ipairs( d.Attributes ) do if( attr.Name == "desc" ) then ParentName = attr.Value end end end --TypeId of 47 appears to be the individual garage door openers if( d.TypeId == GARAGEDOOROPENER ) then --Stop the presses, we found an opener numOpeners = numOpeners +1 local doorState --Each device has an attributes collection, over it we go for j, attr in ipairs ( d.Attributes) do if( attr.Name == "desc") then openerName = attr.Value elseif( attr.Name == "doorstate" ) then local doorstateValue = attr.Value if( doorStatuses[doorstateValue] ~= nill) then doorState = doorStatuses[doorstateValue] else doorState = "ERROR: Unknown State! " .. doorstateValue end end end --Keep track of all the openers along with their state table.insert(openerInfo, numOpeners, { DeviceId = DeviceId, DeviceName = DeviceName, DoorState = doorState, OpenerName = openerName }) end end else connectionResult = false connectionText = "Failed call to the MyQ API, perhaps refresh of token needed?" end else connectionResult = false connectionText = "Unsuccessful at connecting with device URL!" end return connectionResult, openerInfo end --[[ Check on the status of a given garage door opener ]] local function getGarageDoorStatus(deviceURL, SecurityToken, DeviceId) local connectionResult --True if successful, false if not local connectionText --Holds issue with connection local device_response = { } --Table to hold our response from the call --Fire up our connection local response, status, header = https.request { url = deviceURL, method = "GET", headers = { ["Content-Type"] = "application/json", ["Content-Length"] = 0 }, sink = ltn12.sink.table(device_response) } --Check out our response if( response==1 ) then --Decode our JSON: Am unclear as to why device_response[1] threw an error --local deviceContent = json.decode( device_response[1] ) local deviceContent = json.decode(table.concat(device_response)) --A 0 appears to indicate we have had a success if(deviceContent.ReturnCode == "0" ) then connectionResult = true --Time to loop over our device collection for i,d in ipairs (deviceContent.Devices) do -- Only interested in our specified garage door opener if( d.TypeId == GARAGEDOOROPENER and d.DeviceId == DeviceId ) then local doorState for j, attr in ipairs ( d.Attributes ) do if( attr.Name == "doorstate" ) then local doorstateValue = attr.Value if( doorStatuses[doorstateValue] ~= nill) then connectionText = doorStatuses[doorstateValue] else connectionText = "ERROR: Unknown State! " .. doorstateValue end end end end end else connectionResult = false connectionText = "Failed call to the MyQ API, perhaps refresh of token needed?" end else connectionResult = false connectionText = "Unsuccessful at connecting with device URL!" end return connectionResult, connectionText end --[[ Change the state of a garage door. Basically a doorstate of 1 is open and a doorstate of 0 is close DeviceId is found in the output of inspectDevices ]] local function changeGarageDoorState(DoorDeviceId, AppId, DoorAction, SecurityToken) local result --Result of the action, true or false local resultText --Summary of the result local doorActions = {[0] = "close", [1] = "open"} --Our JSON to be delivered... jsonPut = { AttributeName = "desireddoorstate", DeviceId = DoorDeviceId, ApplicationId = AppId, AttributeValue = DoorAction, SecurityToken = SecurityToken } --JSON encode it for delivery json_data = json.encode(jsonPut) local response_body = {} --Fire up our request, noting that we are using the PUT method and setting our content length in the header local response, status, header = https.request{ method = "PUT", url = "https://myqexternal.myqdevice.com/Device/setDeviceAttribute", headers = { ["Content-Type"] = "application/json", ["Content-Length"] = string.len(json_data) }, source = ltn12.source.string(json_data), sink = ltn12.sink.table(response_body) } if( response == 1) then local output = json.decode( response_body[1] ) if ( output.ReturnCode == "0" ) then result = true resultText = "Successfully changed status to " .. doorActions[DoorAction] else result = false resultText = "Authentication error. Perhaps token expired?" end else result = false resultText = "Unsuccessful at communicating with the setDeviceAttribute service" end return result, resultText end ------------------------------------------ -- -- Time to use the functions... -- ------------------------------------------ --Get our security token, making sure we encode our username and password local authResult, authText = retrieveSecurityToken(BASEURL .. auth_string, url_encode(username), url_encode(password)) --Is everything ok here? if(authResult == true ) then print("Success, security token is: " .. authText) SecurityToken = authText else print("ERROR: Message is " .. authText) end --Swing away, let's see what we have for devices.. local connectionResult, openerInfo = inspectDevices(BASEURL .. USERDEVICEDETAILS .. "?appId=" .. APPID .. "&securityToken=" .. SecurityToken, SecurityToken) print("I see that you have " .. #openerInfo .. " garage door openers:") for i=1, #openerInfo do print( openerInfo[i].OpenerName .. " is currently " .. openerInfo[i].DoorState) end --Action: 1=Open, 0=Close --Let's close a door from above.. result, resultText = changeGarageDoorState(openerInfo[2].DeviceId, APPID, 0, SecurityToken) print(result) print(resultText) socket.sleep(3) --Check on the status.. local gdStatus, gdStatusText = getGarageDoorStatus(BASEURL .. USERDEVICEDETAILS .. "?appId=" .. APPID .. "&securityToken=" .. SecurityToken, SecurityToken, openerInfo[2].DeviceId) print(gdStatusText) The ISY cannot do this though. I am hopeful UDI will include parsing JSON responses in the network module in the future. On the I/O Linc: There are issues introduced into MyQ when you short the cables together with an I/O linc (if that's what you are using or planning to use). Every time you operate the door thru I/O linc, the MyQ system resets and you don't get any status thru MyQ. That's a shame - thru MyQ I can see the actual position of the door. I've hacked into a car control and wired that up (I use an Elk output - not an I/O link - but the issue is the same). It kinda works - but it breaks MyQ. So - in summary - it can be done with an external helper server on your lan. But it's not trivial. This is why I hacked an opener button... so I could avoid this nastiness. Michael. Edited August 30, 2014 by MWareman
Recommended Posts