Jump to content

MyQ API Integration


afschmitt

Recommended Posts

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?

Link to comment

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 by MWareman
Link to comment
Guest
This topic is now closed to further replies.

  • Recently Browsing

    • No registered users viewing this page.
  • Who's Online (See full list)

    • There are no registered users currently online
  • Forum Statistics

    • Total Topics
      36.9k
    • Total Posts
      370.6k
×
×
  • Create New...