Smart Lua Scripts

From Domoticz
Jump to navigation Jump to search

Introduction

This wiki describes how combining Lua scripting with a clever device-naming convention based on the use of name-prefixes can make existing dumb devices smarter.

The idea is simple: By giving a device (real or dummy) in Domoticz a name constructed by adding a pre-determined prefix to an existing device/group name, Lua is used to detect when *any* device with that prefix changes state and to carry out a specific action on an existing physical device/group defined already in Domoticz (and named without using the helper-prefix). So as explained below, the combination of device/time scripts on a motion sensor with a fixed name-prefix of e.g. 'PIR' allows a dumb motion sensor which only detects motion (and has no own 'off' action) to be used as a proper PIR where a light is switched on for a set length of time before being switched off again automatically.

Some further extensions (see later) add flexibility so that the same scripts can be configured to act differently for different devices.

Turning a Simple PIR into a Smart PIR

The Siemens motion detector based on the Byron PIR is a simple motion detector, which never turns off. The detector sends an 'On' signal using the Byron SX protocol, but never sends an 'Off' signal, so the detector is permanently 'On'. Using this detector to switch on a light is ok, but the light then has to be switched off manually. The idea behind these scripts is to make this simple PIR more useful by giving it an automatic 'Off' functionality.

Adding Turning Off Functionality

First, in Domoticz add a new Siemens PIR and name it after a light in your house (e.g. the "Study" light, so the PIR is named "PIRStudy") and make its device type "Motion Sensor". Then add the scripts described below.

Device Script to Turn a Light On

Now create the DEVICE script 'script_device_PIRs.lua' and save this in the Domoticz script directory.

-- ~/domoticz/scripts/lua/script_device_PIRs.lua

commandArray = {}

tc=next(devicechanged)
v=tostring(tc)
if (v:sub(1,3) == 'PIR') then
    c=v:sub(4)
    commandArray[c] = 'On'
    tmess = c..' On - time 0'
    print(tmess)
end

return commandArray

The script simply checks to see if any named device that changes state starts with the letters "PIR". If it does then the script takes the rest of the name as the name of a light and sends that light the 'On' command. It also writes to the log to say that the device is On at time 0.

Now each time you activate the motion sensor in question, Domoticz will send the On command to that light. Note that it is not good practice in Domoticz to send unecessary commands so if your system has feedback and you can guarantee that the light is really 'On', then you should test the status and only send the command if the light is not already 'On'. In my system I have no feedback, so the recorded state for the light in Domoticz may well be incorrect.

Time Script to Turn a Light Off

Now create the TIME script 'script_time_PIRs.lua' and save this in the Domoticz script directory.

-- ~/domoticz/scripts/lua/script_time_PIRs.lua

function timedifference(s)
   year = string.sub(s, 1, 4)
   month = string.sub(s, 6, 7)
   day = string.sub(s, 9, 10)
   hour = string.sub(s, 12, 13)
   minutes = string.sub(s, 15, 16)
   seconds = string.sub(s, 18, 19)
   t1 = os.time()
   t2 = os.time{year=year, month=month, day=day, hour=hour, min=minutes, sec=seconds}
   difference = os.difftime (t1, t2)
   return difference
end

commandArray = {}

for i, v in pairs(otherdevices) do
   timeon = 240
   tc = tostring(i)
   v = i:sub(1,3)
   if (v == 'PIR') then
      difference = timedifference(otherdevices_lastupdate[tc])
      if (difference > timeon and difference < (timeon + 60)) then
         tempdiff = tostring(difference)
         c = i:sub(4)
         tempmessage = c.." Light Off - after at least " .. (timeon+1) .. "secs up - actually - " .. tempdiff .. "seconds"
         print(tempmessage)
         commandArray[c] = 'Off'
      end
   end
end

return commandArray

This script is bit more complex to program, but very simple in concept. The function 'timedifference' is defined to calculate the difference in time between now and when the PIR was last activated.

All TIME scripts are called every minute, and so do not need a device change to activate them. This script checks all devices, until it finds a device where the device name start with "PIR". If the device name starts with "PIR" then it checks to see when the last update was and if this is greater than the time 'on' and less than time 'on plus 60 seconds', then the associated light is switched off. Here, the allowed duration of 'on' is set to 4 minutes (240 seconds). Switching off only occurs over a limited range of time, as computers are stupid and if I just asked it to switch 'Off' after 4 minutes, then Domoticz would keep turning the light off every time the timer script executes as the time would continually be greater than 4 minutes. This script then does the same for all the other devices with "PIR" prefix.

Now if you have a room called "Study" and place a Siemens PIR in it which you call "PIRStudy" in Domoticz, then on entering the study, the light "Study" will go on and not switch off until between 4 and 5 minutes after the sensor "PIRStudy" was *last* activated (it's effectively 'retriggerable'). So if you move around in the room for 15 minutes, then the light will definitely be switched off after 20 minutes.

The nice part is that if you buy another Siemens motion sensor and put it in the lounge, simply name it in Domoticz as "PIRLounge" and your lounge light will now switch on and off automatically.

Ultimate Smart PIR

If the concept of adding a fixed prefix to a device name can apply extra functionality, we can use some additional character positions in an extended name-prefix to customise the behaviour for a specific device even further. For example, different PIRs in the same room could turn a light on for different lengths of time, some PIRs could only operate at night and some PIRs could turn on a group and not just a single light, etc.

The two scripts below use the fixed prefix of "PIR" plus the 3 subsequent characters in the name to add extra functionality. The Study PIR now becomes "PIRxyzStudy" where x y and z are replaced by numbers or letters which tell the scripts how to respond.

Device Script to Turn Light or Group On at Specific Times of Day

The DEVICE script is purely concerned with turning the light on and so does not need to worry about the parameter which determines when to turn the light off.

For "PIRxyzStudy", after the "PIR" in the name: x specifies what times of day to turn the light on, so replace x with: a for all day, d for daytime, n for nighttime and 1 for a custom time. Any other letter will stop the light being turned on at all and while this may seem silly, in fact it is useful if you just want to stop room lights being left on unnecessarily. y specifies whether it is a single device or group of devices, with r being a single Domoticz switch and g being a Domoticz Group. z specifies how long the light will stay on for in minutes.

-- ~/domoticz/scripts/lua/script_device_pirs.lua
-- Each of the motion sensors in Domoticz follow this name convention:
-- PIRxrzSwitchName or PIRxgzGroupName
-- x speicifies when the PIR controls - a=all day, n=nighttime, d=daytime,
--   1=custom timer 1 set to 22:00 to 07:30
-- y specifies what the PIR is controlling -
--   r = room (single Domoticz) switch and g = group
-- z specifies how long the light will stay on for in minutes, so z = 5 turns
--   the switch or the group on for 5 minutes
-- N.B. be careful as currently there is little error checking so wrongly
--      named PIRs in Domoticz may cause an error
-- N.B. one wrongly named PIR may stop the script, check log for any issues

function customtest(nowhours, nowmins, starthours, startmins, endhours, endmins)
   nowmins = nowmins + (nowhours * 60)
   startmins = startmins + (starthours * 60)
   endmins = endmins + (endhours * 60)
   if (startmins > endmins) then
--    spans midnight
      if ((nowmins >= startmins) or (nowmins <= endmins)) then
         return true
      else
         return false
      end
   else
--    doesn't span midnight
      if ((nowmins >= startmins) and (nowmins <= endmins)) then
         return true
      else
         return false
      end
   end
end

function timetest(opertime)
   if opertime == "a" then
      return true
   end
   if opertime == "n" then
      if timeofday['Nighttime'] then
         return true
      else
         return false
      end
   end
   if opertime == "d" then
      if timeofday['Daytime'] then
         return true
      else
         return false
      end
   end
   if opertime == "1" then
      time = os.date("*t")
      return customtest(time.hour, time.min, 22, 0, 7, 30)
   end
   return false
end

commandArray = {}

tc=next(devicechanged)
v=tostring(tc)
if (v:sub(1,3) == 'PIR') then
   if timetest(v:sub(4,4)) then
      c=v:sub(7)
      if v:sub(5,5) == "g" then
         c="Group:"..c
      end
      commandArray[c] = 'On'
      tmess = c..' On - time 0'
      print(tmess)
   end
end

return commandArray

Time Script to Turn Light or Group Off after a Variable Time

The TIME script is purely concerned with turning the light off, so it does not worry about x, only y and z.

In PIRxyzStudy, z determines the number of minutes after the last activation of the sensor that the light is to be switched off, so 1 is 1 minutes, 9 is nine minutes and u means the light is never switched off. Note that in this script the maximum time is 9 minutes and setting 0 means the light will go off between 0 and 60 seconds after coming on, depending on when the TIME script is called.

-- ~/domoticz/scripts/lua/script_time_pirs.lua
-- Each of the motion sensors in Domoticz follow this name convention:
-- PIRxrzSwitchName or PIRxgzGroupName
-- x speicifies when the PIR controls - a=all day, n=nighttime, d=daytime,
--   1=custom timer 1 set to 22:00 to 07:30
-- y specifies what the PIR is controlling -
--   r = room (single Domoticz) switch and g = group
-- z specifies how long the ligth will stay on for in minutes, so z = 5 turns
--   the switch or the group on for 5 minutes
-- N.B. be careful as currently there is little error checking so wrongly
--      named PIRs in Domoticz may cause an error
-- N.B. one wrongly named PIR may stop the script, check log for any issues

function timedifference(s)
   year = string.sub(s, 1, 4)
   month = string.sub(s, 6, 7)
   day = string.sub(s, 9, 10)
   hour = string.sub(s, 12, 13)
   minutes = string.sub(s, 15, 16)
   seconds = string.sub(s, 18, 19)
   t1 = os.time()
   t2 = os.time{year=year, month=month, day=day, hour=hour, min=minutes, sec=seconds}
   difference = os.difftime (t1, t2)
   return difference
end

commandArray = {}

for i, v in pairs(otherdevices) do
   tc = tostring(i)
   v = i:sub(1,3)
   if (v == 'PIR') then
      onmode = i:sub(6,6)
      if onmode ~= "u" then
         timeon = (tonumber(onmode) * 60) - 1
--         print(tostring(60 * tonumber(onmode) ^ 2) - 1)
         difference = timedifference(otherdevices_lastupdate[tc])
--         print(i..difference)
         if (difference > timeon and difference < (timeon + 60)) then
            tempdiff = tostring(difference)
            c = i:sub(7)
            if i:sub(5,5) == "g" then
               c="Group:"..c
            end 
            tempmessage = c.." Light Off - after at least " .. (timeon+1) .. "secs up - actually - " .. tempdiff .. "seconds"
            print(tempmessage)
            commandArray[c] = 'Off'
         end
      end
   end
end

return commandArray

Example Configurations

My range of configurations are:

"PIRar4Bathroom" - at any time of day turn the bathroom light on and then it switch off again 4 minutes after the last PIR activation.

"PIRnr4Shower" - only at night time turn shower light on and switch off 4 again minutes after last PIR activation.

"PIRar0Utility" - at any time of day turn the utility room light on and switch off on next timer activation. Theory is in the utility room you are always moving around and go in there for very short lengths of time, so I want the light to go out as soon as possible. This can be a bit surprising on occassions, as I walk into a dark room and the light comes on only to go off a few second later, but it does come back on as soon as you move around. I may set it back to 1 at some stage, so it stays on for at least 1 minute.

"PIRnguReturning" - "PIR" pointing at the inside of the front door, so as soon as I come home at night, the hall light comes on and the kitchen light comes on low. Neither light gets switched off automatically, but stays on until it is manually switched off. This would also work if I had a door sensor and had named it "PIRnguReturning".

Extensions

Reacting to a Dummy Switch

A further extension is a simple modification to the 'timetest' function in 'script_device_pirs' to only operate when a dummy switch is in the on condition (e.g. "Dummy-Nightime", set to switch on 90 minutes before sunset and switch off at sunrise, or whatever):

Modified comments:

-- x specifies when the PIR controls - a=all day, n=nighttime, d=daytime, 1=custom timer 1 set to the range 22:00 to 07:30
--      s=Dummy-Nightime (Dummy switch set in Domoticz which controls manual set day-nighttime)
</pre>

Replacement timetest function:
<pre>
function timetest(opertime)
   if opertime == "a" then
      return true
   end
   if opertime == "n" then
      if timeofday['Nighttime'] then
         return true
      else
         return false
      end
   end
   if opertime == "d" then
      if timeofday['Daytime'] then
         return true
      else
         return false
      end
   end
   if opertime == "s" then
      if (otherdevices['Dummy-Nightime'] == "On") then
         return true
      else
         return false
      end
   end
   if opertime == "1" then
      time = os.date("*t")
      return customtest(time.hour, time.min, 22, 0, 7, 30)
   end
   return false
end

You could set a dummy switch to behave in any way you like or you could use an existing switch, e.g. only turn the light on during the daytime when the garage door is shut and the PIR is activated.

Reacting to Any Number of Dummy Switches for Simplified Coding

By using a fixed naming convention together with these Lua scripts it is possible to use any number of dummy switches to control whether the PIR operates to turn the light on or not.

So for the Study light, having a dummy switch 'Dummy-Study' would mean that PIRsr4Study would only operate if 'Dummy-Study' was switched on - Dummy-Study' could then be set by any other timing or action that Domoticz can use.

This modification makes it possible to simplify the script by removing the CustomTime function from the code and replacing the code with a custom dummy timed switch in Domoticz, shifting time control back to Domoticz. Of course now you can create any number of different timed switches, however each timed switch will only be connected to one PIR, so you may want to stick with the original code if you have multiple PIRs that all want the same custom time behaviour.

The improved script is below:

-- ~/domoticz/scripts/lua/script_device_pirs.lua
-- Each of the motion sensors in Domoticz follow this name convention:
-- PIRxyzSwitchName or PIRxyzGroupName
-- x speicifies when the PIR controls - a=all day, n=nighttime, d=daytime, s=Dummy switch set in Domoticz which
--    has to be on for the PIR to operate - Dummy-SwitchName or Dummy-GroupName
--    custom-timers can be set by creating a timed dummy switch in Domoticz
-- y specifies what the PIR is controlling - r = room (single Domoticz) switch and g = group
-- z specifies how long the ligth will stay on for in minutes, so z =5 turns the switch or the group on for 5 minutes
-- N.B. be carefully as currently there is little error checking so wrongly named PIRs in Domoticz may cause an error
-- N.B. one wrongly named PIR may stop the script, check log for any issues

function timetest(opertime,stem)
   if opertime == "a" then
      return true
   end
   if opertime == "n" then
      if timeofday['Nighttime'] then
         return true
      else
         return false
      end
   end
   if opertime == "d" then
      if timeofday['Daytime'] then
         return true
      else
         return false
      end
   end
   if opertime == "s" then
      if (otherdevices['Dummy-'..stem] == 'On') then
         return true
      else
         return false
      end
   end
   return false
end

commandArray = {}

tc=next(devicechanged)
v=tostring(tc)
if (v:sub(1,3) == 'PIR') then
--   print(v..' spotted')
   c=v:sub(7)
   if timetest(v:sub(4,4),c) then
      if v:sub(5,5) == "g" then
         c="Group:"..c
      end
      commandArray[c] = 'On'
      tmess = c..' On - time 0'
      print(tmess)
   end
end

return commandArray


== THIS WILL NO LONGER WORK AS WUNDERGROUND HAS CEASED API SUPPORT ==


Reading weather station data

Based on a (virtual) weather station, you can program actions for different devices.

Some tips while using: - In the example below, a WUnderground weather station is used (referred to as ['Weerstation']). - The match("([^;]+);([^;]+);([^;]+);([^;]+);([^;]+)") reads the different values. In this case, the ['Weerstation'] gives 5 values (sWeatherTemp, sWeatherHumidity, sWeatherUV, sWeatherPressure, sWeatherUV2). That is why the match tag has 5 times ([^;]+) - Other devices, such as the ['Rainmeter'] deliver only 2 values, therefore the match only contains twice the ([^;]+)")

commandArray = {}

--Weatherstation data:
sWeatherTemp, sWeatherHumidity, sWeatherUV, sWeatherPressure, sWeatherUV2 = otherdevices_svalues['Weerstation']:match("([^;]+);([^;]+);([^;]+);([^;]+);([^;]+)")
sWeatherTemp = tonumber(sWeatherTemp);
sWeatherHumidity = tonumber(sWeatherHumidity);
sWeatherUV = tonumber(sWeatherUV);
sWeatherPressure = tonumber(sWeatherPressure);
sWeatherUV2 = tonumber(sWeatherUV2);

print("Weather station: Temperature is " .. sWeatherTemp .. " ");
print("Weather station: Humidity is " .. sWeatherHumidity .. " ");
print("Weather station: UV is " .. sWeatherUV .. " ");
print("Weather station: Pressure is " .. sWeatherPressure .. " ");
print("Weather station: UV2 is " .. sWeatherUV2 .. " ");

------------------------------------------------------------------------

--Windmeter data:
sWindDirectionDegrees, sWindDirection, sWindSpeed, sWindGust, sWindTemperature, sWindFeel = otherdevices_svalues['Windmeter']:match("([^;]+);([^;]+);([^;]+);([^;]+);([^;]+);([^;]+)")

sWindDirectionDegrees = tonumber(sWindDirectionDegrees);
sWindDirection = (sWindDirection);
sWindSpeed = tonumber(sWindSpeed);
sWindGust = tonumber(sWindGust);
sWindTemperature = tonumber(sWindTemperature);
sWindFeel = tonumber(sWindFeel);

print("Windmeter: Winddirection (in degrees) is: " .. sWindDirectionDegrees .. " ");
print("Windmeter: Winddirection is: " .. sWindDirection .. " ");
print("Windmeter: Windspeed is: " .. sWindSpeed .. " ");
print("Windmeter: Windgust is: " .. sWindGust .. " ");
print("Windmeter: Windtemperature is: " .. sWindTemperature .. " ");
print("Windmeter: Windfeel is: " .. sWindFeel .. " ");

------------------------------------------------------------------------

--Rainmeter data:
sRainmeterCurrent, sRainmeterTotal = otherdevices_svalues['Rainmeter']:match("([^;]+);([^;]+)")

sRainmeterCurrent = tonumber(sRainmeterCurrent);
sRainmeterTotal = tonumber(sRainmeterTotal);

print("Rainmeter: Actual rain is: " .. sRainmeterCurrent .. " ");
print("Rainmeter: Total rain is: " .. sRainmeterTotal .. " ");

------------------------------------------------------------------------

--UV data:
sUV, sSolar = otherdevices_svalues['UV']:match("([^;]+);([^;]+)")

sUV = tonumber(sUV);
sSolar = tonumber(sSolar);

print("UV: UV Strength is: " .. sUV .." ");
print("UV: Solar radiation is: " .. sSolar .." ");

------------------------------------------------------------------------

--Sunscreen thresholds:
        -- Windspeed            = 50
        -- Rain                 =  0.0
        -- UV                   =  4.0
        -- Temperature          = 15.0
        -- Closed       = Down  = On
        -- Open         = Up    = Off

------------------------------------------------------------------------

if (timeofday['Daytime'])
        then
                print('It is past sunrise, monitoring variables for sunscreen')
        if (otherdevices['Sunscreen'] == 'Open' )
        then
                print ('Sunscreen is up')
        else
                print ('Sunscreen is down')

end

if (otherdevices['Sunscreen']   == 'Open'
        and sRainmeterCurrent   == 0
        and sWeatherTemp        > 15.0
        and sUV                 > 3
        and sWindSpeed          < 50
        and sWindGust           < 150)

then

        print ('Sun is shining, all thresholds OK, lowering sunscreen')
        commandArray['SendNotification']='Sunscreen#Sunscreen down --> Sun is shining'
        commandArray['Sunscreen']='On'

        elseif (otherdevices['Sunscreen'] == 'Closed' and sRainmeterCurrent > 0)
                then
                print ('It is raining, raising sunscreen')
                commandArray['SendNotification']='Sunscreen#Sunscreen up --> It is raining'
                commandArray['Sunscreen']='Off'

        elseif (otherdevices['Sunscreen'] == 'Closed' and sWeatherTemp < 15.0)
                then
                print ('Temperature too low, raising sunscreen')
                commandArray['SendNotification']='Sunscreen#Sunscreen up --> It is too cold'
                commandArray['Sunscreen']='Off'

        elseif (otherdevices['Sunscreen'] == 'Closed' and sUV < 4)
                then
                print ('Sun not shining too bright, raising sunscreen')
                commandArray['SendNotification']='Sunscreen#Sunscreen up --> Sunshine not too bright'
                commandArray['Sunscreen']='Off'

        elseif (otherdevices['Sunscreen'] == 'Closed' and sWindSpeed > 50)
                then
                print ('Windspeed too high, raising sunscreen')
                commandArray['SendNotification']='Sunscreen#Sunscreen up --> Windspeed too high'
                commandArray['Sunscreen']='Off'

        elseif (otherdevices['Sunscreen'] == 'Closed' and sWindGust > 150)
                then
                print ('Windgust too high, raising sunscreen')
                commandArray['SendNotification']='Sunscreen#Sunscreen up --> Windgust too high'
                commandArray['Sunscreen']='Off'

else
        print('Sunscreen status OK --> No action')
end

elseif (timeofday['Nighttime']
        and otherdevices['Sunscreen'] == 'Closed')
        then
                print('It is night, raising sunscreen')
                commandArray['SendNotification']='Sunscreen#Sunscreen up --> It is night'
                commandArray['Sunscreen']='Off'
        else
                print('Sunscreen already up --> No action')
end

return commandArray

Simple event to switch on devices based on status of other devices & time

This simple script checks for 2 mobile phones and 1 PC and turns on/off a router in the kitchen accordingly. It also adds the status of the router and of each piece of checked equipment to the log.

You could use a similar script for away/occupied presence-detection (if there are no mobile phones and PCs live on your home network, then it can probably be assumed nobody is at home).

 commandArray = {}

 phonemiguelstatus = otherdevices['PhoneMiguel']
 phoneedastatus = otherdevices['PhoneEda']
 macmiguelstatus = otherdevices['MacMiguel']
 date = os.date("*t")

 if ((phonemiguelstatus == 'On') or (phoneedastatus == 'On') or (macmiguelstatus == 'On')) and date.hour < 22 and date.hour > 6 and otherdevices['Kitchen - Router'] == 'Off' then

        commandArray['Kitchen - Router']='On'
        print ("Turning on kitchen router")

 elseif ((phonemiguelstatus == 'On') or (phoneedastatus == 'On') or (macmiguelstatus == 'On')) and date.hour < 22 and date.hour > 6 and otherdevices['Kitchen - Router'] == 'On' t$

        print ("Kitchen router on")

 elseif  otherdevices['Kitchen - Router'] == 'On' then

        commandArray['Kitchen - Router']='Off'
        print ("Turning off kitchen router")

 else

        print ("Kitchen router off")

 end

 print ("PhoneMiguel: " .. phonemiguelstatus .. " PhoneEda: " .. phoneedastatus .. " MacMiguel: " .. macmiguelstatus)

 return commandArray