Luup Somfy Walkthrough

From MiOS
(Difference between revisions)
Jump to: navigation, search
m (Step 2b: Add your device by building the XML files by hand)
(Syntax color the Lua code, fix a few typo's)
Line 52: Line 52:
 
Open D_TestSerial.xml in your text editor and re-save it as a different name, such as D_SomfyBlinds.xml. Change the xml field deviceType to "urn:somfy-com:device:blinds:1" or use your own domain name instead of somfy-com. Change friendlyName to "Somfy Blind Controller", manufacturer to "Somfy", manufacturerURL to "somfy.com", modelDescription to "16 port RS232 to Somfy blind interface", modelName to "1810686" (the Somfy part number). modelURL and serialNumber and UPC aren't really important so you can just remove them. Add in their place a protocol tag with the value 'raw', and handlechildren (we'll explain this one later) with the value 1, like this:  
 
Open D_TestSerial.xml in your text editor and re-save it as a different name, such as D_SomfyBlinds.xml. Change the xml field deviceType to "urn:somfy-com:device:blinds:1" or use your own domain name instead of somfy-com. Change friendlyName to "Somfy Blind Controller", manufacturer to "Somfy", manufacturerURL to "somfy.com", modelDescription to "16 port RS232 to Somfy blind interface", modelName to "1810686" (the Somfy part number). modelURL and serialNumber and UPC aren't really important so you can just remove them. Add in their place a protocol tag with the value 'raw', and handlechildren (we'll explain this one later) with the value 1, like this:  
  
   <protocol>raw</protocol>
+
<source lang="xml">
&lt;handleChildren&gt;1&lt;/handleChildren&gt;
+
   <protocol>raw</protocol>
 +
  <handleChildren>1</handleChildren>
 +
</source>
  
 
The 'raw' protocol is because the Somfy device doesn't have any particular low level protocol, like terminating blocks of data with a carriage return, etc. Remove all the tags in 'servicelist'.  
 
The 'raw' protocol is because the Somfy device doesn't have any particular low level protocol, like terminating blocks of data with a carriage return, etc. Remove all the tags in 'servicelist'.  
Line 61: Line 63:
 
Open I_TestSerial.xml and delete the sample Lua code within the 'actionList' tag. We have to put some Lua code in the implementation file for the Luup engine to start it, so for now just put a placeholder in the 'functions' xml tag:  
 
Open I_TestSerial.xml and delete the sample Lua code within the 'actionList' tag. We have to put some Lua code in the implementation file for the Luup engine to start it, so for now just put a placeholder in the 'functions' xml tag:  
  
function lug_startup(lul_device)
+
<source lang="lua">
lu_log("Somfy blind #" .. lul_device .. " starting up with ID " .. lug_device[lul_device].ID)
+
  function lug_startup(lul_device)
end
+
    luup.log("Somfy blind #" .. lul_device .. " starting up with ID " .. lug_device[lul_device].ID)
 +
  end
 +
</source>
  
 
and in the 'startup' xml tag, put: ''lug_startup'', like this:  
 
and in the 'startup' xml tag, put: ''lug_startup'', like this:  
  
&lt;startup&gt;lug_startup&lt;/startup&gt;
+
<source lang="xml">
 +
  <startup>lug_startup</startup>
 +
</source>
  
 
So we've created a Lua function, lug_startup, told the Luup engine to call it when the engine is starting up. The name lug_startup is arbitrary, it can be anything you want. Save the file as I_SomfyBlinds.xml.  
 
So we've created a Lua function, lug_startup, told the Luup engine to call it when the engine is starting up. The name lug_startup is arbitrary, it can be anything you want. Save the file as I_SomfyBlinds.xml.  
Line 105: Line 111:
 
The way to implement multiple children is with the 'Reporting child devices' commands documented in [[Luup_Lua_extensions]].  This is how a parent device, in this case the Somfy blind interface, reports to the Luup engine what child devices it has and what ID number it will use internally to keep track of each one.  Every device in Luup has an "ID" value which has no meaning to the Luup engine itself, but which is used by parent devices to keep track of the children.  Many interface devices have a way to get from them the list of child devices so the parent can manage the children automatically.  The Somfy doesn't.  So we have 2 choices: 1) Just automatically report 16 child devices since the interface supports 16 devices, or 2) Let the user indicate which Somfy blind numbers he has and report just those child devices.  The advantage of #1 is that it's very simple, but the drawback is that the user will see 16 blinds in his home, even if he only has 1.  So, we will do it with #2.  The first question is where to let the user store the list of blind numbers that are active.  The usual way to store this information is to create a UPnP service which contains variables for all the various parameters the user should specify to configure the device.  Creating a UPnP service description document and adding it to the device description is a lot of work to get only 1 parameter: a list of device numbers.  But, what we can do is instead just create a variable in Lua using some service id/variable we made up, like this:
 
The way to implement multiple children is with the 'Reporting child devices' commands documented in [[Luup_Lua_extensions]].  This is how a parent device, in this case the Somfy blind interface, reports to the Luup engine what child devices it has and what ID number it will use internally to keep track of each one.  Every device in Luup has an "ID" value which has no meaning to the Luup engine itself, but which is used by parent devices to keep track of the children.  Many interface devices have a way to get from them the list of child devices so the parent can manage the children automatically.  The Somfy doesn't.  So we have 2 choices: 1) Just automatically report 16 child devices since the interface supports 16 devices, or 2) Let the user indicate which Somfy blind numbers he has and report just those child devices.  The advantage of #1 is that it's very simple, but the drawback is that the user will see 16 blinds in his home, even if he only has 1.  So, we will do it with #2.  The first question is where to let the user store the list of blind numbers that are active.  The usual way to store this information is to create a UPnP service which contains variables for all the various parameters the user should specify to configure the device.  Creating a UPnP service description document and adding it to the device description is a lot of work to get only 1 parameter: a list of device numbers.  But, what we can do is instead just create a variable in Lua using some service id/variable we made up, like this:
  
   luup.variable_set("urn:micasaverde-com:serviceId:SomfyBlinds1","BlindIds",lul_ID,lul_device)
+
<source lang="lua">
 +
   luup.variable_set("urn:micasaverde-com:serviceId:SomfyBlinds1", "BlindIds", lul_ID, lul_device)
 +
</source>
  
 
This won't be an 'official' UPnP variable and won't show up in a UPnP scanner.  But, it does show up in Vera's user interface as a variable the user can edit the value.  We want to be sure we set this to a default value if it's not already there so the user sees it in the UI and can change it.
 
This won't be an 'official' UPnP variable and won't show up in a UPnP scanner.  But, it does show up in Vera's user interface as a variable the user can edit the value.  We want to be sure we set this to a default value if it's not already there so the user sees it in the UI and can change it.
Line 111: Line 119:
 
Now we'll add the Lua code to iterate through all the blinds and create the devices, which will go in the lug_startup function we created earlier.  We could put the code directly in the XML implementation file.  But that means saving/uploading the file each time, and is a bit more tedious.  It's often easier to debug it first in the 'Test Luup code' window.  So we'll this to test first.  First, put in the code box: ''lug_startup(123)'' but change the 123 to the actual device number of the Somfy blind, and click 'go'.  Switch back to the ssh console that is tail'ing the LuaUPnP.log.  You'll see that it logged ''Somfy blind #... starting up with ID''.  This is because we already created the lug_startup function in the implementation file, and in the code box we just called that function.  Now put this in the code box:
 
Now we'll add the Lua code to iterate through all the blinds and create the devices, which will go in the lug_startup function we created earlier.  We could put the code directly in the XML implementation file.  But that means saving/uploading the file each time, and is a bit more tedious.  It's often easier to debug it first in the 'Test Luup code' window.  So we'll this to test first.  First, put in the code box: ''lug_startup(123)'' but change the 123 to the actual device number of the Somfy blind, and click 'go'.  Switch back to the ssh console that is tail'ing the LuaUPnP.log.  You'll see that it logged ''Somfy blind #... starting up with ID''.  This is because we already created the lug_startup function in the implementation file, and in the code box we just called that function.  Now put this in the code box:
  
function lug_startup(lul_device)
+
<source lang="lua">
  local lul_ID="01,02"
+
  function lug_startup(lul_device)
  luup.variable_set("urn:micasaverde-com:serviceId:SomfyBlinds1","BlindIds",lul_ID,lul_device)
+
    local lul_ID="01,02"
  lu_log("test2--Somfy blind #" .. lul_device .. " starting up with ID " .. lul_ID)
+
 
end
+
    luup.variable_set("urn:micasaverde-com:serviceId:SomfyBlinds1","BlindIds", lul_ID, lul_device)
lug_startup(123)
+
    luup.log("test2--Somfy blind #" .. lul_device .. " starting up with ID " .. lul_ID)
 +
  end
 +
 
 +
  lug_startup(123)
 +
</source>
  
 
where 123 should be your actual device number for the device.  The Lua code in ''function lug_startup'' will cause the lug_startup function that was already in the implementation file to be replaced with this version, without actually executing the function (just updating it), so afterward we also have lug_startup(123) to actually run the new version we updated.  When you click 'go' you'll see in the ssh log that the it now says ''test2--Somfy blind'', so you can see the lug_startup function was replaced.  Note that any functions you create in the 'Test Luup Code' window will still stay in the Luup engine until you reload the Luup engine.  So if you type in the code box:
 
where 123 should be your actual device number for the device.  The Lua code in ''function lug_startup'' will cause the lug_startup function that was already in the implementation file to be replaced with this version, without actually executing the function (just updating it), so afterward we also have lug_startup(123) to actually run the new version we updated.  When you click 'go' you'll see in the ssh log that the it now says ''test2--Somfy blind'', so you can see the lug_startup function was replaced.  Note that any functions you create in the 'Test Luup Code' window will still stay in the Luup engine until you reload the Luup engine.  So if you type in the code box:
  
 +
<source lang="lua">
 
   function some_test()
 
   function some_test()
     lu_log("some_test")
+
     luup.log("some_test")
 
   end
 
   end
 +
</source>
  
 
click 'go', nothing will get logged.  If you then erase the code box and type: ''some_test()'' when you click 'go', you'll see "some_test" in the log.  If you want to update the some_test function, and run it at the same time, do:
 
click 'go', nothing will get logged.  If you then erase the code box and type: ''some_test()'' when you click 'go', you'll see "some_test" in the log.  If you want to update the some_test function, and run it at the same time, do:
  
 +
<source lang="lua">
 
   function some_test()
 
   function some_test()
     lu_log("some_test2")
+
     luup.log("some_test2")
 
   end
 
   end
 +
 
   some_test()
 
   some_test()
 +
</source>
  
 
Now when you click 'go', you'll see "some_test2" be logged.  So we'll create a simple startup that creates all 16 blinds by putting this in the Test Luup code window:
 
Now when you click 'go', you'll see "some_test2" be logged.  So we'll create a simple startup that creates all 16 blinds by putting this in the Test Luup code window:
  
function lug_startup(lul_device)
+
<source lang="lua">
  child_devices = lu_chdev_start(lul_device);
+
  function lug_startup(lul_device)
  for i=1,16 do
+
    child_devices = luup.chdev.start(lul_device);
    s=string.format("%02d", i)
+
    for i = 1,16 do
    lu_log("Adding blind "..s)
+
      s = string.format("%02d", i)
    lu_chdev_append(lul_device,child_devices,s,"Blind #"..s,"urn:schemas-upnp-org:device:BinaryLight:1","","","",true)
+
      luup.log("Adding blind " .. s)
  end
+
      luup.chdev.append(lul_device, child_devices, s, "Blind #" .. s, "urn:schemas-upnp-org:device:BinaryLight:1", "", "", "", true)
  lu_chdev_sync(lul_device,child_devices)
+
    end
end
+
lug_startup(123)
+
  
The ''for i=1,16'' is runs the block before 'end' 16 times, assigning the variable 'i' to 1 to 16 each time. We want the devices to be 0 padded to 2 digits, so 1 should be "01". This is because the ID which the Somfy needs is a zero-padded 2 digit number.  It also will make it easier to filter just the blinds we want to control as explained later.  The statement ''s=string.format("%02d", i)'' converts the numeric value 'i' to a string that is padded to decimal places.  This is documented in Lua (see [http://www.lua.org/pil/20.html#StringLib]).
+
    luup.chdev.sync(lul_device, child_devices)
 +
  end
  
lu_chdev_start() takes as a parameter our device id (ie the parent device) and it tells the Luup engine we're going to start listing all our children.  It returns a handle which we pass in to to the other chdev_ functions.  lu_chdev_append() adds each child and it takes the parent device number, the handle from lu_chdev_start, the id the parent will use to identify the device (s=01, 02, etc.), and the device type.  This is a UPnP device.  We'll use the existing UPnP standard "Binary light" because this way every UPnP control point will treat the blinds as a light and show the user an 'on' and 'off' button.  We could create our own device type for blinds which had actions "open" and "close", but then the UPnP Control Points (like the iPhone interface) would need to be updated to know how to present the user with blinds.  lu_chdev_sync() is called when we're done to synchronize the child device list with Luup's database, and create/remove any new/missing devices, and restart the Luup engine if devices were added or removed.  See: [[Luup_Lua_extensions]]
+
  lug_startup(123)
 +
</source>
 +
 
 +
The ''for i=1,16'' is runs the block before 'end' 16 times, assigning the variable 'i' to 1 to 16 each time.  We want the devices to be 0 padded to 2 digits, so 1 should be "01".  This is because the ID which the Somfy needs is a zero-padded 2 digit number.  It also will make it easier to filter just the blinds we want to control as explained later.  The statement ''s = string.format("%02d", i)'' converts the numeric value 'i' to a string that is padded to decimal places.  This is documented in Lua (see [http://www.lua.org/pil/20.html#StringLib]).
 +
 
 +
luup.chdev.start() takes as a parameter our device id (ie the parent device) and it tells the Luup engine we're going to start listing all our children.  It returns a handle which we pass in to to the other chdev_ functions.  luup.chdev.append() adds each child and it takes the parent device number, the handle from lu_chdev_start, the id the parent will use to identify the device (s=01, 02, etc.), and the device type.  This is a UPnP device.  We'll use the existing UPnP standard "Binary light" because this way every UPnP control point will treat the blinds as a light and show the user an 'on' and 'off' button.  We could create our own device type for blinds which had actions "open" and "close", but then the UPnP Control Points (like the iPhone interface) would need to be updated to know how to present the user with blinds.  luup.chdev.sync() is called when we're done to synchronize the child device list with Luup's database, and create/remove any new/missing devices, and restart the Luup engine if devices were added or removed.  See: [[Luup_Lua_extensions]]
  
 
Before you click 'go', restart your tail in the ssh window with: ''tail -f LuaUPnP.log | grep '^5\|^01\|^03\|^09\|^11' '' because the 03 logs indicate when the Luup engine stops and restart, the 09 logs show us when devices are created/deleted, and the 11 logs show when the child device functions are calls.  Now when you click 'go', you should see in the logs ''Child_Devices::AddChild'' is called 16 times, and then 16 times ''Child_Devices::ProcessChildDevice created device''.  You should also see lines starting with '03' that show the Luup engine is reloading since new devices were just created.  When it restarts, you'll see 16 new devices in the logs starting with 09.
 
Before you click 'go', restart your tail in the ssh window with: ''tail -f LuaUPnP.log | grep '^5\|^01\|^03\|^09\|^11' '' because the 03 logs indicate when the Luup engine stops and restart, the 09 logs show us when devices are created/deleted, and the 11 logs show when the child device functions are calls.  Now when you click 'go', you should see in the logs ''Child_Devices::AddChild'' is called 16 times, and then 16 times ''Child_Devices::ProcessChildDevice created device''.  You should also see lines starting with '03' that show the Luup engine is reloading since new devices were just created.  When it restarts, you'll see 16 new devices in the logs starting with 09.
Line 152: Line 173:
 
Now if you click 'go' again, you'll see 16 logs for ''Child_Devices::AddChild'', but no devices will be created or removed because the 16 devices were already created, and the parent/child device lists are still in sync.  We have the same 16 child devices with no new ones or removed ones.
 
Now if you click 'go' again, you'll see 16 logs for ''Child_Devices::AddChild'', but no devices will be created or removed because the 16 devices were already created, and the parent/child device lists are still in sync.  We have the same 16 child devices with no new ones or removed ones.
  
Now if you refresh Vera's web UI, you'll see the Somfy device has '16' embedded devices, each with "on" and "Off".  The 'true' at the end of the lu_chdev_append means the child devices are 'embedded'.  That means when they're shown in Vera's web UI, they're all grouped together as one compound device.  If you changed the 'true' to a 'false', all 16 blinds would show up as completely separate devices.  This is really just a cosmetic difference.  The advantage to using 'false' (non-embedded) is that the user can put each blind in a different room.  In this case it's probably better to treat the devices as non-embedded, since it is likely the 16 blinds will be scattered around the house.  So change the true at the end of lu_chdev_append to a false.  Then click 'go'.  Now, the 16 blinds are removed, and re-added as non-embedded devices.  So, if you refresh Vera's web UI and go to devices, you'll 16 different blinds that you can put into different rooms, in addition to the "Somfy blind interface" device.
+
Now if you refresh Vera's web UI, you'll see the Somfy device has '16' embedded devices, each with "on" and "Off".  The 'true' at the end of the luup.chdev.append means the child devices are 'embedded'.  That means when they're shown in Vera's web UI, they're all grouped together as one compound device.  If you changed the 'true' to a 'false', all 16 blinds would show up as completely separate devices.  This is really just a cosmetic difference.  The advantage to using 'false' (non-embedded) is that the user can put each blind in a different room.  In this case it's probably better to treat the devices as non-embedded, since it is likely the 16 blinds will be scattered around the house.  So change the true at the end of luup.chdev.append to a false.  Then click 'go'.  Now, the 16 blinds are removed, and re-added as non-embedded devices.  So, if you refresh Vera's web UI and go to devices, you'll 16 different blinds that you can put into different rooms, in addition to the "Somfy blind interface" device.
  
 
Next, we'll update our Lua code to only create child devices that are specified in the "ID" parameter of the "Somfy blind interface" so that if the user doesn't have 16 devices he can specify just the ones he does have.  To make our coding easier, we'll document in the notes for the user that when specifying the list of blinds in the "ID" he must use 01,02, etc., rather than 1,2, etc. because by storing all child devices as 2 digits, we can do a simple search without having to actually parse the ID.  In other words, if we want to know if blind #1 is active, we can safely search the ID for 01 and know that it will only match if blind #1 is active.  If the user stored single digit blind numbers, like ''5,6,11'' then searching for '1' would give us a false positive and we'd need to write more Lua code to break the list apart.  This also makes it easier to send the blind commands because the Somfy protocol says the blind number must be 2 digits, so we won't need to pad the ID's with 0 for the Somfy device.  This is why we did the for loop with 16 text strings using string.format which would have given us normal, non-zero-padded numbers.
 
Next, we'll update our Lua code to only create child devices that are specified in the "ID" parameter of the "Somfy blind interface" so that if the user doesn't have 16 devices he can specify just the ones he does have.  To make our coding easier, we'll document in the notes for the user that when specifying the list of blinds in the "ID" he must use 01,02, etc., rather than 1,2, etc. because by storing all child devices as 2 digits, we can do a simple search without having to actually parse the ID.  In other words, if we want to know if blind #1 is active, we can safely search the ID for 01 and know that it will only match if blind #1 is active.  If the user stored single digit blind numbers, like ''5,6,11'' then searching for '1' would give us a false positive and we'd need to write more Lua code to break the list apart.  This also makes it easier to send the blind commands because the Somfy protocol says the blind number must be 2 digits, so we won't need to pad the ID's with 0 for the Somfy device.  This is why we did the for loop with 16 text strings using string.format which would have given us normal, non-zero-padded numbers.
  
Put a -- in front of the lu_chdev_append in the window and click 'go' again.  -- means "comments" in Lua, and lines starting with -- are ignored.  Therefore, by eliminating the lu_chdev_append, now the lu_chdev_sync will remove all 16 children since none were appended.  This way we can reload Vera's web UI, go to the devices page, and we won't have those 16 blinds anymore requiring us to provide a room.  Click '+' next to the Somfy blind interface, click 'Advanced' and in the BlindIds enter: '02,07,13', click 'save'.  Now in the 'Test Luup code' window enter:
+
Put a -- in front of the luup.chdev.append in the window and click 'go' again.  -- means "comments" in Lua, and lines starting with -- are ignored.  Therefore, by eliminating the luup.chdev.append, now the luup.chdev.sync will remove all 16 children since none were appended.  This way we can reload Vera's web UI, go to the devices page, and we won't have those 16 blinds anymore requiring us to provide a room.  Click '+' next to the Somfy blind interface, click 'Advanced' and in the BlindIds enter: '02,07,13', click 'save'.  Now in the 'Test Luup code' window enter:
  
function lug_startup(lul_device)
+
<source lang="lua">
  local lul_ID = luup.variable_get("urn:micasaverde-com:serviceId:SomfyBlinds1","BlindIds",lul_device)
+
  function lug_startup(lul_device)
  if( lul_ID==nill ) then
+
    local lul_ID = luup.variable_get("urn:micasaverde-com:serviceId:SomfyBlinds1", "BlindIds", lul_device)
    lul_ID = "01,02"
+
    if (lul_ID == nil) then
    luup.variable_set("urn:micasaverde-com:serviceId:SomfyBlinds1","BlindIds",lul_ID,lul_device)
+
      lul_ID = "01,02"
  end
+
      luup.variable_set("urn:micasaverde-com:serviceId:SomfyBlinds1", "BlindIds", lul_ID, lul_device)
  lul_prefix = luup.variable_get("urn:micasaverde-com:serviceId:SomfyBlinds1","UrtsiId",lul_device)
+
    end
  if( lul_prefix==nill ) then
+
 
    lul_prefix = "01"
+
    local lul_prefix = luup.variable_get("urn:micasaverde-com:serviceId:SomfyBlinds1", "UrtsiId", lul_device)
    luup.variable_set("urn:micasaverde-com:serviceId:SomfyBlinds1","UrtsiId",lul_prefix,lul_device)
+
    if (lul_prefix == nil) then
 +
      lul_prefix = "01"
 +
      luup.variable_set("urn:micasaverde-com:serviceId:SomfyBlinds1", "UrtsiId", lul_prefix, lul_device)
 
   end
 
   end
 +
 
   luup.log("Somfy ID is " .. lul_ID .. " prefix is " .. lul_prefix)
 
   luup.log("Somfy ID is " .. lul_ID .. " prefix is " .. lul_prefix)
   child_devices = lu_chdev_start(lul_device);
+
   child_devices = luup.chdev.start(lul_device);
   for i=1,16 do
+
   for i = 1,16 do
     s=string.format("%02d", i)
+
     s = string.format("%02d", i)
     if( string.find (lul_ID,s)~=nill ) then
+
     if (string.find(lul_ID,s) ~= nil) then
       lu_log("Adding blind "..s)
+
       luup.log("Adding blind " .. s)
       lu_chdev_append(lul_device,child_devices,s,"Blind #"..s,"urn:schemas-upnp-org:device:BinaryLight:1","","","",false)
+
       luup.chdev.append(lul_device, child_devices, s, "Blind #" .. s, "urn:schemas-upnp-org:device:BinaryLight:1", "", "", "", false)
 
     end
 
     end
 
   end
 
   end
   lu_chdev_sync(lul_device,child_devices)
+
 
 +
   luup.chdev.sync(lul_device, child_devices)
 
  end
 
  end
 +
 
  lug_startup(123)
 
  lug_startup(123)
 +
</source>
  
Don't forget to change the '123' to your actual device number.  Now when you click 'go' you'll see the logs indicate we only have 3 blinds with ID's 02, 07 and 13.  Since this part of the code is done, you can copy/paste this from the Luup test code window into the implementation.  '''IMPORTANT:''' Be sure the ''lug_startup(123)'' is NOT in your implementation file, or else your lug_startup function will be called twice at each startup: once by the Luup engine with the actual device number, and once when the startup functions are loaded into the Lua engine with your hard-coded number used for testing, which won't necessarily correspond to the real device number.  We also add the variable lul_prefix because the URTS version 1 takes an ! in the front of every command, whereas with version 2 it's a 2 digit number from 01 to 16.  So we'll default to 01, the default value for an URTS version 2, but we'll let the user chagne it.  We'll store this in a global variable, lul_prefix, so it's available for use in the other functions in our plugin.
+
Don't forget to change the '123' to your actual device number.  Now when you click 'go' you'll see the logs indicate we only have 3 blinds with ID's 02, 07 and 13.  Since this part of the code is done, you can copy/paste this from the Luup test code window into the implementation.  '''IMPORTANT:''' Be sure the ''lug_startup(123)'' is NOT in your implementation file, or else your lug_startup function will be called twice at each startup: once by the Luup engine with the actual device number, and once when the startup functions are loaded into the Lua engine with your hard-coded number used for testing, which won't necessarily correspond to the real device number.  We also add the variable lul_prefix because the URTS version 1 takes an ! in the front of every command, whereas with version 2 it's a 2 digit number from 01 to 16.  So we'll default to 01, the default value for an URTS version 2, but we'll let the user change it.  We'll store this in a global variable, lul_prefix, so it's available for use in the other functions in our plugin.
  
 
If you're editing the XML file by hand, you can save your changes and click 'go' in the Luup files upload page in your web browser to upload your changes.
 
If you're editing the XML file by hand, you can save your changes and click 'go' in the Luup files upload page in your web browser to upload your changes.
Line 194: Line 221:
 
Now, the code first checks for basic startup parameters, which are stored in UPNP variables, and sets them to a default value if they're not already specified.  You should at least set them an empty string because by setting them to something then the user will see them in the web user interface and be able to easily change them, as opposed to creating them again from scratch:
 
Now, the code first checks for basic startup parameters, which are stored in UPNP variables, and sets them to a default value if they're not already specified.  You should at least set them an empty string because by setting them to something then the user will see them in the web user interface and be able to easily change them, as opposed to creating them again from scratch:
  
    local lul_ID = luup.variable_get("urn:micasaverde-com:serviceId:SomfyBlinds1","BlindIds",lul_device)
+
<source lang="lua">
    if( lul_ID==nill ) then
+
  local lul_ID = luup.variable_get("urn:micasaverde-com:serviceId:SomfyBlinds1", "BlindIds", lul_device)
      lul_ID = "01,02"
+
  if (lul_ID = nil) then
      luup.variable_set("urn:micasaverde-com:serviceId:SomfyBlinds1","BlindIds",lul_ID,lul_device)
+
    lul_ID = "01,02"
    end
+
    luup.variable_set("urn:micasaverde-com:serviceId:SomfyBlinds1", "BlindIds", lul_ID, lul_device)
 +
  end
 +
</source>
  
 
Next, since this device uses an IO Port (ie a serial port, or an ethernet port, or a usb connection), we should check that the connection is specified and is active before doing the startup:
 
Next, since this device uses an IO Port (ie a serial port, or an ethernet port, or a usb connection), we should check that the connection is specified and is active before doing the startup:
  
    if( luup.io.is_connected(lul_device)==false ) then
+
<source lang="lua">
      luup.log('No port for Somfy',1)
+
  if (luup.io.is_connected(lul_device) == false) then
      luup.task('Choose the Serial Port for the URTSI',2,'Somfy Blind Interface',-1)
+
    luup.log('No port for Somfy', 1)
      return false
+
    luup.task('Choose the Serial Port for the URTSI', 2, 'Somfy Blind Interface', -1)
    end
+
    return false
 +
  end
 +
</source>
  
 
If there's a failure of any kind, we should call luup.task and put in some sort of description with the status code 2 (Error), and then 'return false'.  This way the user sees in the web panel that the module 'Somfy Blind Interface' is in an error condition and what the problem is (he didn't choose the serial port).  If we didn't do the 'return false', the user would think the Somfy module was ok since, when the startup function doesn't return false, the architecture assumes the module is running fine.  If we called return false, didn't call luup.task, then the user see that the module was failing, but would have only a generic "Startup sequence failed" without knowing the explicit reason.
 
If there's a failure of any kind, we should call luup.task and put in some sort of description with the status code 2 (Error), and then 'return false'.  This way the user sees in the web panel that the module 'Somfy Blind Interface' is in an error condition and what the problem is (he didn't choose the serial port).  If we didn't do the 'return false', the user would think the Somfy module was ok since, when the startup function doesn't return false, the architecture assumes the module is running fine.  If we called return false, didn't call luup.task, then the user see that the module was failing, but would have only a generic "Startup sequence failed" without knowing the explicit reason.
Line 212: Line 243:
 
So now the startup sequence looks like this:
 
So now the startup sequence looks like this:
  
  function lug_startup(lul_device)
+
<source lang="lua">
    local lul_ID = luup.variable_get("urn:micasaverde-com:serviceId:SomfyBlinds1","BlindIds",lul_device)
+
  function lug_startup(lul_device)
    if( lul_ID==nill ) then
+
    local lul_ID = luup.variable_get("urn:micasaverde-com:serviceId:SomfyBlinds1", "BlindIds", lul_device)
 +
    if (lul_ID == nil) then
 
       lul_ID = "01,02"
 
       lul_ID = "01,02"
       luup.variable_set("urn:micasaverde-com:serviceId:SomfyBlinds1","BlindIds",lul_ID,lul_device)
+
       luup.variable_set("urn:micasaverde-com:serviceId:SomfyBlinds1", "BlindIds", lul_ID, lul_device)
    end
+
    end
    lul_prefix = luup.variable_get("urn:micasaverde-com:serviceId:SomfyBlinds1","UrtsiId",lul_device)
+
 
    if( lul_prefix==nill ) then
+
    local lul_prefix = luup.variable_get("urn:micasaverde-com:serviceId:SomfyBlinds1", "UrtsiId", lul_device)
 +
    if (lul_prefix == nil) then
 
       lul_prefix = "01"
 
       lul_prefix = "01"
       luup.variable_set("urn:micasaverde-com:serviceId:SomfyBlinds1","UrtsiId",lul_prefix,lul_device)
+
       luup.variable_set("urn:micasaverde-com:serviceId:SomfyBlinds1", "UrtsiId", lul_prefix, lul_device)
    end
+
    end
    luup.log("Somfy ID is " .. lul_ID .. " prefix is " .. lul_prefix)
+
 
    if( luup.io.is_connected(lul_device)==false ) then
+
    luup.log("Somfy ID is " .. lul_ID .. " prefix is " .. lul_prefix)
       luup.log('No port for Somfy',1)
+
 
       luup.task('Choose the Serial Port for the URTSI',2,'Somfy Blind Interface',-1)
+
    if (luup.io.is_connected(lul_device) == false) then
 +
       luup.log('No port for Somfy', 1)
 +
       luup.task('Choose the Serial Port for the URTSI',2,'Somfy Blind Interface', -1)
 
       return false
 
       return false
    end
+
    end
    child_devices = luup.chdev.start(lul_device);
+
 
    for i=1,16 do
+
    child_devices = luup.chdev.start(lul_device);
      s=string.format("%02d", i)
+
    for i = 1,16 do
      if( string.find (lul_ID,s)~=nill ) then
+
      s = string.format("%02d", i)
        luup.log("Adding blind "..s)
+
      if (string.find (lul_ID,s) ~= nil) then
        luup.chdev.append(lul_device,child_devices,s,"Blind #"..s,"urn:schemas-micasaverde-com:device:WindowCovering:1","D_WindowCovering1.xml","","",false)
+
        luup.log("Adding blind " .. s)
      end
+
        luup.chdev.append(lul_device, child_devices, s, "Blind #" .. s, "urn:schemas-micasaverde-com:device:WindowCovering:1", "D_WindowCovering1.xml", "", "", false)
    end
+
      end
    luup.chdev.sync(lul_device,child_devices)
+
    end
  end
+
 
 +
    luup.chdev.sync(lul_device, child_devices)
 +
  end
 +
</source>
  
 
==Step 6a: Implementation the actions in the web generator ==
 
==Step 6a: Implementation the actions in the web generator ==
Line 248: Line 286:
 
The reason we added the ''<handleChildren>1</handleChildren>'' tag to the Somfy blind interface's device file earlier is because we're going to put all the actions for the child devices (the blinds) within the main implementation file for the Somfy.  So, whenever an action comes in for a child device (a blind), the Lua code in the parent devices implementation file will handle it.  Replace the actionList tag in the implementation file with this:
 
The reason we added the ''<handleChildren>1</handleChildren>'' tag to the Somfy blind interface's device file earlier is because we're going to put all the actions for the child devices (the blinds) within the main implementation file for the Somfy.  So, whenever an action comes in for a child device (a blind), the Lua code in the parent devices implementation file will handle it.  Replace the actionList tag in the implementation file with this:
  
 +
<source lang="xml">
 
   <actionList>
 
   <actionList>
 
     <action>
 
     <action>
Line 254: Line 293:
 
       <run>
 
       <run>
 
         local lul_command = lul_prefix .. luup.devices[lul_device].id .. 'U\r'
 
         local lul_command = lul_prefix .. luup.devices[lul_device].id .. 'U\r'
         local lul_reverse = luup.variable_get("urn:micasaverde-com:serviceId:HaDevice1","ReverseOnOff",lul_device)
+
         local lul_reverse = luup.variable_get("urn:micasaverde-com:serviceId:HaDevice1", "ReverseOnOff", lul_device)
         if( lul_settings.newTargetValue=="1" or (lul_settings.newTargetValue=="0" and lul_reverse=="1") ) then
+
 
 +
         if (lul_settings.newTargetValue == "1" or (lul_settings.newTargetValue == "0" and lul_reverse == "1")) then
 
           lul_command = lul_prefix .. luup.devices[lul_device].id .. 'D\r'
 
           lul_command = lul_prefix .. luup.devices[lul_device].id .. 'D\r'
 
         end
 
         end
         if luup.io.write(lul_command)==false then
+
 
 +
         if (luup.io.write(lul_command) == false) then
 
           luup.log("cannot send: " .. tostring(lul_command),1)
 
           luup.log("cannot send: " .. tostring(lul_command),1)
 
           luup.set_failure(true)
 
           luup.set_failure(true)
Line 266: Line 307:
 
     </action>
 
     </action>
 
   </actionList>
 
   </actionList>
 +
</source>
  
 
This provides the implementation or the SetTarget UPnP action, which is what is sent when the user clicks 'on' or 'off' for a light switch, or 'open' or 'close' for blinds.  We store the command to send in the variable lul_command and put the 'local' keyword in front so the variable is only available while the 'SetTarget' action is running.  We also check the HADevice service's ReverseOnOff value.  As a convention in Luup, we created this as a common service that all home automation devices have, and the ReverseOnOff variable, if true, means reverse the usual on/off behavior, so on is off and off is on.  We did this because it's not uncommon for binary switches, particularly blinds, to be wired the wrong way, or for it to be subjective which position is on vs. off.  This way the user can add this variable for his configuration if the operation is backwards from what he would expect.  lu_SetCommFailure sets a "communication failure" flag for the device, which logs a critical error, and allows the user to see that the device is having problems.  Whenever the Luup engine reloads, such as clicking 'save', the flag is cleared again.
 
This provides the implementation or the SetTarget UPnP action, which is what is sent when the user clicks 'on' or 'off' for a light switch, or 'open' or 'close' for blinds.  We store the command to send in the variable lul_command and put the 'local' keyword in front so the variable is only available while the 'SetTarget' action is running.  We also check the HADevice service's ReverseOnOff value.  As a convention in Luup, we created this as a common service that all home automation devices have, and the ReverseOnOff variable, if true, means reverse the usual on/off behavior, so on is off and off is on.  We did this because it's not uncommon for binary switches, particularly blinds, to be wired the wrong way, or for it to be subjective which position is on vs. off.  This way the user can add this variable for his configuration if the operation is backwards from what he would expect.  lu_SetCommFailure sets a "communication failure" flag for the device, which logs a critical error, and allows the user to see that the device is having problems.  Whenever the Luup engine reloads, such as clicking 'save', the flag is cleared again.
  
 
Now when you click 'on' or 'off' for the blinds you'll see in the log it is sending the corresponding data to the blinds.
 
Now when you click 'on' or 'off' for the blinds you'll see in the log it is sending the corresponding data to the blinds.

Revision as of 22:41, 4 September 2011

This page is for developers wanting to learn how to write a plugin. If you simply want to use the Somfy plugin see: Somfy_Plugin

This page walks through in detail the process of creating a Luup plugin to control the Somfy RS232 interface for motorized blinds as a sample. The Somfy documentation (see: [1]) explains that you talk to the controller using RS232 9600 Baud, 8 Data Bits, 1 Stop Bit, No Parity. The interface supports up to 16 blinds. The communication is one-way only; there are no response codes or incoming data from the blinds when they change manually. Here is the relevant section from the manual.

RS232 Operation
1. Set RS232 communication settings to: 9600 Baud, 8 Data Bits, 1 Stop Bit, No Parity
2. Use the ASCII protocol command string syntax: <!> <2 digit channel number> <Directional Command>
3. The directional commands must be capital letters and are as follows:
  U: UP
  D: DOWN
  S: STOP
4. Examples: Motor 2 Up: !02U
  Motor 5 Down: !05D
  Motor 1 Stop: !01S

Contents

Step 1: SSH into Vera

Since this device is so simple it may not be necessary to debug anything. But we'll do this anyway. You need to give Vera a root password, which is the administrator password to login directly. Do this by either: a) from a command prompt in Windows, Linux or Mac type telnet [ip address of vera] and when you see the root@HomeControl:~# prompt enter: passwd and type in a password twice. Then type exit. Or b) In Vera's web ui click Advanced, Net & Wi-fi and the 'Advanced Configuration' link, and supply the password there. Note, sometimes Windows Vista doesn't have the telnet utility installed, so you have to use step b.

Once you have a root password set, you will no longer be able to use telnet to login to Vera, you must use ssh. The reason you should use ssh to login for debugging, and not telnet, is because some of the useful key sequences, like Ctrl+C, don't work in telnet.

Once you've setup a root password, from a Mac or Linux console type: ssh [ip of vera] From a Windows PC, download putty.exe here: [2]. You don't need to install putty, just put the .exe on your desktop or in a folder. When you run putty.exe, type in Vera's IP address and click 'open'.

When you see login as:, enter: root and then enter your password. Now from the root@HomeControl:# prompt type:

 cd /var/log/cmh
 tail -f LuaUPnP.log

Do something in Vera's web ui and you'll see the logs go. Whenever the system rotates the logs, meaning purges the log files because Vera's memory is limited for storing logs, your tail will end. Press the 'up' arrow and hit 'enter' to display the tail command again and restart it. Press Ctrl + c when you've confirm the logs are working ok.

In Vera's web ui, choose Advanced, Logs, and check 'Verbose logging'. This option will create large logs that have lots of information we may need for debugging. It will stay checked for 24 hours and then automatically turn itself off so you're not creating unnecessarily large log files.

Step 2a: Add your device with the web generator tool

 --coming soon.  for now use step 2b.

Step 2b: Add your device by building the XML files by hand

To do this you'll want a good text editor. I'm using, such as Notepad++ available here: [3] by clicking Download, Download Notepad++ executable files, download and run the npp....Installer.exe file, and you may also want to download the 'XML plugin' from the download page, and unzip it putting the file \Program Files\Notepad++\plugins.

In Vera's setup UI, go to Devices, Luup plugins, click 'Luup files' and download the files:

D_BinaryLight1.xml since there is no UPnP device specification for blinds, and blinds are essentially binary devices that are either up/down, we can implement the up/down using the same service that a light switch uses, and that way any UPnP control point that can control a light switch, will also be able to control blinds.

S_SwitchPower1.xml because when you open D_BinaryLight1.xml, you'll see that in the 'services' section, this is the filename (SCPDURL) for the SwitchPower service.

D_TestSerial.xml because this is a sample serial device we can use as a template.

I_TestSerial.xml because this is a sample implementation for our serial device.

Open D_TestSerial.xml in your text editor and re-save it as a different name, such as D_SomfyBlinds.xml. Change the xml field deviceType to "urn:somfy-com:device:blinds:1" or use your own domain name instead of somfy-com. Change friendlyName to "Somfy Blind Controller", manufacturer to "Somfy", manufacturerURL to "somfy.com", modelDescription to "16 port RS232 to Somfy blind interface", modelName to "1810686" (the Somfy part number). modelURL and serialNumber and UPC aren't really important so you can just remove them. Add in their place a protocol tag with the value 'raw', and handlechildren (we'll explain this one later) with the value 1, like this:

  <protocol>raw</protocol>
  <handleChildren>1</handleChildren>

The 'raw' protocol is because the Somfy device doesn't have any particular low level protocol, like terminating blocks of data with a carriage return, etc. Remove all the tags in 'servicelist'.

Change the filename in the implementationFile tag from I_TestSerial.xml to I_SomfyBlinds.xml.

Open I_TestSerial.xml and delete the sample Lua code within the 'actionList' tag. We have to put some Lua code in the implementation file for the Luup engine to start it, so for now just put a placeholder in the 'functions' xml tag:

  function lug_startup(lul_device)
    luup.log("Somfy blind #" .. lul_device .. " starting up with ID " .. lug_device[lul_device].ID)
  end

and in the 'startup' xml tag, put: lug_startup, like this:

  <startup>lug_startup</startup>

So we've created a Lua function, lug_startup, told the Luup engine to call it when the engine is starting up. The name lug_startup is arbitrary, it can be anything you want. Save the file as I_SomfyBlinds.xml.

Back on Vera's setup ui under 'Luup files' you the first 'upload' button to upload D_SomfyBlinds.xml and the 2nd to upload I_SomfyBlinds.xml. Check Restart Luup after upload because the files won't be processed unless we restart the Luup engine, and click 'go'. Now leave your browser tab open at this page so that as we change the implementation file we can just click 'go' without having to select the files again.

In another browser tab go to Vera's device page again and under 'Add device' put the new device filename D_SomfyBlinds.xml in the input box, and choose the room where the blind controller is located, click 'Add device'. You'll have a new, blank device in that room, so give it a description "Somfy Blind Controller", then click 'save'.

Note that if the Somfy device was bi-direction we should attempt to communicate with it during the startup sequence and the startup function should return false if the sequence failed, or true if it succeeded, followed by some comments and the name of the module, which information is shown to the user in the info panel. So: return false,'Interface not responding','Somfy Blind' or return true,'Initialized OK','Somfy Blind'

Step 3: Setup the port

Vera talks to serial devices using a serial port proxy that turns the serial port into a network port. This way the serial port can reside anywhere on the network. Here are 3 ways to connect Vera to the Somfy device:

1. Get the UC232R-10 usb->serial adapter here: [4] Connect it to one of Vera's USB ports. In Vera's web ui, click the 'save' button, which, even if the 'save' button is grayed out, causes the Luup engine to re-initialize and scan for new serial ports. Wait 30 seconds.

2. Use any Windows COM port or USB->serial port device. Download Windows_Serial_Proxy and run it as explained there. Wait 30 seconds after running it and it will exit after it has reported the port to the Luup engine.

3. Connect a Global Cache GC-100 to your network. Wait 30 seconds or so for the Luup engine to find it. This should happen automatically. Bring up the GC100's setup page in your web browser; you can find the IP address in Vera's device page under the + sign, and set the baud/parity/etc. for the port to match. In the case of the GC-100, this information is supplied to the GC-100 itself and the values in the Serial Port Configuration page have no effect.

After you've done 1, 2 or 3, go to Devices, Luup plugins and click "Serial port configuration". The serial port should be on the list. Use the pull-down's to set the serial port options: baud=9600, Data Bits=8, Stop Bits=1, Parity=none. For the 'Use with device', select your Somfy Blind controller. Choose 'Save'.

Step 4: Test the port

First, make note of the device id for your Somfy blinds by clicking '+' next to the device in the device list. Next, go to Devices, Luup Plugins, Test Luup code. In the 'Device number' input box put the device number for the Somfy blinds. This way whatever code we test uses the Lua instance for the Somfy blinds, which will be configured already to use the serial port.

In the 'Code' input box, type: luup.io.write('!01U') and click 'Go'. That is a command according to the Somfy specs, which should make motor #1 go up. You can try other commands and click 'Go' each time. If everything is working ok, skip to step 6. If it's not working, you will want to do some debugging. Here's some debug things to try:

1. Be sure you checked 'Verbose logging' in Step 1. In your ssh console (ie putty if you're using windows) enter tail -f LuaUPnP.log | grep '^5\|^01' where the | means to send the output through the grep utility, which will filter out only certain lines. The ^ means 'lines that start with'. Line that start with 5 are logs related to Lua, and lines that start with 01 are critical errors. The \| means 'or' for grep. Now go back and click 'go' again in the Test Luup window. Return to the ssh console and you should see a line that starts with 51, which means data sent within Luup, that shows !01U, in this format: 51 06/29/09 17:19:53.453 0x21 0x30 0x31 0x55 0xd 0xa (!01U\r\n), where the human-readable ascii text is in () at the end, following the binary/hex. If you're going to be switching back and forth between the ssh console and web ui to do tests, then before you switch to the web ui, you can either hit 'enter' a few times in the ssh console to add some blank space, or press Ctrl+c and then 'up' followed by 'enter' to restart tail. That way you've created some separation between existing log entries and new ones so you can clearly see what is happening when you click 'go'. If you're not seeing anything, in the Test Luup window add the line lu_log('test somfy') above the luup.io.write. You should see 'test somfy' get logged when you press 'go'.

2. Assuming you do see the line '51' log entry showing that the Luup engine is trying to send data, you may want to check the serial port itself. You can go back to the 'serial port configuration' and remove the 'Somfy blind' device from the serial port so the Luup engine won't open the port. Make a note of the network ip and port for the serial port. Click 'Save' to save your changes. Now from a command prompt run: telnet [ip] [port]. If you don't have telnet, open another putty session and click the 'telnet' radio box, put in the ip address and port and click 'open'. You should now be able to type the commands in the telnet session: !01U and see the blinds work. If it still doesn't work, try connecting the blinds directly to your PC and using a terminal program, like Hyperterminal, to talk directly to the serial port and confirm the connections are ok.

Step 5: Create the child devices for the blinds

In Luup, when you have an interface device which is able to control multiple devices this is represented as a tree where the interface is a 'virtual' device, that probably doesn't implement any actions itself, and multiple 'child devices' under the interface device for each device the interface can control. The Somfy Blind controller is such a device because the 1 interface can control 16 blinds. If it only controlled 1 blind, we wouldn't need to implement the parent/multiple child architecture, because we would have just 1 device which implemented the blind functions. We also wouldn't need to create any functions in Lua and we'd simply have put the control protocol in the 'action' tags in the implementation XML. But we want a parent 'Somfy interface' with up to 16 different child devices for each of the 16 blinds the device can control. This means it's a bit more complicated, so we'll want to use Lua functions, like 'lug_startup'.

The way to implement multiple children is with the 'Reporting child devices' commands documented in Luup_Lua_extensions. This is how a parent device, in this case the Somfy blind interface, reports to the Luup engine what child devices it has and what ID number it will use internally to keep track of each one. Every device in Luup has an "ID" value which has no meaning to the Luup engine itself, but which is used by parent devices to keep track of the children. Many interface devices have a way to get from them the list of child devices so the parent can manage the children automatically. The Somfy doesn't. So we have 2 choices: 1) Just automatically report 16 child devices since the interface supports 16 devices, or 2) Let the user indicate which Somfy blind numbers he has and report just those child devices. The advantage of #1 is that it's very simple, but the drawback is that the user will see 16 blinds in his home, even if he only has 1. So, we will do it with #2. The first question is where to let the user store the list of blind numbers that are active. The usual way to store this information is to create a UPnP service which contains variables for all the various parameters the user should specify to configure the device. Creating a UPnP service description document and adding it to the device description is a lot of work to get only 1 parameter: a list of device numbers. But, what we can do is instead just create a variable in Lua using some service id/variable we made up, like this:

  luup.variable_set("urn:micasaverde-com:serviceId:SomfyBlinds1", "BlindIds", lul_ID, lul_device)

This won't be an 'official' UPnP variable and won't show up in a UPnP scanner. But, it does show up in Vera's user interface as a variable the user can edit the value. We want to be sure we set this to a default value if it's not already there so the user sees it in the UI and can change it.

Now we'll add the Lua code to iterate through all the blinds and create the devices, which will go in the lug_startup function we created earlier. We could put the code directly in the XML implementation file. But that means saving/uploading the file each time, and is a bit more tedious. It's often easier to debug it first in the 'Test Luup code' window. So we'll this to test first. First, put in the code box: lug_startup(123) but change the 123 to the actual device number of the Somfy blind, and click 'go'. Switch back to the ssh console that is tail'ing the LuaUPnP.log. You'll see that it logged Somfy blind #... starting up with ID. This is because we already created the lug_startup function in the implementation file, and in the code box we just called that function. Now put this in the code box:

  function lug_startup(lul_device)
    local lul_ID="01,02"
 
    luup.variable_set("urn:micasaverde-com:serviceId:SomfyBlinds1","BlindIds", lul_ID, lul_device)
    luup.log("test2--Somfy blind #" .. lul_device .. " starting up with ID " .. lul_ID)
  end
 
  lug_startup(123)

where 123 should be your actual device number for the device. The Lua code in function lug_startup will cause the lug_startup function that was already in the implementation file to be replaced with this version, without actually executing the function (just updating it), so afterward we also have lug_startup(123) to actually run the new version we updated. When you click 'go' you'll see in the ssh log that the it now says test2--Somfy blind, so you can see the lug_startup function was replaced. Note that any functions you create in the 'Test Luup Code' window will still stay in the Luup engine until you reload the Luup engine. So if you type in the code box:

  function some_test()
    luup.log("some_test")
  end

click 'go', nothing will get logged. If you then erase the code box and type: some_test() when you click 'go', you'll see "some_test" in the log. If you want to update the some_test function, and run it at the same time, do:

  function some_test()
    luup.log("some_test2")
  end
 
  some_test()

Now when you click 'go', you'll see "some_test2" be logged. So we'll create a simple startup that creates all 16 blinds by putting this in the Test Luup code window:

  function lug_startup(lul_device)
    child_devices = luup.chdev.start(lul_device);
    for i = 1,16 do
      s = string.format("%02d", i)
      luup.log("Adding blind " .. s)
      luup.chdev.append(lul_device, child_devices, s, "Blind #" .. s, "urn:schemas-upnp-org:device:BinaryLight:1", "", "", "", true)
    end
 
    luup.chdev.sync(lul_device, child_devices)
  end
 
  lug_startup(123)

The for i=1,16 is runs the block before 'end' 16 times, assigning the variable 'i' to 1 to 16 each time. We want the devices to be 0 padded to 2 digits, so 1 should be "01". This is because the ID which the Somfy needs is a zero-padded 2 digit number. It also will make it easier to filter just the blinds we want to control as explained later. The statement s = string.format("%02d", i) converts the numeric value 'i' to a string that is padded to decimal places. This is documented in Lua (see [5]).

luup.chdev.start() takes as a parameter our device id (ie the parent device) and it tells the Luup engine we're going to start listing all our children. It returns a handle which we pass in to to the other chdev_ functions. luup.chdev.append() adds each child and it takes the parent device number, the handle from lu_chdev_start, the id the parent will use to identify the device (s=01, 02, etc.), and the device type. This is a UPnP device. We'll use the existing UPnP standard "Binary light" because this way every UPnP control point will treat the blinds as a light and show the user an 'on' and 'off' button. We could create our own device type for blinds which had actions "open" and "close", but then the UPnP Control Points (like the iPhone interface) would need to be updated to know how to present the user with blinds. luup.chdev.sync() is called when we're done to synchronize the child device list with Luup's database, and create/remove any new/missing devices, and restart the Luup engine if devices were added or removed. See: Luup_Lua_extensions

Before you click 'go', restart your tail in the ssh window with: tail -f LuaUPnP.log | grep '^5\|^01\|^03\|^09\|^11' because the 03 logs indicate when the Luup engine stops and restart, the 09 logs show us when devices are created/deleted, and the 11 logs show when the child device functions are calls. Now when you click 'go', you should see in the logs Child_Devices::AddChild is called 16 times, and then 16 times Child_Devices::ProcessChildDevice created device. You should also see lines starting with '03' that show the Luup engine is reloading since new devices were just created. When it restarts, you'll see 16 new devices in the logs starting with 09.

Now if you click 'go' again, you'll see 16 logs for Child_Devices::AddChild, but no devices will be created or removed because the 16 devices were already created, and the parent/child device lists are still in sync. We have the same 16 child devices with no new ones or removed ones.

Now if you refresh Vera's web UI, you'll see the Somfy device has '16' embedded devices, each with "on" and "Off". The 'true' at the end of the luup.chdev.append means the child devices are 'embedded'. That means when they're shown in Vera's web UI, they're all grouped together as one compound device. If you changed the 'true' to a 'false', all 16 blinds would show up as completely separate devices. This is really just a cosmetic difference. The advantage to using 'false' (non-embedded) is that the user can put each blind in a different room. In this case it's probably better to treat the devices as non-embedded, since it is likely the 16 blinds will be scattered around the house. So change the true at the end of luup.chdev.append to a false. Then click 'go'. Now, the 16 blinds are removed, and re-added as non-embedded devices. So, if you refresh Vera's web UI and go to devices, you'll 16 different blinds that you can put into different rooms, in addition to the "Somfy blind interface" device.

Next, we'll update our Lua code to only create child devices that are specified in the "ID" parameter of the "Somfy blind interface" so that if the user doesn't have 16 devices he can specify just the ones he does have. To make our coding easier, we'll document in the notes for the user that when specifying the list of blinds in the "ID" he must use 01,02, etc., rather than 1,2, etc. because by storing all child devices as 2 digits, we can do a simple search without having to actually parse the ID. In other words, if we want to know if blind #1 is active, we can safely search the ID for 01 and know that it will only match if blind #1 is active. If the user stored single digit blind numbers, like 5,6,11 then searching for '1' would give us a false positive and we'd need to write more Lua code to break the list apart. This also makes it easier to send the blind commands because the Somfy protocol says the blind number must be 2 digits, so we won't need to pad the ID's with 0 for the Somfy device. This is why we did the for loop with 16 text strings using string.format which would have given us normal, non-zero-padded numbers.

Put a -- in front of the luup.chdev.append in the window and click 'go' again. -- means "comments" in Lua, and lines starting with -- are ignored. Therefore, by eliminating the luup.chdev.append, now the luup.chdev.sync will remove all 16 children since none were appended. This way we can reload Vera's web UI, go to the devices page, and we won't have those 16 blinds anymore requiring us to provide a room. Click '+' next to the Somfy blind interface, click 'Advanced' and in the BlindIds enter: '02,07,13', click 'save'. Now in the 'Test Luup code' window enter:

  function lug_startup(lul_device)
    local lul_ID = luup.variable_get("urn:micasaverde-com:serviceId:SomfyBlinds1", "BlindIds", lul_device)
    if (lul_ID == nil) then
      lul_ID = "01,02"
      luup.variable_set("urn:micasaverde-com:serviceId:SomfyBlinds1", "BlindIds", lul_ID, lul_device)
    end
 
    local lul_prefix = luup.variable_get("urn:micasaverde-com:serviceId:SomfyBlinds1", "UrtsiId", lul_device)
    if (lul_prefix == nil) then
      lul_prefix = "01"
      luup.variable_set("urn:micasaverde-com:serviceId:SomfyBlinds1", "UrtsiId", lul_prefix, lul_device)
   end
 
   luup.log("Somfy ID is " .. lul_ID .. " prefix is " .. lul_prefix)
   child_devices = luup.chdev.start(lul_device);
   for i = 1,16 do
     s = string.format("%02d", i)
     if (string.find(lul_ID,s) ~= nil) then
       luup.log("Adding blind " .. s)
       luup.chdev.append(lul_device, child_devices, s, "Blind #" .. s, "urn:schemas-upnp-org:device:BinaryLight:1", "", "", "", false)
     end
   end
 
   luup.chdev.sync(lul_device, child_devices)
 end
 
 lug_startup(123)

Don't forget to change the '123' to your actual device number. Now when you click 'go' you'll see the logs indicate we only have 3 blinds with ID's 02, 07 and 13. Since this part of the code is done, you can copy/paste this from the Luup test code window into the implementation. IMPORTANT: Be sure the lug_startup(123) is NOT in your implementation file, or else your lug_startup function will be called twice at each startup: once by the Luup engine with the actual device number, and once when the startup functions are loaded into the Lua engine with your hard-coded number used for testing, which won't necessarily correspond to the real device number. We also add the variable lul_prefix because the URTS version 1 takes an ! in the front of every command, whereas with version 2 it's a 2 digit number from 01 to 16. So we'll default to 01, the default value for an URTS version 2, but we'll let the user change it. We'll store this in a global variable, lul_prefix, so it's available for use in the other functions in our plugin.

If you're editing the XML file by hand, you can save your changes and click 'go' in the Luup files upload page in your web browser to upload your changes.

Every time you re-start the Luup engine, such as clicking 'Save', you'll see in the logs this startup sequence.

Step 5b: Polish up the startup sequence

The the startup sequence as it is the user will always think the Somfy module loaded fine, even when it didn't, giving the user a false sense that everything was ok even when he hadn't yet specified basic parameters.

Now, the code first checks for basic startup parameters, which are stored in UPNP variables, and sets them to a default value if they're not already specified. You should at least set them an empty string because by setting them to something then the user will see them in the web user interface and be able to easily change them, as opposed to creating them again from scratch:

  local lul_ID = luup.variable_get("urn:micasaverde-com:serviceId:SomfyBlinds1", "BlindIds", lul_device)
  if (lul_ID = nil) then
    lul_ID = "01,02"
    luup.variable_set("urn:micasaverde-com:serviceId:SomfyBlinds1", "BlindIds", lul_ID, lul_device)
  end

Next, since this device uses an IO Port (ie a serial port, or an ethernet port, or a usb connection), we should check that the connection is specified and is active before doing the startup:

  if (luup.io.is_connected(lul_device) == false) then
    luup.log('No port for Somfy', 1)
    luup.task('Choose the Serial Port for the URTSI', 2, 'Somfy Blind Interface', -1)
    return false
  end

If there's a failure of any kind, we should call luup.task and put in some sort of description with the status code 2 (Error), and then 'return false'. This way the user sees in the web panel that the module 'Somfy Blind Interface' is in an error condition and what the problem is (he didn't choose the serial port). If we didn't do the 'return false', the user would think the Somfy module was ok since, when the startup function doesn't return false, the architecture assumes the module is running fine. If we called return false, didn't call luup.task, then the user see that the module was failing, but would have only a generic "Startup sequence failed" without knowing the explicit reason.

So now the startup sequence looks like this:

  function lug_startup(lul_device)
    local lul_ID = luup.variable_get("urn:micasaverde-com:serviceId:SomfyBlinds1", "BlindIds", lul_device)
    if (lul_ID == nil) then
      lul_ID = "01,02"
      luup.variable_set("urn:micasaverde-com:serviceId:SomfyBlinds1", "BlindIds", lul_ID, lul_device)
    end
 
    local lul_prefix = luup.variable_get("urn:micasaverde-com:serviceId:SomfyBlinds1", "UrtsiId", lul_device)
    if (lul_prefix == nil) then
      lul_prefix = "01"
      luup.variable_set("urn:micasaverde-com:serviceId:SomfyBlinds1", "UrtsiId", lul_prefix, lul_device)
    end
 
    luup.log("Somfy ID is " .. lul_ID .. " prefix is " .. lul_prefix)
 
    if (luup.io.is_connected(lul_device) == false) then
      luup.log('No port for Somfy', 1)
      luup.task('Choose the Serial Port for the URTSI',2,'Somfy Blind Interface', -1)
      return false
    end
 
    child_devices = luup.chdev.start(lul_device);
    for i = 1,16 do
      s = string.format("%02d", i)
      if (string.find (lul_ID,s) ~= nil) then
        luup.log("Adding blind " .. s)
        luup.chdev.append(lul_device, child_devices, s, "Blind #" .. s, "urn:schemas-micasaverde-com:device:WindowCovering:1", "D_WindowCovering1.xml", "", "", false)
      end
    end
 
    luup.chdev.sync(lul_device, child_devices)
  end

Step 6a: Implementation the actions in the web generator

--coming soon--

Step 6b: Implementation the actions in the xml file

The reason we added the <handleChildren>1</handleChildren> tag to the Somfy blind interface's device file earlier is because we're going to put all the actions for the child devices (the blinds) within the main implementation file for the Somfy. So, whenever an action comes in for a child device (a blind), the Lua code in the parent devices implementation file will handle it. Replace the actionList tag in the implementation file with this:

  <actionList>
    <action>
      <serviceId>urn:upnp-org:serviceId:SwitchPower1</serviceId>
      <name>SetTarget</name>
      <run>
        local lul_command = lul_prefix .. luup.devices[lul_device].id .. 'U\r'
        local lul_reverse = luup.variable_get("urn:micasaverde-com:serviceId:HaDevice1", "ReverseOnOff", lul_device)
 
        if (lul_settings.newTargetValue == "1" or (lul_settings.newTargetValue == "0" and lul_reverse == "1")) then
          lul_command = lul_prefix .. luup.devices[lul_device].id .. 'D\r'
        end
 
        if (luup.io.write(lul_command) == false) then
          luup.log("cannot send: " .. tostring(lul_command),1)
          luup.set_failure(true)
          return false
        end
      </run>
    </action>
  </actionList>

This provides the implementation or the SetTarget UPnP action, which is what is sent when the user clicks 'on' or 'off' for a light switch, or 'open' or 'close' for blinds. We store the command to send in the variable lul_command and put the 'local' keyword in front so the variable is only available while the 'SetTarget' action is running. We also check the HADevice service's ReverseOnOff value. As a convention in Luup, we created this as a common service that all home automation devices have, and the ReverseOnOff variable, if true, means reverse the usual on/off behavior, so on is off and off is on. We did this because it's not uncommon for binary switches, particularly blinds, to be wired the wrong way, or for it to be subjective which position is on vs. off. This way the user can add this variable for his configuration if the operation is backwards from what he would expect. lu_SetCommFailure sets a "communication failure" flag for the device, which logs a critical error, and allows the user to see that the device is having problems. Whenever the Luup engine reloads, such as clicking 'save', the flag is cleared again.

Now when you click 'on' or 'off' for the blinds you'll see in the log it is sending the corresponding data to the blinds.

Personal tools