Jump to content

MyQ API Integration


afschmitt

Recommended Posts

Posted

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?

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

×
×
  • Create New...