[EXAMPLE] Different light levels during the day for a PIR triggered group

Discussion in 'C-Bus Automation Controllers' started by ssaunders, Jan 13, 2022.

  1. ssaunders

    ssaunders

    Joined:
    Dec 17, 2008
    Messages:
    232
    Likes Received:
    31
    Location:
    Melbourne
    Sharing in case someone else finds it useful.

    As the title says, this script allows setting a group address to three different levels depending on time of day. Sunrise = high, then two other hours can be set to vary the triggered light level. The group switches off after a timeout.

    I use this in a well-trafficked hallway that is adjacent to bedrooms, which may have doors ajar at night. A low light level is enough to see, but not enough to wake anyone up.

    Egress delays are allowed for, as is manual control of the group level (which essentially disables the timed function). Plus a placeholder to do other things at sunrise, and scene-triggered "late night" light level of super low.

    The code is extensively commented / logged, so it should be reasonably self-explanatory.

    Cheers.

    Code:
    -- Dynamic Triggered Light Level
    -- -----------------------------
    
    if not initialised then
      logging = true        -- Whether to log all actions performed
    
      dynamicGroup          = 'Hall Mid Pendants'
      dynamicTrigger        = 'Hall PIR_1 Trigger Night'
      dynamicTriggerEnable  = 'Hall PIR_1 Enable'
      lateNightScene        = 'Late night'
      levelHigh = 127       -- Target light levels throughout the day
      levelLow = 63
      levelSuperLow = 31
      hourLow = 22          -- The hour at which to switch to low (must be after sunrise and before super-low hour)
      hourSuperLow = 0      -- The hour for super-low level (must be after low hour and before sunrise)
      timerRuntime = 90     -- How long to switch the group on for (re-trigger will reset the timer)
      rampOn = 4            -- The ramp on rate of the group
      rampOff = 12          -- The ramp off rate of the group
      disableDuration = 15  -- If the group is manually switched off then allow time to leave without re-triggering
    end
    
    -- Do things at sunrise that are unrelated to the dynamic group (leave empty to do nothing)
    local function actionsAtSunrise()
        SetLightingState('Outside Sensors Disable', false) -- If the outside PIR sensors had been disabled at some point then re-enable them
    end
    
    local function calculateSunriseSunset()
      sunrise, sunset = rscalc(-37.826389, 144.968056) -- Melbourne, Australia
    end
    
    --[[
    Notes:
    
    A PIR to turn on the group should be configured to turn on a trigger group for one second,
    and also have an enable group defined.
    
    At sunrise the target level is set to high, then lowered at a specified evening time, then
    lowered again at midnight (or when a late night scene is set). When the optional 'lateNightScene'
    is set then the target level will be immediately set to super-low. The late night scene is
    triggered elsewhere (a key?). To disable the scene use an empty string or comment the variable.
    
    If the group is manually set to off (or by a scene), then control of the group by this script
    will suspend for a given 'disableDuration' to allow for area egress.
    
    If the group is manually set to a level other than the script target (like with a switch timer)
    then this script will not turn it off after the timer runtime. To re-enable the timer function
    the group must be switched off.
    
    Run it as a resident script with zero delay.
    
    LIKELY BUG: If the work-around to detect correct group level when ramping off happens to occur at
    the same time that a target level change needs to happen, then the change will not happen. This
    is because the work-around blocks using a while loop.
    --]]
    
    local function timerStart()
      timerStarted = os.time()
      timerDuration = timerRuntime
    end
    
    local function timerStop()
      timerStarted = 0
      timerDuration = 0
    end
    
    local function isEmpty(s)
      return s == nil or s == ''
    end
    
    now = os.date('*t')
    
    if initialised then
      -- GET THE STATE OF GROUP AND THE TRIGGER
    
      groupLevel = GetLightingLevel(dynamicGroup)
     
      if not suspended then -- Suspension occurs to allow egress
    
        if (groupLevel ~= oldGroupLevel) then
          oldGroupLevel = groupLevel
          groupChange = true
        end
    
        triggerState = GetLightingState(dynamicTrigger)
        if triggerState ~= oldTriggerState then
          oldTriggerState = triggerState
          if triggerState then triggered = true end
        end
    
        -- CHECK FOR A TRIGGER
    
        if triggered then
          if logging then log('Triggered') end
          if timerStarted > 0 then
            timerStart() -- Reset the timer if already running
          else
            if (groupLevel == 0) or rampingOff then -- Turn on the group and start the timer
              SetLightingLevel(dynamicGroup, dynamicSet, rampOn)
              groupLevel = dynamicSet
              dynamicLevel = dynamicSet
              timerStart()
              timerExpired = false
              rampingOff = false
            end
          end
        end
    
        -- CHECK FOR TIMER EXPIRY
    
        if timerStarted > 0 then
          if os.time() - timerStarted >= timerDuration then
            if logging then log('Timer expired') end
            if groupLevel == dynamicLevel then
              if logging then log('Ramping off ' .. dynamicGroup) end
              SetLightingLevel(dynamicGroup, 0, rampOff) -- Despite the ramp, the SHAC will immediately report that the group is off (for a while), so the next line is a work-around
              timeoutBegin = os.time() while GetLightingLevel(dynamicGroup) == 0 do if os.time() > timeoutBegin + 5 then break end end -- Handle delay in getting a correct level (SHAC idiosyncrasy)
              rampingOff = true
            else
              if logging then log(dynamicGroup .. ' at unexpected level, doing nothing') end
            end
            timerStop()
            timerExpired = true
          end
        end
    
        -- CHECK FOR GROUP SWITCHED OFF
    
        if groupChange and (groupLevel == 0) then
          if logging then log(dynamicGroup .. ' is off') end
          if (not timerExpired) and GetLightingState(dynamicTriggerEnable) then
            PulseCBusLevel('Local Network', 'Lighting', dynamicTriggerEnable, 0, 0, disableDuration, 255)
            if logging then log('Stopping timer and delaying ' .. disableDuration .. ' seconds') end
            timerStop()
            suspended = os.time() -- Suspend trigger detection until the end of the disable duration, as no point running
          end
          timerExpired = false
          rampingOff = false
        end
    
        -- DESIRED STATE LOGGING
    
        if logging then
          if groupChange and (GetLightingLevel(dynamicGroup) == dynamicSet) then
            log(dynamicGroup .. ' is at desired level of ' .. dynamicSet)
          end
        end
     
      else
        if os.time() - suspended >= disableDuration then
          suspended = nil
          if logging then log('Resuming') end
        end
      end -- if not suspended
    
      -- CALCULATE SUNRISE/SUNSET ONCE PER DAY
    
      if (now.hour == 1) and (now.min == 0) and (now.sec == 0) and (not setSR) then
        calculateSunriseSunset()
        setSR = true
        if logging then log('Sunrise set to: ' .. sunrise .. ', and sunset: ' .. sunset) end
      end
    
      -- SET THE TARGET GROUP LEVEL BASED ON TIME OF DAY
    
      local function setDynamicGroupLevel(level)
        if logging then log('Adjusting dynamic level to ' .. dynamicSet) end
        dynamicSet = level
        if GetLightingLevel(dynamicGroup) > 0 then -- Lights are on, so set the new level and start the clock
          SetLightingLevel(dynamicGroup, dynamicSet, rampOn)
          PulseCBusLevel('Local Network', 'Lighting', dynamicTrigger, 255, 0, 1, 0) -- Simulate a trigger
          rampingOff = true -- Set rampingOff to ensure group is set
        end
      end
    
      if (not setLevel) and (now.sec == 0) then
        if now.hour * 60 + now.min == sunrise then
          setDynamicGroupLevel(levelHigh)
          setLevel = true
          actionsAtSunrise() -- Do the desired actions at sunrise
        end
        if now.min == 0 then
          if now.hour == hourLow then
            -- Set to low, unless already at super-low because of scene trigger
            if dynamicSet ~= levelSuperLow then setDynamicGroupLevel(levelLow) end
            setLevel = true
          end
          if now.hour == hourSuperLow then
            setDynamicGroupLevel(levelSuperLow)
            setLevel = true
          end
        end
      end
    
      if (now.sec == 1) then -- Reset the time-based 'set' flags
        setSR = false
        setLevel = false
      end
     
      -- CHECK FOR LATE NIGHT SCENE SET
     
      if not isEmpty(lateNightScene) then
        if (not lateNightSet) and SceneIsSet(lateNightScene) then
          lateNightSet = true
          dynamicSet = levelSuperLow
          if logging then log('Adjusted for late night mode') end
          if groupLevel > 0 then -- Group is on, so adjust it
            PulseCBusLevel('Local Network', 'Lighting', dynamicTrigger, 255, 0, 1, 0) -- Simulate a trigger
            rampingOff = true
          end
        else
          if lateNightSet and (not SceneIsSet(lateNightScene)) then
            lateNightSet = false
            if logging then log('Late night mode off') end
          end
        end
        end
    
      -- Clear the group level change and triggered flags
      groupChange = false
      triggered = false
    
    else
    -- INITIALISATION
    
      if logging then log(dynamicGroup .. ' intialising') end
      initialised = true
      calculateSunriseSunset()
      timerStop()
      rampingOff = false
      SetLightingState(dynamicTriggerEnable, true)
     
      nowMinute = now.hour * 60 + now.min
      if hourLow * 60 > sunrise then -- i.e. Low is before midnight
        dynamicSet = levelSuperLow
        if nowMinute >= sunrise then dynamicSet = levelHigh end
        if now.hour >= hourLow then dynamicSet = levelLow end
        if hourSuperLow > hourLow and now.hour >= hourSuperLow then dynamicSet = levelSuperLow end -- For super-low before midnight too
        else -- Low is after midnight
        dynamicSet = levelHigh
        if nowMinute < sunrise then
          if nowMinute >= hourLow * 60 then dynamicSet = levelLow end
          if nowMinute >= hourSuperLow * 60 then dynamicSet = levelSuperLow end -- Super-low must be after low
        end
      end
    
      if logging then log('Adjusted dynamic level to ' .. dynamicSet) end
      if GetLightingLevel(dynamicGroup) == dynamicSet then -- If the group is currently at the dynamic level then start the timer
        dynamicLevel = dynamicSet
        timerStart()
      end
      timerExpired = true
      lateNightSet = false
    end
     
    Last edited: Jan 13, 2022
    ssaunders, Jan 13, 2022
    #1
  2. ssaunders

    ssaunders

    Joined:
    Dec 17, 2008
    Messages:
    232
    Likes Received:
    31
    Location:
    Melbourne
    Fixed the work-around bug, and also fixed that when the 'late night' theme is un-set the script now restores to normal time-of-day levels. Unfortunately I can't edit the original post now, so this reply will have to do.

    Code:
    -- Dynamic Triggered Light Level
    -- -----------------------------
    
    if not initialised then
      logging = true        -- Whether to log all actions performed
    
      dynamicGroup          = 'Hall Mid Pendants'
      dynamicTrigger        = 'Hall PIR_1 Trigger Night'
      dynamicTriggerEnable  = 'Hall PIR_1 Enable'
      lateNightScene        = 'Late night'
      levelHigh = 127       -- Target light levels throughout the day
      levelLow = 63
      levelSuperLow = 31
      hourLow = 22          -- The hour at which to switch to low (must be after sunrise and before super-low hour)
      hourSuperLow = 0      -- The hour for super-low level (must be after low hour and before sunrise)
      timerRuntime = 90     -- How long to switch the group on for (re-trigger will reset the timer)
      rampOn = 4            -- The ramp on rate of the group
      rampOff = 12          -- The ramp off rate of the group
      disableDuration = 15  -- If the group is manually switched off then allow time to leave without re-triggering
    end
    
    -- Do things at sunrise that are unrelated to the dynamic group (leave empty to do nothing)
    local function actionsAtSunrise()
        SetLightingState('Outside Sensors Disable', false) -- If the outside PIR sensors had been disabled at some point then re-enable them
    end
    
    local function calculateSunriseSunset()
      sunrise, sunset = rscalc(-37.826389, 144.968056) -- Melbourne, Australia
    end
    
    --[[
    Notes:
    
    A PIR to turn on the group should be configured to turn on a trigger group for one second,
    and also have an enable group defined.
    
    At sunrise the target level is set to high, then lowered at a specified evening time, then
    lowered again at super-low time (or when a late night scene is set). When the optional 'lateNightScene'
    is set then the target level will be immediately set to super-low. The late night scene is
    triggered elsewhere (a key?). To disable the scene use an empty string or comment the variable.
    
    If the group is manually set to off (or by a scene), then control of the group by this script
    will suspend for a given 'disableDuration' to allow for area egress.
    
    If the group is manually set to a level other than the script target (like with a switch timer)
    then this script will not turn it off after the timer runtime. To re-enable the timer function
    the group must be switched off.
    
    Run it as a resident script with zero delay.
    --]]
    
    local function timerStart()
      timerStarted = os.time()
      timerDuration = timerRuntime
    end
    
    local function timerStop()
      timerStarted = 0
      timerDuration = 0
    end
    
    local function isEmpty(s)
      return s == nil or s == ''
    end
    
    now = os.date('*t')
    
    if initialised then
      -- GET THE STATE OF GROUP AND THE TRIGGER
    
      groupLevel = GetLightingLevel(dynamicGroup)
     
      if not suspended then -- Suspension occurs to allow egress
     
        if (groupLevel ~= oldGroupLevel) then
          oldGroupLevel = groupLevel
          groupChange = true
        end
    
        triggerState = GetLightingState(dynamicTrigger)
        if triggerState ~= oldTriggerState then
          oldTriggerState = triggerState
          if triggerState then triggered = true end
        end
    
        -- CHECK FOR A TRIGGER
    
        if triggered then
          if logging then log('Triggered') end
          if timerStarted > 0 then
            timerStart() -- Reset the timer if already running
          else
            if (groupLevel == 0) or rampingOff then -- Turn on the group and start the timer
              SetLightingLevel(dynamicGroup, dynamicSet, rampOn)
              groupLevel = dynamicSet
              dynamicLevel = dynamicSet
              timerStart()
              timerExpired = false
              rampingOff = false
            end
          end
        end
    
        -- CHECK FOR TIMER EXPIRY
    
        if timerStarted > 0 then
          if os.time() - timerStarted >= timerDuration then
            if logging then log('Timer expired') end
            if groupLevel == dynamicLevel then
              if logging then log('Ramping off ' .. dynamicGroup) end
              SetLightingLevel(dynamicGroup, 0, rampOff) -- Despite the ramp, the SHAC will immediately report that the group is off (for a while), so the next line is a work-around
              timeoutBegin = os.time() while GetLightingLevel(dynamicGroup) == 0 do if os.time() > timeoutBegin + 5 then break end end -- Handle delay in getting a correct level (SHAC idiosyncrasy)
              rampingOff = true
            else
              if logging then log(dynamicGroup .. ' at unexpected level, doing nothing') end
            end
            timerStop()
            timerExpired = true
          end
        end
    
        -- CHECK FOR GROUP SWITCHED OFF
    
        if groupChange and (groupLevel == 0) then
          if logging then log(dynamicGroup .. ' is off') end
          if (not timerExpired) and GetLightingState(dynamicTriggerEnable) then
            PulseCBusLevel('Local Network', 'Lighting', dynamicTriggerEnable, 0, 0, disableDuration, 255)
            if logging then log('Stopping timer and delaying ' .. disableDuration .. ' seconds') end
            timerStop()
            suspended = os.time() -- Suspend trigger detection until the end of the disable duration, as no point running
          end
          timerExpired = false
          rampingOff = false
        end
    
        -- DESIRED STATE LOGGING
    
        if logging then
          if groupChange and (GetLightingLevel(dynamicGroup) == dynamicSet) then
            log(dynamicGroup .. ' is at desired level of ' .. dynamicSet)
          end
        end
     
      else
        if os.time() - suspended >= disableDuration then
          suspended = nil
          if logging then log('Resuming') end
        end
      end -- if not suspended
    
      -- CALCULATE SUNRISE/SUNSET ONCE PER DAY
    
      if (not setSR) and (now.hour == 1) and (now.min == 0) then
        calculateSunriseSunset()
        setSR = true
        if logging then log('Sunrise set to: ' .. sunrise .. ', and sunset: ' .. sunset) end
      end
    
      if (now.min == 1) then -- Reset the time-based 'set' flag
        setSR = false
      end
     
      -- SET THE TARGET GROUP LEVEL BASED ON TIME OF DAY
    
      local function setDynamicGroupLevel(level)
        dynamicSet = level
        if logging then log('Adjusted dynamic level to ' .. dynamicSet) end
        if GetLightingLevel(dynamicGroup) > 0 then -- Lights are on, so set the new level and start the clock
          SetLightingLevel(dynamicGroup, dynamicSet, rampOn)
          PulseCBusLevel('Local Network', 'Lighting', dynamicTrigger, 255, 0, 1, 0) -- Simulate a trigger
          rampingOff = true -- Set rampingOff to ensure group is set
        end
      end
    
      if now.min ~= lastMinute then -- check for change every minute
        lastMinute = now.min
        if now.hour * 60 + now.min == sunrise then
          setDynamicGroupLevel(levelHigh)
          actionsAtSunrise() -- Do the desired actions at sunrise
        end
        if now.min == 0 then
          -- Set to low if the right hour, unless already at super-low because of scene trigger
          if now.hour == hourLow then if dynamicSet ~= levelSuperLow then setDynamicGroupLevel(levelLow) end end
          -- Set to super-low if the right hour
          if now.hour == hourSuperLow then setDynamicGroupLevel(levelSuperLow) end
        end
      end
    
      -- CHECK FOR LATE NIGHT SCENE SET
     
      if not isEmpty(lateNightScene) then
        if (not lateNightSet) and SceneIsSet(lateNightScene) then
          lateNightSet = true
          dynamicSet = levelSuperLow
          if logging then log('Adjusted for late night mode') end
            if logging then log('Adjusted dynamic level to ' .. dynamicSet) end
          if groupLevel > 0 then -- Group is on, so adjust it
            PulseCBusLevel('Local Network', 'Lighting', dynamicTrigger, 255, 0, 1, 0) -- Simulate a trigger
            rampingOff = true
          end
        else
          if lateNightSet and (not SceneIsSet(lateNightScene)) then
            lateNightSet = false
            if logging then log('Late night mode off') end
            nowMinute = now.hour * 60 + now.min
            if hourLow * 60 > sunrise then -- i.e. Low is before midnight
              if now.hour < hourLow then dynamicSet = levelHigh else dynamicSet = levelLow end
              if hourSuperLow > hourLow and now.hour >= hourSuperLow then dynamicSet = levelSuperLow end
            else
              dynamicSet = levelHigh
              if nowMinute < sunrise then
                if nowMinute >= hourLow * 60 then dynamicSet = levelLow end
                if nowMinute >= hourSuperLow * 60 then dynamicSet = levelSuperLow end
              end
            end
            if logging then log('Adjusted dynamic level to ' .. dynamicSet) end
          end
        end
      end
    
      -- Clear the group level change and triggered flags
      groupChange = false
      triggered = false
    
    else
    -- INITIALISATION
    
      if logging then log(dynamicGroup .. ' intialising') end
      initialised = true
      calculateSunriseSunset()
      timerStop()
      rampingOff = false
      SetLightingState(dynamicTriggerEnable, true)
     
      nowMinute = now.hour * 60 + now.min
      if hourLow * 60 > sunrise then -- i.e. Low is before midnight
        dynamicSet = levelSuperLow
        if nowMinute >= sunrise then dynamicSet = levelHigh end
        if now.hour >= hourLow then dynamicSet = levelLow end
        if hourSuperLow > hourLow and now.hour >= hourSuperLow then dynamicSet = levelSuperLow end -- For super-low before midnight too
        else -- Low is after midnight
        dynamicSet = levelHigh
        if nowMinute < sunrise then
          if nowMinute >= hourLow * 60 then dynamicSet = levelLow end
          if nowMinute >= hourSuperLow * 60 then dynamicSet = levelSuperLow end -- Super-low must be after low
        end
      end
    
      if logging then log('Adjusted dynamic level to ' .. dynamicSet) end
      if GetLightingLevel(dynamicGroup) == dynamicSet then -- If the group is currently at the dynamic level then start the timer
        dynamicLevel = dynamicSet
        timerStart()
      end
      timerExpired = true
      lateNightSet = false
      lastMinute = -1
    end
     
    Last edited: Jan 13, 2022
    ssaunders, Jan 13, 2022
    #2
Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments (here). After that, you can post your question and our members will help you out.