SHAC ramp group begins at level zero

Discussion in 'C-Bus Automation Controllers' started by ssaunders, May 10, 2022.

  1. ssaunders

    ssaunders

    Joined:
    Dec 17, 2008
    Messages:
    232
    Likes Received:
    31
    Location:
    Melbourne
    I have a script that is watching for level changes of groups, and is being messed up slightly when CBus groups get ramped on.

    It would seem that the first level set during a ramp on is zero, then increasing values are seen after that. My script interprets this as an "off", followed quickly by several "on" triggers as the ramp progresses.

    Can anyone confirm that this is actually what LUA code should see during a ramp? It seems to me more sensible if the SHAC didn't do this, and instead started with a non-zero value when ramping on.
     
    ssaunders, May 10, 2022
    #1
  2. ssaunders

    Pie Boy

    Joined:
    Nov 21, 2012
    Messages:
    248
    Likes Received:
    31
    Location:
    New Zealand
    I’m sure this is expected behaviour,
    this is how cbus dimming is done, the on/255 or off/0 commands tell the dimmer output unit which way to dim up or down, subsequent ramp commands are sent after that.

    Is it possible to filter out the 0 and 255 values and use them as a reference point?
     
    Pie Boy, May 10, 2022
    #2
  3. ssaunders

    ssaunders

    Joined:
    Dec 17, 2008
    Messages:
    232
    Likes Received:
    31
    Location:
    Melbourne
    Thanks Pie Boy. I don't think filtering would work with my script designed as an event.

    I'm going to change it to be resident instead of event-based. That way I can watch over time to detect a 'ramp up' for my groups of interest and ignore the ramps down. I'll use the event to send over a group for the resident to add to a 'monitor list', which it could do for up to 30 seconds, or until no further movement in the level is seen. Using that approach would avoid constantly monitoring 63 groups.
     
    ssaunders, May 10, 2022
    #3
  4. ssaunders

    Pie Boy

    Joined:
    Nov 21, 2012
    Messages:
    248
    Likes Received:
    31
    Location:
    New Zealand
    Using a resident script will most likely use a lot of cpu for this… specially if you want a snappy result…

    What about a way in which you can detect a ramp command on event
    Like
    rrate = tonumber(string.sub(event.datahex, 5 , 8),16)
    Log(rrate)
    Or
    rrate = GetCBusRampRate(0, 56, 1)

    I think when the on/off ramp command is issued it is issued as an on/off ramp it would have a value in the ramp rate) so Could be detected with a ramp rate value.

    that’s off the top of my head so could be incorrect…
     
    Pie Boy, May 10, 2022
    #4
  5. ssaunders

    ssaunders

    Joined:
    Dec 17, 2008
    Messages:
    232
    Likes Received:
    31
    Location:
    Melbourne
    I really like that trick to get the ramp rate from event.datahex! And I also never noticed that GetCBusRampRate was a thing. Does that return the rate that a group is presently ramping at?

    I ended up settling on the little resident nugget below, which works very, very well so far... In short, it looks for a level that hasn't changed for around three seconds, and if that's the case (and the level is non-zero) it sets a 'lastLevel' value to be saved.

    This script gets fed by an event-based script that looks for level change that fires require('socket').udp():sendto(event.dst, '127.0.0.1', 5433), which causes this script to add a group to monitor.

    Sleep interval is zero, but that doesn't bother the CPU because it's also looking for keep-alive messages on a UDP socket with 0.5 second timeout. CPU sitting at ~1.5 load with a bunch of other stuff happening.

    Code:
    --[[
    Maintain CBus 'lastlevels'
    
    Used so that MQTT 'on' events can return a light/fan/blind to the last known set level.
    
    Also monitors keepalive messages from 'MQTT send', and disables/enables that script should it fail for whatever reason.
    --]]
    
    function loadLastLevel()
      lls = storage.get('lastLevel', '')
      llt = string.split(lls, ',')
      for _, v in ipairs(llt) do
        parts = string.split(v, '=')
        lastLevel[parts[1]] = tonumber(parts[2])
      end
    end
    
    if not initialised then
      logging = false
    
      server = require('socket').udp()
      server:settimeout(0.5)
      server:setsockname('127.0.0.1', 5433)
    
      monitoring = {}
      iterations = {}
      last = {}
    
      lastLevel = {}
      lastSaved = os.time()
      loadLastLevel()
    
      log('MQTT lastlevel initialised')
    
      initialised = true
    end
    
    function saveLastLevel()
      local ll = {}
      for k, v in pairs(lastLevel) do
        table.insert(ll, k..'='..v)
      end
      table.sort(ll)
      local lls = table.concat(ll, ',')
      old = storage.get('lastLevel', '')
      if lls ~= old then
          storage.set('lastLevel', lls)
        log('Saved last levels')
      end
    end
    
    
    remove = {}
    
    for g, ts in pairs(monitoring) do
      parts = string.split(g, '/')
      net = tonumber(parts[1])
      app = tonumber(parts[2])
      grp = tonumber(parts[3])
      current = GetCBusLevel(net, app, grp)
    
      -- If the monitored group stays at the same level for six iterations then it's a stable value and not ramping (0.5s per iteration)
      if current == last[g] then
        if iterations[g] > 6 then
          table.insert(remove, g)
          if current > 0 then -- If the stable value is non-zero then save it as the lastLevel
            lastLevel[g] = current
            if logging then log('Setting lastLevel to '..current..' for '..g) end
            saveLastLevel()
          end
        else
          iterations[g] = iterations[g] + 1
        end
      else
        iterations[g] = 0 -- Level changed, so reset
        last[g] = current
      end
     
      if os.time() - ts > 30 then -- Level has been changing for more than 30 seconds, so terminate the monitor
        if logging then log('Terminating monitor for '..g..' (30 second timeout)') end
        table.insert(remove, g)
      end
    end
    
    for i=1,#remove do
      if logging then log('Stopping monitor for '..remove[i]..' at '..os.time()) end
      monitoring[remove[i]] = nil -- Clean up removed monitors
    end
    
    
    function contains(prefix, text)
      pos = text:find(prefix, 1, true)
      if pos then return pos >= 1 else return false end
    end
    
    -- Look for lastlevel values
    cmd = server:receive()
    if cmd and type(cmd) == 'string' then
    
      -- If the command contains a slash it's to start monitoring a group
      if contains('/', cmd) then
        -- processCommand(cmd)
        if not monitoring[cmd] then
          last[cmd] = -1
          iterations[cmd] = 0
          monitoring[cmd] = os.time()
          if logging then log('Starting monitor for '..cmd..' at '..os.time()) end
        end
    
      -- If it contains a plus then it's a heartbeat
      elseif contains('+', cmd) then
        parts = string.split(cmd, '+')
        if parts[1] == 'MQTTsend' then
          MQTTsendHeartbeat = tonumber(parts[2])
        end
      end
    end
    
    
    --[[
    Heartbeats:
    
    The MQTT send script infinite loop can fail, stopping that script. To ensure it gets
    restarted without intervention, this script listens for a heartbeat from it every ten
    seconds. If two consecutive heartbeats are not received then that script is disabled
    and re-enabled.
    --]]
    
    -- If last heartbeat is nil (i.e. not yet received) then initialise it
    if not MQTTsendHeartbeat then
      MQTTsendHeartbeat = os.time()
    end
    
    MQTTsendSecondsSince = os.time() - MQTTsendHeartbeat
    if MQTTsendSecondsSince > 20 then -- Missed two heartbeats, so re-start script
      log('Missed two MQTT send heartbeats (last received '..MQTTsendSecondsSince..' seconds ago) - Re-starting MQTT send')
      script.disable('MQTT send')
      script.enable('MQTT send')
      MQTTsendHeartbeat = nil
    end
     
    ssaunders, May 10, 2022
    #5
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.