Author Topic: Chamberlain/Liftmaster MyQ Plugin  (Read 115929 times)

macrho

  • Guest
Chamberlain/Liftmaster MyQ Plugin
« on: April 21, 2014, 05:12:04 pm »
INSTALLATION INSTRUCTIONS:
The first step is to copy the files in the zip archive found below to your Vera

Navigate to the APPS tab in the Vera UI and then click on 'Develop Apps'
Click on 'Luup files' and then upload the 6 lua, xml and json files:
 -D_MyQGateway.json
 -D_MyQGateway.xml
 -I_MyQGateway.xml
 -L_MyQGateway.lua
 -L_MyQGateway_json.lua
 -S_MyQGateway.xml

Scroll down a bit and check 'Restart Luup after upload' and then click the 'Go' button

If you want to use the icon with the device, use WinSCP or another app to ssh to your Vera box.
Copy the "MyQ_Gateway.png" file to  /www/cmh/skins/default/icons/

]Now we have to create the device:

Navigate to the APPS tab in the Vera UI and then click on 'Develop Apps'
Click 'Create device' and:
 1) In the Description field type "MyQ Gateway"
 2) In the Upnp Device Filename type "D_MyQGateway.xml"

Finally, click the "Create device" button. A pop-up should occur with "Device created: Dev #"

Click "Reload" on the top right of the Vera UI

If all went right, you should see a warning at the top of the page:
MyQGateway: Username not configured. Password not configured.

Click on the wrench of the MyQ Gateway device and then click on the "Advanced" tab

If you don't see a "Username" or "Password" variable variable under Variables, you'll need to click on 'Reload' again.

Check the Advanced tab of the "MyQ Gateway" device and enter your username and password for MyQ and then click on Save

Your Vera should startup again and create your children device, which are as many garage doors as you have associated to the gateway.


05/19/2014 Update
Displayed last auth (last time an authentication event occurred) and last check (last time garage door status was checked) to the MyQ Gateway device display. Added some error handling


05/18/2014 Update
Added a new typeId constant to GARAGE_DOOR_OPENER

05/12/2014 Update
I changed the re-authentication procedure a few days ago and left the code out to do it, doh! This version has it
I've changed everything to be referred to as "MyQGateway" instead of LiftMasterOpener -- thought that might make some more sense



05/09/2014 Update
I've changed the code to re-authenticate every 23 hours as it seems that somewhere after 24 hours I have a failure. Not sure if this will fix it
Initial attempt at using task to give feedback through the Vera UI
Added an icon that can be uploaded to /www/cmh/skins/default/icons



04/25/2014 Update
Am at a pre-alpha stage: The second page of this thread has a zip file with code to create a MyQ Gateway device along with creating any garage door openers found.  Looking for constructive feedback on my first foray into Lua/LUUP programming.  So far, the devices work in that they:

a) Report status of openers
b) Allow openers to be opened/closed through a doorlock control

Look for more documentation at a future date as I learn more about plugins

 


My Subject line is a little bold as I don't have a plugin to offer, but... I believe there's hope
I have LUA code (found in this thread: http://forum.micasaverde.com/index.php/topic,9398.0.html) that can open and close my garage door along with checking their state.

I plan on trying to create a plugin (though I'm fairly clueless in this department.). I gather this topic will be more of a documentation of my struggles in creating the plugin. I'd like to thank @guessed for his help to date -- recall, I'm a bumbling buffoon in this

anyway, for this to work:

You need to purchase their gateway device: http://www.liftmaster.com/lmcv2/accessoryfamily/213/myq-enabled-accessories/  [it's around $31 at amazon] and create an account for their service:

http://www.liftmaster.com/lmcv2/products/introducingliftmasterinternetgateway.htm

Unfortunately, there are no MyQ supported APIs on how to interface with their services but some smart folks have reverse engineered it and that's what I'm using.  Here's the latest iteration of code, which I documented in the previous thread:

I have now created 4 separate functions:

retrieveSecurityToken: Has two returns- authResult and authText
authResult is true or false, if true authText has the SecurityToken if false, it has the reason for a failure in retrieving the security token
Is the way I'm returning the result(s) bad form?  So there would be a timer calling this and keeping this in "state" in Vera eventually

inspectDevices: Has two returns ( though should be 3 ) one is a table of tables that builds up info about the devices [ID, Names, Description, door status] -- I imagine this general code could create all the original devices [left garage door opener with associated device id and device name] -- on reloads does a plug check all of these again so that you can automatically add new openers if they are found?

changeGarageDoorState: opens or closes a given garage door.  The action buttons on a plugin, I'd presume

getGarageDoorStatus: shows the current state of a given garage door [open, opening, closing, closed]. Showing the current state on the plugin

and the horrible code follows:

Code: [Select]
--[[
The beginning of a plugin to connect to the Chamberlain Liftmaster MyQ API
Unofficial documentation of the API is found here:
http://docs.unofficialliftmastermyq.apiary.io/

There is also a Ruby gem implementation at GitHub:
https://github.com/pfeffed/liftmaster_myq

]]


--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)


I'd be grateful for anyone's comments/suggestions/thoughts along the way.
« Last Edit: May 20, 2014, 12:32:35 pm by macrho »

Offline watou

  • Hero Member
  • *****
  • Posts: 866
  • Karma: +43/-12
Re: Chamberlain/Liftmaster MyQ Plugin [WIP]
« Reply #1 on: April 21, 2014, 07:45:26 pm »
Hi macrho,

I was asked about this recently, and sent the link to https://github.com/pfeffed/liftmaster_myq and http://docs.unofficialliftmastermyq.apiary.io/ .  Here was my response:

Quote
Interesting stuff.  From what the fellow has figured out, a Vera plugin is definitely possible.  Of course, there is a potential question of LiftMaster's license terms to its users, and the risk that the unique app ID that their mobile app(s) use could be revoked and replaced, killing the plugin dead.  In a perfect world, LiftMaster would officially enable an external developer with the documented API and a unique app ID that couldn't be exposed or used by "unauthorized" parties.  I developed a plugin for the Ecobee thermostat under this kind of arrangement, and it is certainly less harrowing than relying on a reverse-engineered, could-die-any-minute interface.  (The unique app ID for the Ecobee plugin in distributed in an encrypted file that is only ever decrypted into Vera's memory at runtime, making it extremely difficult to compromise.)

There is nothing glaring about your code, so I would encourage you to keep plugging away.  My Ecobee plugin is not nearly the same kind of beast you're trying to build, but in case its code is useful to you (similar issues around web services API over HTTPS using auth tokens and app key), you can find it at https://github.com/watou/vera-ecobee-thermostat.

Good luck!

watou

macrho

  • Guest
Re: Chamberlain/Liftmaster MyQ Plugin [WIP]
« Reply #2 on: April 22, 2014, 05:47:12 am »
Thank you, watou -

I've reached out to LiftMaster/Chamberlain on a couple of occasions trying to get some sort of developers access but to date they don't seem to be open to it. Not sure why, it would be a nice feature to offer.

And thank you for the link to your code, I'll start looking over it. I'm looking at this as a challenge: Can I actually do this? I hope the appId I'm using stays viable -- if not, I've gotten to better understand Vera and LUA/LUUP through this exercise :)

« Last Edit: May 09, 2014, 11:37:41 am by macrho »

Offline Don Diego

  • Hero Member
  • *****
  • Posts: 540
  • Karma: +285/-3
Re: Chamberlain/Liftmaster MyQ Plugin [WIP]
« Reply #3 on: April 22, 2014, 06:26:13 am »
My Subject line is a little bold as I don't have a plugin to offer, but... I believe there's hope
I have LUA code (found in this thread: http://forum.micasaverde.com/index.php/topic,9398.0.html) that can open and close my garage door along with checking their state.

I plan on trying to create a plugin (though I'm fairly clueless in this department.). I gather this topic will be more of a documentation of my struggles in creating the plugin. I'd like to thank @guessed for his help to date -- recall, I'm a bumbling buffoon in this

anyway, for this to work:

You need to purchase their gateway device: http://www.liftmaster.com/lmcv2/accessoryfamily/213/myq-enabled-accessories/  [it's around $31 at amazon] and create an account for their service:

http://www.liftmaster.com/lmcv2/products/introducingliftmasterinternetgateway.htm

Unfortunately, there are no MyQ supported APIs on how to interface with their services but some smart folks have reverse engineered it and that's what I'm using.  Here's the latest iteration of code, which I documented in the previous thread:

I have now created 4 separate functions:

retrieveSecurityToken: Has two returns- authResult and authText
authResult is true or false, if true authText has the SecurityToken if false, it has the reason for a failure in retrieving the security token
Is the way I'm returning the result(s) bad form?  So there would be a timer calling this and keeping this in "state" in Vera eventually

inspectDevices: Has two returns ( though should be 3 ) one is a table of tables that builds up info about the devices [ID, Names, Description, door status] -- I imagine this general code could create all the original devices [left garage door opener with associated device id and device name] -- on reloads does a plug check all of these again so that you can automatically add new openers if they are found?

changeGarageDoorState: opens or closes a given garage door.  The action buttons on a plugin, I'd presume

getGarageDoorStatus: shows the current state of a given garage door [open, opening, closing, closed]. Showing the current state on the plugin

and the horrible code follows:

Code: [Select]
--[[
The beginning of a plugin to connect to the Chamberlain Liftmaster MyQ API
Unofficial documentation of the API is found here:
http://docs.unofficialliftmastermyq.apiary.io/

There is also a Ruby gem implementation at GitHub:
https://github.com/pfeffed/liftmaster_myq

]]


--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)


I'd be grateful for anyone's comments/suggestions/thoughts along the way.

Hi macrho,

  That would be great if we could get a plug-in working. Thanks

     Don
Vera 3 (@1.5.622) (3); Vera Plus (2);
Trane/Schlage TStats (1); Schlage Deadbolt (2); Kwikset Lock (3);  GE 45602 Dimmer (14); GE 45603 Dimmer (17); HSM-100 (16); Everspring Siren (8), Everspring Temp/Humidity (4); HSM 200 (1)

macrho

  • Guest
Re: Chamberlain/Liftmaster MyQ Plugin [WIP]
« Reply #4 on: April 22, 2014, 12:01:11 pm »
Color me confused. When I look in the log file on Vera, I see an error in creating my device (D_LiftMasterOpener.xml):

Code: [Select]
01 04/22/14 11:37:49.899 JobHandler_LuaUPnP::CreateDevice_LuaUPnP failed to load 358/D_LiftMasterOpener.xml so device 358 is offline <0x2b5fb000>

I've attached the files I've created so far

D_LiftMasterOpener.xml
I've compared this to a number of different device files and it appears to have the right structure

I_LiftMasterOpener.xml
I set the procol to CR [carriage return] and pointed it to L_LifeMasterOpener.lua to run the init function on startup

D_LiftMasterOpener.json
I'm absolutely confused by this one. So this is how I can interact with the plugin. I see this as being the master (or parent) device that stores the username and password for the user. I've looked at a number of JSON files but I can't seem to make the connection on mapping fields (and the next jump, storing them)

L_LiftMasterOpener.lua
Nothing much here, wanted to write out the device id to the log

I think I've given myself a headache. The steps in creating the device are to:
  • Upload the files to Vera  through Develop Apps and Restart Luup after uploaded
  • Create a device using only the Upnp Device Filename

Offline watou

  • Hero Member
  • *****
  • Posts: 866
  • Karma: +43/-12
Re: Chamberlain/Liftmaster MyQ Plugin [WIP]
« Reply #5 on: April 22, 2014, 12:12:31 pm »
I_LiftMasterOpener.xml, line 8 has a closing </actionList> tag only -- so it's invalid XML.

I don't think you need to worry about protocol for a plugin that is doing HTTPS .  I think it's for serial-port devices.

A device type D_ implements one or more services, and the services S_ have variables defined.  Your state is kept in variables defined by the service(s) your device type implements.  The I_ file is the Lua code that reads and writes the variables.  The .json file has a lot to do with presentation and triggers.

Writing your first plugin is indeed headache-inducing.  Try to visually trace through the files of a small example plugin.

watou

P.S. Think more "object-oriented" in coming up with device types and services, instead of thinking of it as an "opener" or garage doors.  You are wanting to logically represent the thing as the device type (LiftMasterGarageDoor), and the variables and actions it exposes as sets of one or more services (open/close).  See if anyone already made UPnP service descriptions for garage doors and consider following their lead.  The I_ file is the code that makes it all go.  The .json is mainly for how it will look to the user.
« Last Edit: April 22, 2014, 12:17:19 pm by watou »

macrho

  • Guest
Re: Chamberlain/Liftmaster MyQ Plugin [WIP]
« Reply #6 on: April 22, 2014, 02:04:57 pm »
@watou: I have the device now actually being created and think I should name the parent device the MyQGateway and then children would be openers  -- just trying to find my way through this fun and thank you for the tips

I'm now completely and utterly stumped as to how I add fields in the advanced properties of the device.. I was looking at a SimpelSerialPlugin that helped, I'll now look for some Garage Door examples

macrho

  • Guest
Re: Chamberlain/Liftmaster MyQ Plugin [WIP]
« Reply #7 on: April 22, 2014, 03:11:45 pm »
Ok, step by painful step: I can no add variables to the advanced tab and persist their state:

for example, my startup is:

Code: [Select]
function init  (lul_device)

local username_ID = luup.variable_set("urn:macrho-com:serviceId:LiftMasterOpener1", "Username", "",lul_device)

end

and there we go, field added and persisted


EDIT
However, changes do not appear to be persisted.
« Last Edit: April 22, 2014, 05:31:35 pm by macrho »

macrho

  • Guest
Re: Chamberlain/Liftmaster MyQ Plugin [WIP]
« Reply #8 on: April 22, 2014, 06:13:42 pm »
I figure I'll continue to add my progress as I go. It might help someone who is smarter than me to do this quicker

This code is really sloppy and it's basically me testing as I go, trying to figure this out..
With that being said, the device creates properly

  • Username and password are in the advanced properties of the device -- though I think I've messed something up given that I can't persist changes to username or password
  • The device has a lastupdate field (stolen from the netatmo plugin) that gets updated with the name of the gateway device
  • If the initial authentication doesn't work, you'll get an error


so... I think this is some sort of progress. I can actually retrieve the Security Token (not sure where I store it for reuse, in the advanced properties?), I can then query devices, which then becomes the piece for creating all the children along with their status and then hook up the actions and I'm done..

well... not that quickly. I'd appreciate if anyone can look the files over and offer some feedback. Please be forewarned that it's not at all clean and it's basically me testing, trying, repeating - some stuff doesn't make sense (I believe I'm asking for the username and password in the authentication piece but passing that in already in the URL...)

hope this is helpful to watch a half-wit attempt to write a plugin

macrho

  • Guest
Re: Chamberlain/Liftmaster MyQ Plugin [WIP]
« Reply #9 on: April 23, 2014, 02:06:25 pm »
...and another update, this is a lot more time consuming than I would have thought. Finding documentation is a challenge though lots of code examples are very helpful. 

My latest version is attached. Some improvements:

  • A timer is working that refreshes the Security Token, currently hard-coded to 20 minutes
  • Children devices are created upon successful authentication: but how do I create proper children devices??? and is my creation code correct??
  • I have code to inspect the statues of doors along with change their statuses

So, I need to create my children devices, right now I just said they are DoorLocks but have not done anything beyond that
Creating the devices with lock/unlock buttons along with actions and a timer to update the door statuses would seem to be the final steps.
Anyone with some guidance for me?
It does appear that I'm getting closer... but hopefully someone will give some additional pointers?

EDIT
Helps to actually attach the zip file
« Last Edit: April 23, 2014, 03:05:14 pm by macrho »

Offline guessed

  • Master Member
  • *******
  • Posts: 5294
  • Karma: +90/-22
  • Release compat is not a bolted-on afterthought
Re: Chamberlain/Liftmaster MyQ Plugin [WIP]
« Reply #10 on: April 23, 2014, 03:12:04 pm »
A few quick comments to get you moving to the next step(s):

a) Child devices are programatically/explicitly created in your code during startup* processing.
Look for sample code that uses the trio
    luup.chdev.start()
    luup.chdev.append()
    luup.chdev.sync()

You'll see examples of these in the startup of the Weather plugin
    http://code.mios.com/trac/mios_weather/browser/trunk/I_WUIWeather.xml#L346

I'm looking at the last posted ZIP of the content, so you probably have some of these changes in a newer ZIP.

b) Add some constants for your serviceId
These can just be internal constants, since only your code will use them, but it'll save you headaches later on should you mistype the service Id strings (I've done this loads of times)

c) The <files> tag is a UI5 ism.
This isn't a problem, as long as you want your code only to run in UI5.  That said, it's not hard to transform your .lua file into a real lua module, and we can give you the required snippets to include it (as a module, not just a ball of code) into your I_ file when you're ready.

d) Parameter/Plugin validations.
When you're ready, you'll want to read up on luup.task, and look at the samples in code.mios.com.  You can use these to give the user more obvious error output when parameters aren't specified (etc)

e) For un/pw encoding, you can use the standard one that comes with LuaSocket (http).
This will save you getting all of the encoding combinations correct, since it's basically written for you.  I don't remember it's exact name right now, but take a look at the documentation for LuaSocket and you'll see it.

f) Conventions.
Again, not required, but it's common to choose a single format for variables and another for constants.

eg. connectionResult, openerInfo, parentName, parentId
eg. APP_ID, BASE_URL, VALIDATION_PATH

Up to you exactly what you use in your code, but remember to use it consistently throughout ... at least in those cases where you're making the declaration (you can't help the ones created by the JSON Parser)

g) It's possible that you're JSON response won't be JSON.
and in this case you just need to trap that scenario, since it'll cause the current code to stop executing.
eg. if authresponseData doesn't have a ReturnCode field, of if auth_response table is empty (or not JSON)





* At least, officially they need to be.  There was some work done to make this possible, but it hasn't been documented.

macrho

  • Guest
Re: Chamberlain/Liftmaster MyQ Plugin [WIP]
« Reply #11 on: April 23, 2014, 05:38:55 pm »
Thanks @guessed. I must say that my head is starting to spin from all these moving parts and it's so difficult to find complete examples with comments. The somfy walk-through was help for a bit of it but where is the source? You'd think that there'd be a nice zip file to download at the bottom of the page (though perhaps I didn't see it -- when I looked on the store, there were 2 plugins, didn't bother downloading, perhaps I should).

Anyway..

a) I think my updated code is doing this properly .. maybe :)

b) I'll fix up all the constants later, I've been finding snippets of code that might work, paste, modify, repeat. But I agree I should be consistent

c) I'll follow-up with you if I ever figure out how to create this plugin

d) I'm looking into it now, I do have some returns if authorization fails that displays a reason in the UI

e) Is this what you mean?

url = require("socket.url")
code = url.escape("myname@myemail.com")

f) I will definitely do this

g) Will work on this though am doing this now, not sure if sufficient:

Code: [Select]
  --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


I'm really having a brain fart now. I can create the child devices but I'm lost on how I actually hook up my methods to set the status of the door or to open/close a door. I thought I could use this in the child setup:

Code: [Select]
      luup.chdev.append( lul_device,              --parent (this device)
                     child_devices,           --Pointer from above start call
       openerInfo[i].DeviceId,     --Our child ID taken from the opener device id
       openerInfo[i].OpenerName,                     --Child device description
       "urn:micasaverde-com:serviceId:DoorLock1", --ServiceId defined in device file
     "D_DoorLock1.xml",                         --Device file
     "",                      --No implementation file required
     "",                      --No parameters to set
     false)                   --Not embedded child device (can go in any room)


Perhaps this has been just too much for me to digest too quickly :)

Offline guessed

  • Master Member
  • *******
  • Posts: 5294
  • Karma: +90/-22
  • Release compat is not a bolted-on afterthought
Re: Chamberlain/Liftmaster MyQ Plugin [WIP]
« Reply #12 on: April 23, 2014, 09:06:25 pm »
Quote
I must say that my head is starting to spin from all these moving parts and it's so difficult to find complete examples with comments. The somfy walk-through was help for a bit of it but where is the source? You'd think that there'd be a nice zip file to download at the bottom of the page (though perhaps I didn't see it -- when I looked on the store, there were 2 plugins, didn't bother downloading, perhaps I should).
A lot has been documented... about MCV's lack of documentation  8)

a) Children...
I think I missed one of the attachments last time around, so I'll recheck what you next post "in one hit".

d) Parameter/Plugin validations.
Have a look at the following const/fn definitions in the Weather Plugin:
    TASK_ERROR, TASK_ERROR_PERM, TASK_SUCCESS, TASK_BUSY
    local function task
    function clearTask -- must be declared public (without the "local" at the front)

    http://code.mios.com/trac/mios_weather/browser/trunk/I_WUIWeather.xml#L45

These are just convenience wrappers for luup.task, which is the most basic way to put the "red banner" at the top of the page indicating that there's a problem, or a status update.

Feel free to use them, or the underlying stuff as you see fit.  You can look at the rest of that code to see some simple example uses.....  there's more that can be done but it should give you a taste.

Quote
e) Is this what you mean?

url = require("socket.url")
code = url.escape("myname@myemail.com")
Yup, that'll encode the parameter part of the URL correctly.  You still glue the string bits together, but each variable needs to be escaped prior to doing that (inline is fine) to avoid data-value problems creeping in (among other things)

Quote
g) Will work on this though am doing this now, not sure if sufficient:
What happens if non JSON data is returned or invalid.

ie. how does the JSON parser handle a nil value for auth_response[1], how does it handle a blank value, how does it handle a non JSON value.

These will give you a few extra checks since it's likely that you'll get nil as a result somewhere along the lines, and the code will bump-out.

Quote
I'm really having a brain fart now. I can create the child devices but I'm lost on how I actually hook up my methods to set the status of the door or to open/close a door. I thought I could use this in the child setup:
Events enter the plugin through <ACTION> handler blocks in the I_xxxx.xml file.  One of the parameters to each of the ACTION calls is lul_device (amoung others, see examples).

There are a fixed #/type of these ACTIONS that can be called, depending upon what children you have (etc)

Here's a very simple one from the Weather Plugin:
    http://code.mios.com/trac/mios_weather/browser/trunk/I_WUIWeather.xml#L418

It handles the "Button" presses on the Dashboard to let the user switch "hot" between Metric and Imperial units.  The Button of the UI, behind the json file, has a binding to call this when the user presses the button to switch to Metric (etc)


In other words
  • a) the luup.chdev stuff declares that you want children, and what they should look like (a Button, a Thermostat, etc)
  • b) the <ACTION> Blocks define what to do when the user interacts with the component
  • c) <handleChildren>1</handleChildren> indicates that you want one (1) Implementation file to handle all of the events for all Children.
That's not all of it, but it should help you may to whatever event system you've worked with before.

Users will click "Open" or "Close", or the counterpart actions through a URL, and your <ACTION> block will be called (more details coming, get a one-door case working).

To do the reverse, you need to reset the current "value" (StateVariable) of one of these children.  To do this, to use luup.variable_set

eg.
    local SWITCH_SID = "urn:upnp-org:serviceId:SwitchPower1"

    ...
    -- Turn on a light as defined by deviceId "lul_device"
    luup.variable_set(SWITCH_SID, "Status", 1, lul_device)


There are serviceId's like this for light switches, dimmers, thermostats (etc) and each of these has a bunch of these stateVariables (like "Status") that can be set upon them depending upon what you're doing.

You might do this, for example, when you poll the WebService, and need to store the state back again to keep the UI up to date.


Give it time, once you've done all the pieces, and you can see it all together, it'll make a whole lot more sense.

macrho

  • Guest
Re: Chamberlain/Liftmaster MyQ Plugin [WIP]
« Reply #13 on: April 24, 2014, 02:00:28 pm »
guessed, one quick question:

The ActionList directive needs to be within an XML file, right? So I need to remove:
Code: [Select]
   <files>L_LiftMasterOpener.lua</files>

and stuff everything in there between <function> and </function>

I've attempted that but then lose any ability to have Notepad++ or ZeroBrane do any context highlighting

I also uploaded my changes to my Vera and it appears that my luup.call_delay function doesn't work though I see no errors

Anyway, I'm wondering if I'm going about this completely wrong.

I was thinking that I could create my child devices like:

Code: [Select]
      luup.chdev.append( lul_device,              --parent (this device)
                     child_devices,           --Pointer from above start call
             openerInfo[i].DeviceId,     --Our child ID taken from the opener device id
             openerInfo[i].OpenerName,                     --Child device description
             "urn:micasaverde-com:serviceId:DoorLock1", --ServiceId defined in device file
             "D_DoorLock1.xml",                         --Device file
             "S_DoorLock1.xml",                      --No implementation file required
             "",                      --No parameters to set
             false)                   --Not embedded child device (can go in any room)
      child_IDs[i] = openerInfo[i].DeviceId

and then somehow I could hook up the status and lock/unlock actions but I don't know how to get there-- I was hoping there was a way to override the default doorlock device..

I'm still in a Land of Confusion, I should queue up some Genesis to play :)


or can I do something like:

Code: [Select]
<?xml version="1.0"?>
<implementation>
    <settings>
        <protocol>cr</protocol>
    </settings>
   <files>L_LiftMasterOpener.lua</files>
   <startup>init</startup>
   <actionList>
     <action>
   <serviceId>urn:micasaverde-com:serviceId:DoorLock1</serviceId>
   <name>SetTarget</name>
   <run>
   local deviceStatResult, deviceStatText = changeDeviceStatus(openerInfo[1].DeviceId, APPID,  "desireddoorstate", 1, SecurityToken)
   </run>
</action>
   </actionList>
</implementation>
« Last Edit: April 24, 2014, 02:28:19 pm by macrho »

Offline guessed

  • Master Member
  • *******
  • Posts: 5294
  • Karma: +90/-22
  • Release compat is not a bolted-on afterthought
Re: Chamberlain/Liftmaster MyQ Plugin [WIP]
« Reply #14 on: April 24, 2014, 03:36:08 pm »
The <files> element is telling Vera that there's some code in another location that it needs to load in addition to whatever it's already loaded as part of the Implementation XML file.

... so it augments any code/config already in the Implementation file, and doesn't replace it.

Think of it as "#include <yourLuaStuff>" ;)


Quote
I've attempted that but then lose any ability to have Notepad++ or ZeroBrane do any context highlighting
Side-bar on Notepad, be careful with this editor and XML files.  Just don't save them as UTF-8, since it puts some preamble on the front that messes with Vera in a big way.

Quote
I also uploaded my changes to my Vera and it appears that my luup.call_delay function doesn't work though I see no errors
I'll have an eyeball of the code, but you should check the logs also (/var/log/cmh/LuaUPnP.log), since any error will be spat out there when Vera is running.  In particular, look for lines that start with "01".

eg. grep ^01 /var/log/cmh/LuaUPnP.log



In your luup.chdev.append calls, the parameters look off.  You don't pass a serviceId here, since the "device" being instantiated already declares it's list of services in it's D_xxxx.xml file.  You also don't need to pass the implementation file (I_xxxxx.xml, not S_xxxxx.xml) since it's [default] implementation is listed inside the D_xxxxx.xml file.

Confused?  Take a look at the Weather plugin:
    http://code.mios.com/trac/mios_weather/browser/trunk/I_WUIWeather.xml#L346

At this stage, I would attempt to mimic a Door Sensor, and reflect the "open/closed" state of the Garage door in that.  It'll be a simpler start.

Relevant parameter values can be see in the convenience method used by the DSC Plugin, when it's asked to construct a "door" (vs a Window or Smoke sensor):
    http://code.mios.com/trac/mios_dscalarmpanel/browser/trunk/I_DSCAlarmPanel1.xml#L1057



Then we can resurrect the call_delay to "fill in" the values, and you can test that working altogether before moving on to making changes via Vera's UI.