SHAC/MQTT/HomeAssistant recall previous level

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

  1. ssaunders

    ssaunders

    Joined:
    Dec 17, 2008
    Messages:
    231
    Likes Received:
    31
    Location:
    Melbourne
    TL: DR> Get HomeAssistant working with CBus with a SHAC and without C-Gate.

    I got sick of MisterHouse. OMG, it's sooo ten years ago and no longer developed, and good riddance given the bolts-hanging-out-of-its-neck integration with CBus and Google Assistant.

    And then on replacing it, I rapidly got sick of HomeAssistant with Google integration setting my CBus lights to 100% for 'on' every time I barked "Hoy Google, Turn on the dunny light"... I wanted Google/HA to remember the previous 'on' ramp level. MisterHouse used to do it, even if it was a pig (so I s'pose it had some lipstick after all...)

    Which it turned out was easier said than done for HA.

    Sooo I did... my code below.

    Some notes:
    • I don't care about security on my MQTT broker. There is no username/password, so adjust to suit.
    • Obviously change the broker address
    • Obviously change the client names to avoid collissions with existing clients
    • 'Last Levels' are stored frequently when changed, so should survive reboots / script restarts on the SHAC
    • I had to set a MQTT client keep-alive to 25 seconds. I'm using mosquitto broker on my network, and anything 30 seconds or above it hated for some reason and kicked the connections repeatedly. I had to do the same with HomeAssistant too. It shouldn't be needed, as default should have worked, but it didn't, because the server should listen to what the client wants. So that's the '25' in each client:connect statement. YMMV depending on broker...
    • I haven't implemented MQTT discovery. Ugh. Next level. That's for another day.
    And lastly, if there is another far easier way to achieve this, like a "FFS Remember My Last Levels" setting anywhere in HA then PLEASE ... shoot me down and point out that setting in HA ASAP... I would gladly throw away a day of wasted coding for that nugget.

    I might have forgotten to share some aspect. If I have, hit me up for whatever.

    Event-based script, on tag = MQTT for the groups you want integrated:
    Code:
    --[[
    Pushes CBus events to MQTT resident scripts via internal sockets.
    
    Tag required objects with the "MQTT" keyword and this script will run whenever one of those objects change.
    --]]
    
    dataparts = string.split(event.dst, "/")
    lastLevel = GetCBusLevel(dataparts[1], dataparts[2], dataparts[3])
    
    -- Send an event to MQTT send to publish
    require('socket').udp():sendto(event.dst .. "/" .. event.getvalue(), '127.0.0.1', 5432)
    
    -- Send an event to MQTT receive to save lastlevel
    require('socket').udp():sendto(event.dst .. "/" .. event.getvalue(), '127.0.0.1', 5433)
    Resident 'MQTT send', seleep interval zero, but that doesn't matter as it never exits...
    Code:
    --[[
    Push Cbus events to MQTT
    --]]
    
    logging = false
    
    mqtt_broker = '192.168.9.10'
    -- mqtt_username = 'YOUR_MQTT_USERNAME'
    -- mqtt_password = 'YOUR_MQTT_PASSWORD'
    mqtt_clientid = 'shac-send'
    
    mqtt_read_topic = 'cbus/read/'
    
    socket = require('socket')
    
    unpublished = {}
    notify = true
    
    function publishCurrent()
      mqttP = GetCBusByKW('MQTT', 'or')
      n = 1
      for k, v in pairs(mqttP) do
        app = tonumber(v['address'][2])
        group = tonumber(v['address'][3])
        unpublished[n] = {app=app, group=group}
        n = n + 1
      end
    end
    
    mqtt = require('mosquitto')
    client = mqtt.new(mqtt_clientid)
    server = require('socket').udp()
    server:settimeout(0.5)
    server:setsockname('127.0.0.1', 5432)
    
    client.ON_CONNECT = function()
      log('MQTT send connected')
      publishCurrent()
    end
    
    client.ON_DISCONNECT = function(...)
      log('MQTT send disconnected')
      notify = true
    end
    
    -- client:login_set(mqtt_username, mqtt_password)
    client:connect(mqtt_broker, 1883, 25)
    client:loop_start()
    
    while true do
      function publish(network, app, group, level)
          state = (level ~= 0) and 'ON' or 'OFF'
        client:publish(mqtt_read_topic .. network .. '/' .. app .. '/' .. group .. '/state', state, 1, true)
        client:publish(mqtt_read_topic .. network .. '/' .. app .. '/' .. group .. '/level', level, 1, true)
        if logging then log('Publishing state and level ' .. mqtt_read_topic .. network .. '/' .. app .. '/' .. group .. ' to ' .. state .. '/' .. level) end
      end
     
        cmd = server:receive()
        if cmd and type(cmd) == 'string' then
        parts = string.split(cmd, '/')
        publish(254, tonumber(parts[2]), tonumber(parts[3]), tonumber(parts[4]))
        end
      if #unpublished > 0 then
            if notify then
          log('Publishing current levels')
          notify = false
        end
        publish(255, unpublished[1].app, unpublished[1].group, GetCBusLevel(0, unpublished[1].app, unpublished[1].group))
        table.remove(unpublished, 1)
        if #unpublished == 0 then log('Publishing current levels completed') end
      end
    end
    And a resident 'MQTT receive', again sleep doesn't matter...
    Code:
    --[[
    Push MQTT events to Cbus
    --]]
    
    logging = false
    
    mqtt_broker = '192.168.9.10'
    -- mqtt_username = 'YOUR_MQTT_USERNAME'
    -- mqtt_password = 'YOUR_MQTT_PASSWORD'
    mqtt_clientid = 'shac-receive'
    
    mqtt_read_topic = 'cbus/read/'
    mqtt_write_topic = 'cbus/write/#';
    
    -- load mqtt module and create UDP server
    mqtt = require('mosquitto')
    server = require('socket').udp()
    server:settimeout(0.5)
    server:setsockname('127.0.0.1', 5433)
    
    
    lastLevel = {}
    
    function saveLastLevel()
      local ll = {}
      for k, v in pairs(lastLevel) do
        table.insert(ll, k..'='..v)
      end
      local lls = table.concat(ll, ',')
      old = storage.get('lastLevel', '')
      if lls ~= old then
        log('Saving last levels')
          storage.set('lastLevel', lls)
      end
    end
    
    function loadLastLevel()
      local lls = storage.get('lastLevel', '')
      local llt = string.split(lls, ',')
      for _, v in ipairs(llt) do
        parts = string.split(v, '=')
        lastLevel[parts[1]] = parts[2]
      end
    end
    
    loadLastLevel()
    
    
    -- create new mqtt client
    client = mqtt.new(mqtt_clientid)
    
    client.ON_CONNECT = function(success)
      if (success) then
        log('MQTT receive connected')
        local mid = client:subscribe(mqtt_write_topic, 2)
      end
    end
    
    client.ON_DISCONNECT = function(...)
      log('MQTT receive disconnected')
    end
    
    client.ON_MESSAGE = function(mid, topic, payload)
      if logging then log(topic .. ' to ' .. payload) end
     
      parts = string.split(topic, '/')
    
      if not parts[6] then
        log('MQTT error: Invalid message format')
    
      elseif parts[6] == 'getall' then
        datatable = grp.all()
        for key,value in pairs(datatable) do
          parts = string.split(value.address, '/')
                network = tonumber(parts[1])
                app = tonumber(parts[2])
          group = tonumber(parts[3])
          if app == tonumber(parts[4]) and group ~= 0 then
                    level = tonumber(value.data)
                    state = (level ~= 0) and 'ON' or 'OFF'
            if logging then log(parts[3], app, group, state, level) end
            client:publish(mqtt_read_topic .. parts[3] .. '/' .. app .. '/' .. group .. '/state', state, 1, true)
                    client:publish(mqtt_read_topic .. parts[3] .. '/' .. app .. '/' .. group .. '/level', level, 1, true)
                end   
            end
    
      elseif parts[6] == 'switch' then
        if payload == 'ON' then
          if logging then log('Payload is ON') end
                SetCBusLevel(0, parts[4], parts[5], 255, 0)
        elseif payload == 'OFF' then
          if logging then log('Payload is OFF') end
          SetCBusLevel(0, parts[4], parts[5], 0, 0)
        end
        
      elseif parts[6] == 'measurement' then        -- UNTESTED
            SetCBusMeasurement(0, parts[4], parts[5], payload, 0)
        
      elseif parts[6] == 'ramp' then
        if payload == 'ON' then
          if logging then log('Payload is ON') end
                SetCBusLevel(0, parts[4], parts[5], 255, 0)
        elseif payload == 'OFF' then
          if logging then log('Payload is OFF') end
          SetCBusLevel(0, parts[4], parts[5], 0, 0)
        else
          ramp = string.split(payload, ',')
          num = math.floor(ramp[1] + 0.5)
          if num and num < 256 then
            key = '0'..'/'..parts[4]..'/'..parts[5]
            if lastLevel[key] then toSet = lastLevel[key] else toSet = num end
            if ramp[2] ~= nil and tonumber(ramp[2]) > 1 then
                        SetCBusLevel(0, parts[4], parts[5], toSet, ramp[2])
            else
                        SetCBusLevel(0, parts[4], parts[5], toSet, 0)
            end
          end
        end
      end
    end
    
    -- client:login_set(mqtt_username, mqtt_password)
    client:connect(mqtt_broker, 1883, 25)
    client:loop_start()
    
    receivedOff = {}
    rampDetect = {}
    lastSaved = os.time()
    
    -- Loop looking for lastlevel values
    while true do
      cmd = server:receive()
        if cmd and type(cmd) == 'string' then
        if logging then log('Command received: '..cmd) end
        parts = string.split(cmd, '/')
        key = parts[1]..'/'..parts[2]..'/'..parts[3]
        level = tonumber(parts[4])
        if level == 0 then
          if (receivedOff[key] and os.time() - receivedOff[key] >= 30) or not receivedOff[key] then
            receivedOff[key] = os.time()
            rampDetect[key] = nil
            if logging then log('Executing OFF ' .. key) end
            if logging then log('Last received OFF '..receivedOff[key]) end
          end
        else
          if logging then log('Executing ON ' .. key) end
          ll = 0
          if not rampDetect[key] then
            rampDetect[key] = level
          end
          if receivedOff[key] and os.time() - receivedOff[key] < 30 then
            if logging then log('Last received OFF '..receivedOff[key]) end
            if level > rampDetect[key] then -- must be ramping on
              ll = level
            end
          end
          if not receivedOff[key] or os.time() - receivedOff[key] > 30 then
            ll = level
          end
          if ll > 0 then
            if logging then log('Saving lastlevel of '..level) end
            lastLevel[key] = ll
                end
        end
        end
      if os.time() - lastSaved > 60 then
        saveLastLevel()
        lastSaved = os.time()
      end
    end
    And in HomeAssistant configuration.yaml...
    Code:
    mqtt:
       client_id: hass     
       keepalive: 20   
                      
    light: !include map59-light.yaml
    switch: !include map59-switch.yaml
    And an example in map59-light.yaml...
    Code:
      - platform: mqtt
        state_topic: "cbus/read/254/56/7/state"
        command_topic: "cbus/write/254/56/7/switch"
        brightness_state_topic: "cbus/read/254/56/7/level"
        brightness_command_topic: "cbus/write/254/56/7/ramp"
        payload_off: "OFF"
        on_command_type: "brightness"
        name: Main
        unique_id: mq_dining_room
    And map59-switch.yaml...
    Code:
      - platform: mqtt
        state_topic: "cbus/read/254/56/111/state"
        command_topic: "cbus/write/254/56/111/switch"
        payload_off: "OFF"
        payload_on: "ON"
        name: Pool Side Heater 1
        unique_id: mq_hutch_heater_1
     
    ssaunders, May 1, 2022
    #1
    Damaxx likes this.
  2. ssaunders

    ssaunders

    Joined:
    Dec 17, 2008
    Messages:
    231
    Likes Received:
    31
    Location:
    Melbourne
    Bug in 'MQTT receive'... sorry...

    Line that reads:
    Code:
            if lastLevel[key] then toSet = lastLevel[key] else toSet = num end
    Should be:
    Code:
            if lastLevel[key] and payload == 255 then toSet = lastLevel[key] else toSet = num end
    And another one in 'MQTT send'... again sorry...

    Line that reads:
    Code:
        publish(255, unpublished[1].app, unpublished[1].group, GetCBusLevel(0, unpublished[1].app, unpublished[1].group))
    Should be:
    Code:
        publish(254, unpublished[1].app, unpublished[1].group, GetCBusLevel(0, unpublished[1].app, unpublished[1].group))[CODE]
     
    ssaunders, May 2, 2022
    #2
  3. ssaunders

    ssaunders

    Joined:
    Dec 17, 2008
    Messages:
    231
    Likes Received:
    31
    Location:
    Melbourne
    Stupid 120 minute edit limit...
    Code:
            if lastLevel[key] and num == 255 then toSet = lastLevel[key] else toSet = num end
    Plus, I have number/string confusion. I love/hate LUA... In 'MQTT receive':
    Code:
        lastLevel[parts[1]] = tonumber(parts[2])
    And for some LUA reason:
    Code:
            lastLevel[key] = tonumber(ll)
    I'll get back to you with MQTT discovery code. It's not simple in my case, but my example should turn on light bulbs, pardon the pun for some in terms of setting icons/preferred areas and manipulating desired names for HomeAssistant.
     
    Last edited: May 2, 2022
    ssaunders, May 2, 2022
    #3
  4. ssaunders

    ssaunders

    Joined:
    Dec 17, 2008
    Messages:
    231
    Likes Received:
    31
    Location:
    Melbourne
    MQTT / HomeAssistant discovery is implemented. That bit is a bit of a dog's breakfast, but simple enough to follow.

    So it turns out that trying to use a UDP socket to receive "last levels" in conjunction with the same mosquitto receive resident script ends in badness. This is what I had previously. A kind of lock-up-the-script-and-stop-working badness... and at one stage, power off/on the SHAC badness. Weird stuff that staring at the code to find an issue could not resolve. So I split out the "last level" business into its own resident and used SHAC storage.

    The issues doing this with the code earlier seemed to occur randomly, but occurred always when asking HomeAssistant to do many things together, like turn on a whole room. Some things would turn on, and then scripts would hang.

    It seems to work like a champ now, plus with no manual configuration of entities in HomeAssistant because discovery. HA needs to be configured to listen to the topic 'cbus'. I tossed this in my configuration.yaml:
    Code:
    mqtt:
      client_id: hass
      keepalive: 20
      discovery_prefix: cbus
    
    Cheers.

    Event-based on keyword "MQTT", or whatever you choose:
    Code:
    --[[
    Pushes CBus events to MQTT resident scripts via internal sockets.
    
    Tag required objects with the "MQTT" keyword and this script will run whenever one of those objects change
    --]]
    
    -- Send an event to MQTT send to publish
    require('socket').udp():sendto(event.dst .. "/" .. event.getvalue(), '127.0.0.1', 5432)
    
    -- Send an event to MQTT receive to save lastlevel
    require('socket').udp():sendto(event.dst .. "/" .. event.getvalue(), '127.0.0.1', 5433)
    Resident MQTT send/zero sleep because infinte loop so it doesn't Parramatta. Note the dog's breakfast name/suggested area/image manipulation:
    Code:
    --[[
    Push Cbus events to MQTT
    --]]
    
    logging = false
    
    mqtt_broker = '192.168.9.10'
    -- mqtt_username = 'YOUR_MQTT_USERNAME'
    -- mqtt_password = 'YOUR_MQTT_PASSWORD'
    mqtt_clientid = 'shac-send'
    
    mqtt_read_topic = 'cbus/read/'
    mqtt_discovery_topic = 'cbus'
    
    socket = require('socket')
    
    unpublished = {}
    notify = true
    status = 0
    
    function publishCurrent()
      mqttP = GetCBusByKW('MQTT', 'or')
      n = 1
      for k, v in pairs(mqttP) do
        app = tonumber(v['address'][2])
        group = tonumber(v['address'][3])
        unpublished[n] = {app=app, group=group}
        n = n + 1
      end
    end
    
    mqtt = require('mosquitto')
    client = mqtt.new(mqtt_clientid)
    server = require('socket').udp()
    server:settimeout(0.25)
    server:setsockname('127.0.0.1', 5432)
    
    client.ON_CONNECT = function(success)
      if success then
          log('MQTT send connected')
        status = 1
          publishCurrent()
      end
    end
    
    client.ON_DISCONNECT = function(...)
      log('MQTT send disconnected')
      notify = true
      status = 2
    end
    
    -- client:login_set(mqtt_username, mqtt_password)
    client:connect(mqtt_broker, 1883, 25)
    client:loop_start()
    
    function publish(net, app, group, level)
      state = (level ~= 0) and 'ON' or 'OFF'
      client:publish(mqtt_read_topic..net..'/'..app..'/'..group..'/state', state, 1, true)
      client:publish(mqtt_read_topic..net..'/'..app..'/'..group..'/level', level, 1, true)
      if logging then log('Publishing state and level '..mqtt_read_topic..net..'/'..app..'/'..group..' to '..state..'/'..level) end
    end
    
    function unpublish(net, app, group)
      client:publish(mqtt_read_topic..net..'/'..app..'/'..group..'/state', '', 1, true)
      client:publish(mqtt_read_topic..net..'/'..app..'/'..group..'/level', '', 1, true)
    end
    
    function begins(prefix, text)
      pos = text:find(prefix, 1, true)
      if pos then return pos == 1 else return false end
    end
    
    function contains(prefix, text)
      pos = text:find(prefix, 1, true)
      if pos then return pos >= 1 else return false end
    end
    
    function addDiscover(net, app, group)
      local name = GetCBusGroupTag(0, app, group)
      local sa = ''
      local img = ''
      local dType = ''
      local preferName = ''
    
      -- Set suggested area
        if     begins('Kitchen', name) then sa = 'Kitchen'
      elseif begins('Family', name) then sa = 'Family Room'
      elseif begins('Formal', name) then sa = 'Front Lounge'
      elseif begins('Dining', name) then sa = 'Dining Room'
      elseif begins('Bedroom 1', name) then sa = 'Bedroom 1'
      elseif begins('Bedroom 2', name) then sa = 'Bedroom 2'
      elseif begins('Bedroom 3', name) then sa = 'Bedroom 3'
      elseif begins('Bathroom 1', name) then sa = 'Bathroom 1'
      elseif begins('Bathroom 2', name) then sa = 'Bathroom 2'
      elseif begins('Hall', name) then sa = 'Hall'
      elseif begins('Den', name) then sa = 'Den'
      elseif begins('Laundry', name) then sa = 'Den'
      elseif begins('Outside', name) then sa = 'Outside'
        elseif begins('Hutch Bathroom', name) then sa = 'Hutch Bathroom'
      elseif begins('Hutch Kitchen', name) then sa = 'Hutch Kitchen'
      elseif begins('Hutch', name) then sa = 'Hutch'
      elseif begins('Open', name) then sa = 'Outside'
      elseif begins('Close', name) then sa = 'Outside'
      elseif begins('Pool', name) then sa = 'Pool'
      elseif begins('Spa', name) then sa = 'Pool'
      else sa = 'Unknown'
      end
      log(name..' in '..sa)
    
      -- Set image
        if     contains('Pool Heat', name) then img = 'mdi:pump'
        elseif contains('Heat', name) then img = 'mdi:radiator'
        elseif contains('Pump', name) then img = 'mdi:pump'
        elseif contains('Blower', name) then img = 'mdi:chart-bubble'
        elseif contains('Gate', name) and contains('Open', name) then img = 'mdi:gate-open'
        elseif contains('Gate', name) and contains('Close', name) then img = 'mdi:gate'
        elseif contains('Sensors', name) then img = 'mdi:motion-sensor-off'
        elseif contains('Fan', name) then img = 'mdi:fan'
        elseif contains('Rail Enable', name) then img = 'mdi:radiator-disabled'
        elseif contains('Towel Rail', name) then img = 'mdi:radiator'
        elseif contains('Floor Enable', name) then img = 'mdi:radiator-disabled'
        elseif contains('Under Floor', name) then img = 'mdi:radiator'
      else img = 'mdi:lightbulb'
      end
    
        -- Adjust the device name
        if     contains('Pantry LV', name) then preferName = 'Pantry'
        elseif contains('Dining LV', name) then preferName = 'Perimeter'
        elseif contains('Formal LV', name) then preferName = 'Perimeter'
        --elseif contains('Perimeter', name) then preferName = 'Perimeter'
        elseif contains('Hall Front', name) then preferName = 'Front Hall'
        elseif contains('Hall Mid', name) then preferName = 'Hall'
        elseif contains('Kitchen', name) and contains('LV', name) then preferName = 'Main'
        elseif contains('Bathroom 2', name) and contains('LV 1', name) then preferName = 'Main'
        elseif contains('Bathroom 2', name) and contains('LV 2', name) then preferName = 'Spa'
        elseif contains('Laundry', name) and not contains('Outside', name) then preferName = 'Laundry'
        elseif contains('Bedroom 1 LV', name) then preferName = 'Wardrobe'
        elseif contains('Dining Garden', name) then preferName = 'Outside Cat Hutch'
        elseif contains('Verandah Pendant', name) then preferName = 'Verandah'
        elseif contains('Verandah LV', name) then preferName = 'Verandah Perimeter'
        elseif contains('Hutch Bathroom', name) then preferName = 'Main'
        elseif contains('Hutch Kitchen', name) then preferName = 'Galley'
        elseif contains('Heat 1', name) then preferName = 'Pool Side Heater 1'
        elseif contains('Heat 2', name) then preferName = 'Pool Side Heater 2'
        elseif contains('Heat 3', name) then preferName = 'TV Side Heater 1'
        elseif contains('Heat 4', name) then preferName = 'TV Side Heater 2'
        elseif contains('Pendant', name) then preferName = 'Main'
        elseif contains('LV', name) then preferName = 'Main'
      else preferName = name
      end
    
      -- Set type of device
      if img == 'mdi:lightbulb' then
        dType = 'light'
      else
        dType = 'switch'
      end
    
      oid = 'cbus_mqtt_'..net..'_'.. app..'_'.. group
    
      if dType == 'light' then
        payload = {
          ['name'] = preferName,
          ['unique_id'] = oid,
          ['ic'] = img,
          ['dev'] = {
            ['ids'] = oid,
            ['sa'] = sa
          },
          ['stat_t'] = 'cbus/read/'..net..'/'..app..'/'..group..'/state',
          ['cmd_t'] = 'cbus/write/'..net..'/'..app..'/'..group..'/switch',
          ['bri_stat_t'] = 'cbus/read/'..net..'/'..app..'/'..group..'/level',
          ['bri_cmd_t'] = 'cbus/write/'..net..'/'..app..'/'..group..'/ramp',
          ['pl_off'] = 'OFF',
          ['on_cmd_type'] = 'brightness',
        }
      elseif dType == 'switch' then
        payload = {
          ['name'] = preferName,
          ['unique_id'] = oid,
          ['ic'] = img,
          ['dev'] = {
            ['ids'] = oid,
            ['sa'] = sa
          },
          ['stat_t'] = 'cbus/read/'..net..'/'..app..'/'..group..'/state',
          ['cmd_t'] = 'cbus/write/'..net..'/'..app..'/'..group..'/switch',
          ['pl_on'] = 'ON',
          ['pl_off'] = 'OFF',
        }
      end
    
      local j = json.encode(payload)
      local topic = mqtt_discovery_topic..'/'..dType..'/'..oid..'/config'
    --  log(topic)
    --  log(j)
    
        client:publish(mqtt_discovery_topic..'/'..'light'..'/'..'cbus_mqtt_254_56_33'..'/config', '', 1, true)
      if sa == 'deleteme' then
        client:publish(topic, '', 1, true)
      else
          client:publish(topic, j, 1, true)
      end
    end
    
    while true do
        cmd = server:receive()
        if cmd and type(cmd) == 'string' then
        parts = string.split(cmd, '/')
        publish(254, tonumber(parts[2]), tonumber(parts[3]), tonumber(parts[4]))
        end
    
      if #unpublished > 0 then
            if notify then
          log('Publishing current levels')
          notify = false
        end
        addDiscover(254, unpublished[1].app, unpublished[1].group)
        -- unpublish(255, unpublished[1].app, unpublished[1].group) -- clean up from a bug
        publish(254, unpublished[1].app, unpublished[1].group, GetCBusLevel(0, unpublished[1].app, unpublished[1].group))
        table.remove(unpublished, 1)
        if #unpublished == 0 then log('Publishing current levels completed') end
      end
    
      if status == 2 then
        log('MQTT send reconnection')
        if client:reconnect() then
          status = 1
        end
      end
    end
    And then the MQTT receive resident/zero. There's a "getall" that still wants work. The major change here from earlier is to switch to a loop_forever() in favour of loop_start(), from which I subsequently looked for socket receive in an infinite loop, which worked usually, but broke when loaded up with many things to do.
    Code:
    --[[
    Push MQTT events to Cbus
    --]]
    
    logging = false
    
    mqtt_broker = '192.168.9.10'
    -- mqtt_username = 'YOUR_MQTT_USERNAME'
    -- mqtt_password = 'YOUR_MQTT_PASSWORD'
    mqtt_clientid = 'shac-receive'
    
    mqtt_read_topic = 'cbus/read/'
    mqtt_write_topic = 'cbus/write/#';
    
    -- load mqtt module and create UDP server
    mqtt = require('mosquitto')
    
    status = 0
    
    lastLevel = {}
    lls = ''
    
    function loadLastLevel()
      local llsTest = storage.get('lastLevel', '')
      if llsTest ~= lls then
        lls = llsTest
        local llt = string.split(lls, ',')
        for _, v in ipairs(llt) do
          parts = string.split(v, '=')
          lastLevel[parts[1]] = tonumber(parts[2])
        end
        end
    end
    
    
    -- create new mqtt client
    client = mqtt.new(mqtt_clientid)
    
    client.ON_CONNECT = function(success)
      if (success) then
        log('MQTT receive connected')
        status = 1
        local mid = client:subscribe(mqtt_write_topic, 2)
      end
    end
    
    client.ON_DISCONNECT = function(...)
      log('MQTT receive disconnected')
      status = 2
    end
    
    client.ON_MESSAGE = function(mid, topic, payload)
      if logging then log(topic .. ' to ' .. payload) end
    
      loadLastLevel()
    
      local parts = string.split(topic, '/')
      local net = 0
      local app = tonumber(parts[4])
      local group = tonumber(parts[5])
    
      if not parts[6] then
        log('MQTT error: Invalid message format')
    
      --[[
      NEEDS TO BE MODIFIED to just publish our groups of interest!!! Currently doing all groups
        GetCBusByKW('MQTT', 'or')
    
      elseif parts[6] == 'getall' then
        local datatable = grp.all()
        for key,value in pairs(datatable) do
          parts = string.split(value.address, '/')
                net = tonumber(parts[1])
                app = tonumber(parts[2])
          group = tonumber(parts[3])
          if app == tonumber(parts[4]) and group ~= 0 then
                    level = tonumber(value.data)
                    state = (level ~= 0) and 'ON' or 'OFF'
            if logging then log(parts[3], app, group, state, level) end
            client:publish(mqtt_read_topic .. net .. '/' .. app .. '/' .. group .. '/state', state, 1, true)
                    client:publish(mqtt_read_topic .. net .. '/' .. app .. '/' .. group .. '/level', level, 1, true)
                end 
            end
      --]]
    
      elseif parts[6] == 'switch' then
        if payload == 'ON' then
          if logging then log('Payload is ON') end
                SetCBusLevel(0, app, group, 255, 0)
        elseif payload == 'OFF' then
          if logging then log('Payload is OFF') end
          SetCBusLevel(0, app, group, 0, 0)
        end
      
      elseif parts[6] == 'measurement' then        -- UNTESTED
            SetCBusMeasurement(0, app, group, payload, 0)
      
      elseif parts[6] == 'ramp' then
        if payload == 'ON' then
          if logging then log('Payload is ON') end
                SetCBusLevel(0, app, group, 255, 0)
        elseif payload == 'OFF' then
          if logging then log('Payload is OFF') end
          SetCBusLevel(0, app, group, 0, 0)
        else
          local key = '0'..'/'..app..'/'..group
          parts = string.split(payload, ',')
          local lev = tonumber(parts[1])
          local num = math.floor(lev + 0.5)
          if num and num < 256 then
              if logging then log('Payload is RAMP '..payload) end
            local toSet = 0
            local ramp = 0
            if logging and lastLevel[key] then log('Last level '..lastLevel[key]) end
            if lastLevel[key] and num == 255 then toSet = lastLevel[key] else toSet = num end
            if parts[2] ~= nil then ramp = tonumber(parts[2]) else ramp = 0 end
                    SetCBusLevel(0, app, group, toSet, ramp)
          end
        end
      end
    end
    
    -- client:login_set(mqtt_username, mqtt_password)
    client:connect(mqtt_broker, 1883, 25)
    client:loop_forever()
    And finally the MQTT lastlevel resident/zero sleep script, which receives messages from groups of interest and manages the lastLevel storage. Saves every two seconds should there be any change.
    Code:
    logging = false
    
    server = require('socket').udp()
    server:settimeout(0.5)
    server:setsockname('127.0.0.1', 5433)
    
    lastLevel = {}
    receivedOff = {}
    rampDetect = {}
    lastSaved = os.time()
    
    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
    
    function saveLastLevel()
      local ll = {}
      for k, v in pairs(lastLevel) do
        table.insert(ll, k..'='..v)
      end
      local lls = table.concat(ll, ',')
      old = storage.get('lastLevel', '')
      if lls ~= old then
          storage.set('lastLevel', lls)
        log('Saved last levels')
      end
    end
    
    function processCommand(cmd)
        if logging then log('Command received: '..cmd) end
      local parts = string.split(cmd, '/')
      local key = parts[1]..'/'..parts[2]..'/'..parts[3]
      local level = tonumber(parts[4])
      if level == 0 then
        if not receivedOff[key] or (receivedOff[key] and os.time() - receivedOff[key] >= 30) then
          receivedOff[key] = os.time()
          rampDetect[key] = nil
          if logging then log('Executing OFF ' .. key) end
          if logging then
            if receivedOff[key] then log('Last received OFF '..receivedOff[key]..' more than 30 seconds ago') else log('Last received OFF never') end
          end
        end
      else
        if logging then log('Executing ON ' .. key) end
        local ll = 0
        if not rampDetect[key] then
          rampDetect[key] = level
        end
        if receivedOff[key] and (os.time() - receivedOff[key] < 30) then
          if logging then log('Last received OFF '..receivedOff[key]..' within 30 seconds') end
          if level > rampDetect[key] then -- must be ramping on
            ll = level
          end
        end
        if not receivedOff[key] or (os.time() - receivedOff[key] > 30) then
          if logging then log('Last received off key does not exist or off key is older than 30 seconds') end
          ll = level
        end
        if ll > 0 then
          if logging then log('Saving lastlevel of '..level) end
          lastLevel[key] = ll
        end
      end
    end
    
    loadLastLevel()
    
    -- Loop looking for lastlevel values
    while true do
      cmd = server:receive()
        if cmd and type(cmd) == 'string' then
        processCommand(cmd)
        end
    
      if os.time() - lastSaved > 2 then
        saveLastLevel()
        lastSaved = os.time()
      end
    end
     
    Last edited: May 3, 2022
    ssaunders, May 3, 2022
    #4
  5. ssaunders

    ssaunders

    Joined:
    Dec 17, 2008
    Messages:
    231
    Likes Received:
    31
    Location:
    Melbourne
    For posterity, in case someone needs/wants similar functionality, the following code has been further tested/debugged/extended.

    Discovery and send/receive now incorporates 'covers' and 'fans' as well, as I have some Somfy blinds and a sweep fan controller. The MQTT send script was also sometimes unreliable on re-connect, so keep-alive messaging is implemented to automatically enable/disable that script should it become unresponsive.

    To get everything I wanted with CBus/Mosquitto/Home Assistant working well with the SHAC has been quite a learning curve, so it's possible that other strange behaviour will rear its head in future that my code doesn't cover, so your (and my) mileage may vary.

    I've been replying to myself in this thread exclusively, so please forgive that. But I don't mind. I'm not a stranger to chatting with myself. :)

    My sincere thanks to others out there that helped me get to this point.

    Event-based on keyword "MQTT":

    Code:
    --[[
    Pushes CBus events to MQTT resident scripts via internal sockets.
    
    Tag required objects with the "MQTT" keyword and this script will run whenever one of those objects change
    --]]
    
    -- Send an event to publish to broker
    require('socket').udp():sendto(event.dst .. "/" .. event.getvalue(), '127.0.0.1', 5432)
    
    -- Send an event to save lastlevel
    require('socket').udp():sendto(event.dst .. "/" .. event.getvalue(), '127.0.0.1', 5433)
    Resident 'MQTT send' (zero sleep, the name of the script is significant for keep-alive):

    Code:
    --[[
    Push Cbus events to MQTT
    --]]
    
    logging = false
    
    mqtt_broker = '192.168.9.10'
    -- mqtt_username = 'YOUR_MQTT_USERNAME'
    -- mqtt_password = 'YOUR_MQTT_PASSWORD'
    mqtt_clientid = 'shac-send'
    
    mqtt_read_topic = 'cbus/read/'
    mqtt_discovery_topic = 'cbus'
    
    socket = require('socket')
    
    unpublished = {}
    toPublish = 0
    published = 0
    notify = true
    status = 0
    
    function publishCurrent()
      local mqttP = GetCBusByKW('MQTT', 'or')
      n = 1
      for k, v in pairs(mqttP) do
        app = tonumber(v['address'][2])
        group = tonumber(v['address'][3])
        unpublished[n] = {app=app, group=group}
        n = n + 1
      end
      toPublish = n - 1
        published = 0
      log('Queued '..toPublish..' objects with keyword MQTT for publication')
    end
    
    function begins(prefix, text)
      pos = text:find(prefix, 1, true)
      if pos then return pos == 1 else return false end
    end
    
    function contains(prefix, text)
      pos = text:find(prefix, 1, true)
      if pos then return pos >= 1 else return false end
    end
    
    mqtt = require('mosquitto')
    client = mqtt.new(mqtt_clientid)
    server = require('socket').udp()
    server:settimeout(0.5)
    server:setsockname('127.0.0.1', 5432)
    
    client.ON_CONNECT = function(success)
      if success then
          log('MQTT send connected')
        status = 1
          publishCurrent()
      end
    end
    
    client.ON_DISCONNECT = function(...)
      log('MQTT send disconnected')
      notify = true
      status = 2
    end
    
    -- client:login_set(mqtt_username, mqtt_password)
    client:connect(mqtt_broker, 1883, 25)
    client:loop_start()
    
    function publish(net, app, group, level)
      if contains('Blind', GetCBusGroupTag(0, app, group)) then
          -- state = (level ~= 0) and 'open' or 'closed'
        state = 'stopped'
      else
          state = (level ~= 0) and 'ON' or 'OFF'
      end
      client:publish(mqtt_read_topic..net..'/'..app..'/'..group..'/state', state, 1, true)
      client:publish(mqtt_read_topic..net..'/'..app..'/'..group..'/level', level, 1, true)
      if logging then log('Publishing state and level '..mqtt_read_topic..net..'/'..app..'/'..group..' to '..state..'/'..level) end
    end
    
    function unpublish(net, app, group)
      client:publish(mqtt_read_topic..net..'/'..app..'/'..group..'/state', '', 1, true)
      client:publish(mqtt_read_topic..net..'/'..app..'/'..group..'/level', '', 1, true)
      if logging then log('Un-publishing state and level '..mqtt_read_topic..net..'/'..app..'/'..group) end
    end
    
    function addDiscover(net, app, group)
      local name = GetCBusGroupTag(0, app, group)
      local sa = ''
      local img = ''
      local dType = ''
      local preferName = ''
    
      -- Set suggested area
        if     begins('Kitchen', name) then sa = 'Kitchen'
      elseif begins('Family', name) then sa = 'Family Room'
      elseif begins('Formal', name) then sa = 'Front Lounge'
      elseif begins('Dining', name) then sa = 'Dining Room'
      elseif begins('Bedroom 1', name) then sa = 'Bedroom 1'
      elseif begins('Bedroom 2', name) then sa = 'Bedroom 2'
      elseif begins('Bedroom 3', name) then sa = 'Bedroom 3'
      elseif begins('Bathroom 1', name) then sa = 'Bathroom 1'
      elseif begins('Bathroom 2', name) then sa = 'Bathroom 2'
      elseif begins('Hall', name) then sa = 'Hall'
      elseif begins('Den', name) then sa = 'Den'
      elseif begins('Laundry', name) then sa = 'Den'
      elseif begins('Outside', name) then sa = 'Outside'
      elseif contains('Storage', name) then sa = 'Store'
        elseif begins('Hutch Bathroom', name) then sa = 'Hutch Bathroom'
      elseif begins('Hutch Kitchen', name) then sa = 'Hutch Kitchen'
      elseif begins('Hutch', name) then sa = 'Hutch'
      elseif begins('Open', name) then sa = 'Outside'
      elseif begins('Close', name) then sa = 'Outside'
      elseif begins('Pool', name) then sa = 'Pool'
      elseif begins('Spa', name) then sa = 'Pool'
      else sa = 'Unknown'
      end
    
      -- Set image
        if     contains('Pool Heat', name) then img = 'mdi:pump'
        elseif contains('Heat', name) then img = 'mdi:radiator'
        elseif contains('Pump', name) then img = 'mdi:pump'
        elseif contains('Blower', name) then img = 'mdi:chart-bubble'
        elseif contains('Gate', name) and contains('Open', name) then img = 'mdi:gate-open'
        elseif contains('Gate', name) and contains('Close', name) then img = 'mdi:gate'
        elseif contains('Sensors', name) then img = 'mdi:motion-sensor-off'
      elseif contains('Sweep Fan', name) then img =  'mdi:ceiling-fan'
        elseif contains('Fan', name) then img = 'mdi:fan'
        elseif contains('Rail Enable', name) then img = 'mdi:radiator-disabled'
        elseif contains('Towel Rail', name) then img = 'mdi:radiator'
        elseif contains('Floor Enable', name) then img = 'mdi:radiator-disabled'
        elseif contains('Under Floor', name) then img = 'mdi:radiator'
        elseif contains('Blind', name) then img = 'mdi:blinds'
      else img = 'mdi:lightbulb'
      end
    
      -- Set type of device
      if img == 'mdi:lightbulb' then dType = 'light'
      elseif img == 'mdi:ceiling-fan' then dType = 'fan'
      elseif img == 'mdi:blinds' then dType = 'cover'
      else dType = 'switch' end
    
        -- Adjust the device name
        if     contains('Pantry LV', name) then preferName = sa..' Pantry Light'
        elseif contains('Dining LV', name) then preferName = sa..' Perimeter Lights'
        elseif contains('Formal LV', name) then preferName = sa..' Perimeter Lights'
        elseif contains('Bedroom 2', name) then preferName = sa..' Light'
        elseif contains('Bedroom 3', name) then preferName = sa..' Light'
        elseif contains('Hall Front', name) then preferName = 'Front Hall'
        elseif contains('Hall Mid', name) then preferName = 'Hall'
        elseif contains('Floor Enable',name) then preferName = sa..' Under Floor Heating Enable'
        elseif contains('Under Floor', name) then preferName = sa..' Under Floor Heating'
        elseif contains('Kitchen', name) and contains('LV', name) then preferName = sa..' Main Lights'
        elseif contains('Bathroom 2', name) and contains('LV 1', name) then preferName = sa..' Main Lights'
        elseif contains('Bathroom 2', name) and contains('LV 2', name) then preferName = sa..' Spa Lights'
        elseif contains('Laundry', name) and not contains('Outside', name) then preferName = 'Laundry Lights'
        elseif contains('Bedroom 1 LV', name) then preferName = sa..' Wardrobe Lights'
        elseif contains('Dining Garden', name) then preferName = 'Outside Cat Hutch Light'
        elseif contains('Verandah Pendant', name) then preferName = 'Verandah Light'
        elseif contains('Verandah LV', name) then preferName = 'Verandah Perimeter Lights'
        elseif contains('Hutch Bathroom', name) then preferName = sa..' Main Light'
        elseif contains('Hutch Kitchen', name) then preferName = 'Galley Lights'
        elseif contains('Central', name) then preferName = sa..' Central Light'
        elseif contains('Hutch Perimeter', name) then preferName = sa..' Perimeter Lights'
        elseif contains('Hutch Shrubbery', name) then preferName = sa..' Shrubbery Lights'
        elseif contains('Hutch Storage', name) then preferName = 'Store Light'
        elseif contains('BBQ', name) then preferName = sa..' BBQ Light'
        elseif contains('Store Flood', name) then preferName = sa..' Store Flood Light'
        elseif contains('Side Door', name) then preferName = sa..' Side Door Light'
        elseif contains('Laundry Door', name) then preferName = sa..' Laundry Door Light'
      elseif contains('Heat 1', name) then preferName = 'Pool Side Heater 1'
        elseif contains('Heat 2', name) then preferName = 'Pool Side Heater 2'
        elseif contains('Heat 3', name) then preferName = 'TV Side Heater 1'
        elseif contains('Heat 4', name) then preferName = 'TV Side Heater 2'
        elseif contains('Pendant', name) then preferName = sa..' Main Light'
        elseif contains('LV', name) then preferName = sa..' Main Lights'
      elseif dType == 'light' then preferName = name..' Lights'
      else preferName = name
      end
    
      if logging then log('Publish discovery '..name..' as '..dType..':'..preferName..' in area '..sa) end
    
      oid = 'cbus_mqtt_'..net..'_'.. app..'_'.. group
    
      if dType == 'light' then
        payload = {
          ['name'] = preferName,
          ['unique_id'] = oid,
          ['ic'] = img,
          ['dev'] = {
            ['ids'] = oid,
            ['sa'] = sa
          },
          ['stat_t'] = 'cbus/read/'..net..'/'..app..'/'..group..'/state',
          ['cmd_t'] = 'cbus/write/'..net..'/'..app..'/'..group..'/switch',
          ['bri_stat_t'] = 'cbus/read/'..net..'/'..app..'/'..group..'/level',
          ['bri_cmd_t'] = 'cbus/write/'..net..'/'..app..'/'..group..'/ramp',
          ['pl_off'] = 'OFF',
          ['on_cmd_type'] = 'brightness',
        }
      elseif dType == 'switch' then
        payload = {
          ['name'] = preferName,
          ['unique_id'] = oid,
          ['ic'] = img,
          ['dev'] = {
            ['ids'] = oid,
            ['sa'] = sa
          },
          ['stat_t'] = 'cbus/read/'..net..'/'..app..'/'..group..'/state',
          ['cmd_t'] = 'cbus/write/'..net..'/'..app..'/'..group..'/switch',
          ['pl_on'] = 'ON',
          ['pl_off'] = 'OFF',
        }
      elseif dType == 'fan' then
        payload = {
          ['name'] = preferName,
          ['unique_id'] = oid,
          ['ic'] = img,
          ['dev'] = {
            ['ids'] = oid,
            ['sa'] = sa
          },
          ['stat_t'] = 'cbus/read/'..net..'/'..app..'/'..group..'/state',
          ['cmd_t'] = 'cbus/write/'..net..'/'..app..'/'..group..'/ramp',
          ['pl_on'] = 'ON',
          ['pl_off'] = 'OFF',
          ['pr_mode_cmd_t'] = 'cbus/write/'..net..'/'..app..'/'..group..'/ramp',
          ['pr_mode_cmd_tpl'] = '{% if value == "low" %} 86 {% elif value == "medium" %} 170 {% elif value == "high" %} 255 {% endif %}',
          ['pr_mode_stat_t'] = 'cbus/read/'..net..'/'..app..'/'..group..'/level',
          ['pr_mode_val_tpl'] = '{% if value == 0 %} OFF {% elif value == 86 %} low {% elif value == 170 %} medium {% elif value == 255 %} high {% endif %}',
          ['pr_modes'] = {'low', 'medium', 'high'}
            }
      elseif dType == 'cover' then
        payload = {
          ['name'] = preferName,
          ['unique_id'] = oid,
          ['ic'] = img,
          ['dev'] = {
            ['ids'] = oid,
            ['sa'] = sa
          },
          ['stat_t'] = 'cbus/read/'..net..'/'..app..'/'..group..'/state',
          ['cmd_t'] = 'cbus/write/'..net..'/'..app..'/'..group..'/ramp',
          ['pos_open'] = 255,
          ['pos_clsd'] = 0,
            ['pl_open'] = 'OPEN',
            ['pl_cls'] = 'CLOSE',
          ['pos_t'] = 'cbus/read/'..net..'/'..app..'/'..group..'/level',
          ['set_pos_t'] = 'cbus/write/'..net..'/'..app..'/'..group..'/ramp',
            }
      end
    
      local j = json.encode(payload)
      local topic = mqtt_discovery_topic..'/'..dType..'/'..oid..'/config'
    
      if sa == 'changeme' then -- Alter 'changeme' to an area name to remove all topics in that area instead of publishing - for debug
        client:publish(topic, '', 1, true)
      else
          client:publish(topic, j, 1, true)
      end
    end
    
    heartbeat = os.time()
    
    while true do
        cmd = server:receive()
        if cmd and type(cmd) == 'string' then
        parts = string.split(cmd, '/')
        publish(254, tonumber(parts[2]), tonumber(parts[3]), tonumber(parts[4]))
        end
    
      if #unpublished > 0 then
            if notify then
          log('Publishing discovery and current level topics')
          notify = false
        end
        addDiscover(254, unpublished[1].app, unpublished[1].group)
        -- unpublish(255, unpublished[1].app, unpublished[1].group) -- clean up from a bug
        publish(254, unpublished[1].app, unpublished[1].group, GetCBusLevel(0, unpublished[1].app, unpublished[1].group))
        table.remove(unpublished, 1)
        published = published + 1
        if #unpublished == 0 then log('Publishing completed of '..published..' discovery and current level topics') end
      end
    
      if status == 2 then
        log('MQTT send reconnection')
        if client:reconnect() then
          status = 1
        end
      end
     
      --[[
      Send a heartbeat periodically to port 5433. If execution is disrupted by an error then
      this script will be re-started by the 'MQTT lastlevel' resident script
      --]]
      if os.time() - heartbeat >= 10 then
        heartbeat = os.time()
        require('socket').udp():sendto('MQTTsend+'..heartbeat, '127.0.0.1', 5433)
      end
    end
    MQTT receive (resident, 0 sleep):

    Code:
    --[[
    Push MQTT events to Cbus
    --]]
    
    logging = false
    
    mqtt_broker = '192.168.9.10'
    -- mqtt_username = 'YOUR_MQTT_USERNAME'
    -- mqtt_password = 'YOUR_MQTT_PASSWORD'
    mqtt_clientid = 'shac-receive'
    
    mqtt_read_topic = 'cbus/read/'
    mqtt_write_topic = 'cbus/write/#';
    
    lastLevel = {}
    lls = ''
    
    function loadLastLevel()
      local llsTest = storage.get('lastLevel', '')
      if llsTest ~= lls then
        lls = llsTest
        local llt = string.split(lls, ',')
        for _, v in ipairs(llt) do
          parts = string.split(v, '=')
          lastLevel[parts[1]] = tonumber(parts[2])
        end
        end
    end
    
    
    function contains(prefix, text)
      pos = text:find(prefix, 1, true)
      if pos then return pos >= 1 else return false end
    end
    
    
    -- Load MQTT module and create new client
    mqtt = require('mosquitto')
    client = mqtt.new(mqtt_clientid)
    status = 0
    
    
    -- MQTT callbacks
    
    client.ON_CONNECT = function(success)
      if (success) then
        log('MQTT receive connected')
        status = 1
        local mid = client:subscribe(mqtt_write_topic, 2)
      end
    end
    
    client.ON_DISCONNECT = function(...)
      log('MQTT receive disconnected')
      status = 2
    end
    
    client.ON_MESSAGE = function(mid, topic, payload)
      if logging then log(topic .. ' to ' .. payload) end
    
      loadLastLevel()
    
      local parts = string.split(topic, '/')
      local net = 0
      local app = tonumber(parts[4])
      local group = tonumber(parts[5])
    
      if not parts[6] then
        log('MQTT error: Invalid message format')
    
      elseif parts[6] == 'getall' then
        local mqttP = GetCBusByKW('MQTT', 'or')
        local mqttPs = ''
        for k, v in pairs(mqttP) do mqttPs = mqttPs..v['address'][2]..'/'..v['address'][3] end
    
        local datatable = grp.all()
        for key,value in pairs(datatable) do
          parts = string.split(value.address, '/')
                net = tonumber(parts[1])
                app = tonumber(parts[2])
          group = tonumber(parts[3])
            if contains(parts[2]..'/'..parts[3], mqttPs) then -- only publish groups of interest (keyword 'MQTT')
            if app == tonumber(parts[4]) and parts[3] then
              level = tonumber(value.data)
              state = (level ~= 0) and 'ON' or 'OFF'
              if logging then log(parts[3], app, group, state, level) end
              client:publish(mqtt_read_topic .. net .. '/' .. app .. '/' .. group .. '/state', state, 1, true)
              client:publish(mqtt_read_topic .. net .. '/' .. app .. '/' .. group .. '/level', level, 1, true)
            end
            end
            end
    
      elseif parts[6] == 'switch' then
        if payload == 'ON' then
          if logging then log('Payload is ON') end
                SetCBusLevel(0, app, group, 255, 0)
        elseif payload == 'OFF' then
          if logging then log('Payload is OFF') end
          SetCBusLevel(0, app, group, 0, 0)
        end
        
      elseif parts[6] == 'measurement' then        -- UNTESTED
            SetCBusMeasurement(0, app, group, payload, 0)
        
      elseif parts[6] == 'ramp' then
        if payload == 'OPEN' then
          if logging then log("Payload is OPEN, so using RAMP instead") end
          payload = '255'
        elseif payload == 'CLOSE' then
          if logging then log("Payload is CLOSE, so using RAMP instead") end
          payload = '0'
        elseif payload == 'STOP' then
          -- Once a blind level has been set for CBus it is set regardless of the current blind position, which is not updated like a ramp, so a stop command is nonsensical
          if logging then log("Payload is STOP, which is incompatible with CBus... ignoring") end
          do return end
        elseif payload == 'ON' then
          if contains('Fan', GetCBusGroupTag(net, app, group)) then
            if logging then log("Payload is 'Fan' ON, so using RAMP instead") end
            payload = '255'
          else
            if logging then log('Payload is ON') end
            SetCBusLevel(0, app, group, 255, 0)
            do return end
          end
        end
        if payload == 'OFF' then
          if logging then log('Payload is OFF') end
          SetCBusLevel(0, app, group, 0, 0)
        else
          local key = '0'..'/'..app..'/'..group
          parts = string.split(payload, ',')
          local lev = tonumber(parts[1])
          local num = math.floor(lev + 0.5)
          if num and num < 256 then
              if logging then log('Payload is RAMP '..payload) end
            local toSet = 0
            local ramp = 0
            if logging and lastLevel[key] then log('Last level '..lastLevel[key]) end
            if lastLevel[key] and num == 255 then
              if contains('Blindzzz', GetCBusGroupTag(net, app, group)) then -- If blind fully open is desirable instead of lastlevel, then change to actually match a 'Blind' tag
                if logging then log("Payload is 'Blind' ramp on, so ignoring lastlevel") end
                toSet = num
              else
                  toSet = lastLevel[key]
              end
            else
              toSet = num
            end
            if parts[2] ~= nil then ramp = tonumber(parts[2]) else ramp = 0 end
                    SetCBusLevel(0, app, group, toSet, ramp)
          end
        end
      end
    end
    
    
    -- client:login_set(mqtt_username, mqtt_password)
    client:connect(mqtt_broker, 1883, 25)
    client:loop_forever()
    MQTT lastlevel (resident, 0 sleep):

    Code:
    --[[
    Maintain Cbus 'lastlevels'
    
    Used so that MQTT 'on'/ ramp 255 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.
    --]]
    
    logging = false
    
    server = require('socket').udp()
    server:settimeout(0.5)
    server:setsockname('127.0.0.1', 5433)
    
    lastLevel = {}
    receivedOff = {}
    rampDetect = {}
    lastSaved = os.time()
    
    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
    
    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
    
    function processCommand(cmd)
        if logging then log('Command received: '..cmd) end
      local parts = string.split(cmd, '/')
      if not parts[1] or not parts[2] or not parts[3] or not parts[4] then
        do return end
      end
      local key = parts[1]..'/'..parts[2]..'/'..parts[3]
      local level = tonumber(parts[4])
      if level == 0 then
        if not receivedOff[key] or (receivedOff[key] and os.time() - receivedOff[key] >= 30) then
          receivedOff[key] = os.time()
          rampDetect[key] = nil
          if logging then log('Executing OFF ' .. key) end
          if logging then
            if receivedOff[key] then log('Last received OFF '..receivedOff[key]..' more than 30 seconds ago') else log('Last received OFF never') end
          end
        end
      else
        if logging then log('Executing ON ' .. key) end
        local ll = 0
        if not rampDetect[key] then
          rampDetect[key] = level
        end
        if receivedOff[key] and (os.time() - receivedOff[key] < 30) then
          if logging then log('Last received OFF '..receivedOff[key]..' within 30 seconds') end
          if level > rampDetect[key] then -- must be ramping on
            ll = level
          end
        end
        if not receivedOff[key] or (os.time() - receivedOff[key] > 30) then
          if logging then log('Last received off key does not exist or off key is older than 30 seconds') end
          ll = level
        end
        if ll > 0 then
          if logging then log('Saving lastlevel of '..level) end
          lastLevel[key] = ll
        end
      end
    end
    
    function contains(prefix, text)
      pos = text:find(prefix, 1, true)
      if pos then return pos >= 1 else return false end
    end
    
    loadLastLevel()
    
    log('MQTT lastlevel initialised')
    
    -- Loop looking for lastlevel values
    while true do
      cmd = server:receive()
        if cmd and type(cmd) == 'string' then
        if contains('/', cmd) then
            processCommand(cmd)
        elseif contains('+', cmd) then
          parts = string.split(cmd, '+')
          if parts[1] == 'MQTTsend' then
              MQTTsendHeartbeat = tonumber(parts[2])
          end
        end
      end
     
      if os.time() - lastSaved > 2 then
        saveLastLevel()
        lastSaved = os.time()
      end
    
      --[[
      Heartbeats:
     
      The MQTT send script infinite loop occasionally fails, 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 MQTT send heartbeat (last received '..MQTTsendSecondsSince..' seconds ago) - Re-starting MQTT send')
        script.disable('MQTT send')
        script.enable('MQTT send')
        MQTTsendHeartbeat = nil
      end
    end
     
    ssaunders, May 8, 2022
    #5
    MarkB likes this.
  6. ssaunders

    ssaunders

    Joined:
    Dec 17, 2008
    Messages:
    231
    Likes Received:
    31
    Location:
    Melbourne
    So to continue this thread of chatting amongst myself, I've had to make some changes to ensure reliability.

    The first relates to how the 'last level' of groups are detected. The previous method was messed up by groups ramping on. Nothing broke, but last levels were repeatedly saved during the ramp. The code below fixes this.

    The second relates to occasional MQTT disconnects, and how this is handled. Some changes were required to ensure that any restart of the MQTT broker was handled well, and also for any failure of the SHAC send script given it uses an infinite loop.

    And the third is the saving of 'last levels' to storage, which should only happen when a change is made. Sorting the list was required prior to old value comparison to prevent some 'false saves' because of LUA not guaranteeing list order.

    It's been a week of running perfectly, with broker restarted a few times, and much house activity.

    Enjoy. I'm delighted with the result.

    Event-based 'MQTT' script, on required groups having a 'MQTT' label:
    Code:
    --[[
    Pushes CBus events to MQTT resident scripts via internal sockets.
    
    Tag required objects with the "MQTT" keyword and this script will run whenever one of those objects change.
    
    It seems that this script must be disabled/enabled to enable it to be tied to new objects assigned the MQTT keyword.
    --]]
    
    -- Send an event to publish to broker
    require('socket').udp():sendto(event.dst .. "/" .. event.getvalue(), '127.0.0.1', 5432)
    
    -- Send an event to monitor for lastlevel
    require('socket').udp():sendto(event.dst, '127.0.0.1', 5433)
    'MQTT send' resident, zero sleep. Modify tag translation for publishing to the broker using preferred names, as yours definitely will not match mine:
    Code:
    --[[
    Push CBus events to MQTT
    --]]
    
    logging = false
    
    socketTimeout = 0.4 -- Timeout should not be any less to ensure reliability
    mqttBroker = '192.168.9.10'
    -- Un-comment and set values for authentication if needed for the broker
    -- mqttUsername = 'YOUR_mqttUsername'
    -- mqttPassword = 'YOUR_mqttPassword'
    mqttClientId = 'shac-send'
    
    mqttReadTopic = 'cbus/read/'
    mqttDiscoveryTopic = 'cbus'
    
    socket = require('socket')
    
    unpublished = {}
    toPublish = 0
    published = 0
    notify = true
    status = 0
    
    function publishCurrent()
      local mqttP = {}
      retry = 3
      while retry > 0 do -- Very occasionally GetCBusByKW will return no results, so retry three times
        mqttP = GetCBusByKW('MQTT', 'or')
        if #mqttP > 0 then
          retry = 0
        else
          socket.sleep(1)
          retry = retry - 1
        end
      end
     
      n = 1
      for k, v in pairs(mqttP) do
        app = tonumber(v['address'][2])
        group = tonumber(v['address'][3])
        unpublished[n] = {app=app, group=group}
        n = n + 1
      end
      toPublish = n - 1
      published = 0
      log('Queued '..toPublish..' objects with keyword MQTT for publication')
    end
    
    function begins(prefix, text)
      pos = text:find(prefix, 1, true)
      if pos then return pos == 1 else return false end
    end
    
    function contains(prefix, text)
      pos = text:find(prefix, 1, true)
      if pos then return pos >= 1 else return false end
    end
    
    mqtt = require('mosquitto')
    client = mqtt.new(mqttClientId)
    server = require('socket').udp()
    server:settimeout(socketTimeout)
    server:setsockname('127.0.0.1', 5432)
    
    client.ON_CONNECT = function(success)
      if success then
        log('MQTT send connected')
        status = 1
        publishCurrent()
      end
    end
    
    client.ON_DISCONNECT = function(...)
      log('MQTT send disconnected')
      notify = true
      status = 2
    end
    
    if mqttUsername then
      client:login_set(mqttUsername, mqttPassword)
    end
    client:connect(mqttBroker, 1883, 25)
    client:loop_start()
    
    function publish(net, app, group, level)
      if contains('Blind', GetCBusGroupTag(0, app, group)) then
        -- state = (level ~= 0) and 'open' or 'closed'
        state = 'stopped'
      else
        state = (level ~= 0) and 'ON' or 'OFF'
      end
      client:publish(mqttReadTopic..net..'/'..app..'/'..group..'/state', state, 1, true)
      client:publish(mqttReadTopic..net..'/'..app..'/'..group..'/level', level, 1, true)
      if logging then log('Publishing state and level '..mqttReadTopic..net..'/'..app..'/'..group..' to '..state..'/'..level) end
    end
    
    function unpublish(net, app, group)
      client:publish(mqttReadTopic..net..'/'..app..'/'..group..'/state', '', 1, true)
      client:publish(mqttReadTopic..net..'/'..app..'/'..group..'/level', '', 1, true)
      if logging then log('Un-publishing state and level '..mqttReadTopic..net..'/'..app..'/'..group) end
    end
    
    function addDiscover(net, app, group)
      local name = GetCBusGroupTag(0, app, group)
      local sa = ''
      local img = ''
      local dType = ''
      local preferName = ''
    
      -- Set suggested area
      if     begins('Kitchen', name) then sa = 'Kitchen'
      elseif begins('Family', name) then sa = 'Family Room'
      elseif begins('Formal', name) then sa = 'Front Lounge'
      elseif begins('Dining', name) then sa = 'Dining Room'
      elseif begins('Bedroom 1', name) then sa = 'Bedroom 1'
      elseif begins('Bedroom 2', name) then sa = 'Bedroom 2'
      elseif begins('Bedroom 3', name) then sa = 'Bedroom 3'
      elseif begins('Bathroom 1', name) then sa = 'Bathroom 1'
      elseif begins('Bathroom 2', name) then sa = 'Bathroom 2'
      elseif begins('Hall', name) then sa = 'Hall'
      elseif begins('Den', name) then sa = 'Den'
      elseif begins('Laundry', name) then sa = 'Den'
      elseif begins('Outside', name) then sa = 'Outside'
      elseif contains('Storage', name) then sa = 'Store'
      elseif begins('Hutch Bathroom', name) then sa = 'Hutch Bathroom'
      elseif begins('Hutch Kitchen', name) then sa = 'Hutch Kitchen'
      elseif begins('Hutch', name) then sa = 'Hutch'
      elseif begins('Open', name) then sa = 'Outside'
      elseif begins('Close', name) then sa = 'Outside'
      elseif begins('Pool', name) then sa = 'Pool'
      elseif begins('Spa', name) then sa = 'Pool'
      else sa = 'Unknown'
      end
    
      -- Set image
      if     contains('Pool Heat', name) then img = 'mdi:pump'
      elseif contains('Heat', name) then img = 'mdi:radiator'
      elseif contains('Pump', name) then img = 'mdi:pump'
      elseif contains('Blower', name) then img = 'mdi:chart-bubble'
      elseif contains('Gate', name) and contains('Open', name) then img = 'mdi:gate-open'
      elseif contains('Gate', name) and contains('Close', name) then img = 'mdi:gate'
      elseif contains('Sensors', name) then img = 'mdi:motion-sensor-off'
      elseif contains('Sweep Fan', name) then img =  'mdi:ceiling-fan'
      elseif contains('Fan', name) then img = 'mdi:fan'
      elseif contains('Rail Enable', name) then img = 'mdi:radiator-disabled'
      elseif contains('Towel Rail', name) then img = 'mdi:radiator'
      elseif contains('Floor Enable', name) then img = 'mdi:radiator-disabled'
      elseif contains('Under Floor', name) then img = 'mdi:radiator'
      elseif contains('Blind', name) then img = 'mdi:blinds'
      else img = 'mdi:lightbulb'
      end
    
      -- Set type of device
      if img == 'mdi:lightbulb' then dType = 'light'
      elseif img == 'mdi:ceiling-fan' then dType = 'fan'
      elseif img == 'mdi:blinds' then dType = 'cover'
      else dType = 'switch' end
    
      -- Adjust the device name
      if     contains('Pantry LV', name) then preferName = sa..' Pantry Light'
      elseif contains('Dining LV', name) then preferName = sa..' Perimeter Lights'
      elseif contains('Formal LV', name) then preferName = sa..' Perimeter Lights'
      elseif contains('Bedroom 2', name) then preferName = sa..' Light'
      elseif contains('Bedroom 3', name) then preferName = sa..' Light'
      elseif contains('Hall Front', name) then preferName = 'Front Hall'
      elseif contains('Hall Mid', name) then preferName = 'Hall'
      elseif contains('Floor Enable',name) then preferName = sa..' Under Floor Heating Enable'
      elseif contains('Under Floor', name) then preferName = sa..' Under Floor Heating'
      elseif contains('Kitchen', name) and contains('LV', name) then preferName = sa..' Main Lights'
      elseif contains('Bathroom 2', name) and contains('LV 1', name) then preferName = sa..' Main Lights'
      elseif contains('Bathroom 2', name) and contains('LV 2', name) then preferName = sa..' Spa Lights'
      elseif contains('Laundry', name) and not contains('Outside', name) then preferName = 'Laundry Lights'
      elseif contains('Bedroom 1 LV', name) then preferName = sa..' Wardrobe Lights'
      elseif contains('Dining Garden', name) then preferName = 'Outside Cat Hutch Light'
      elseif contains('Verandah Pendant', name) then preferName = 'Verandah Light'
      elseif contains('Verandah LV', name) then preferName = 'Verandah Perimeter Lights'
      elseif contains('Hutch Bathroom', name) then preferName = sa..' Main Light'
      elseif contains('Hutch Kitchen', name) then preferName = 'Galley Lights'
      elseif contains('Central', name) then preferName = sa..' Central Light'
      elseif contains('Hutch Perimeter', name) then preferName = sa..' Perimeter Lights'
      elseif contains('Hutch Shrubbery', name) then preferName = sa..' Shrubbery Lights'
      elseif contains('Hutch Storage', name) then preferName = 'Store Light'
      elseif contains('BBQ', name) then preferName = sa..' BBQ Light'
      elseif contains('Store Flood', name) then preferName = sa..' Store Flood Light'
      elseif contains('Side Door', name) then preferName = sa..' Side Door Light'
      elseif contains('Laundry Door', name) then preferName = sa..' Laundry Door Light'
      elseif contains('Heat 1', name) then preferName = 'Pool Side Heater 1'
      elseif contains('Heat 2', name) then preferName = 'Pool Side Heater 2'
      elseif contains('Heat 3', name) then preferName = 'TV Side Heater 1'
      elseif contains('Heat 4', name) then preferName = 'TV Side Heater 2'
      elseif contains('Pendant', name) then preferName = sa..' Main Light'
      elseif contains('LV', name) then preferName = sa..' Main Lights'
      elseif dType == 'light' then preferName = name..' Lights'
      else preferName = name
      end
    
      if logging then log('Publish discovery '..name..' as '..dType..':'..preferName..' in area '..sa) end
    
      oid = 'cbus_mqtt_'..net..'_'.. app..'_'.. group
    
      if dType == 'light' then
        payload = {
          ['name'] = preferName,
          ['unique_id'] = oid,
          ['ic'] = img,
          ['dev'] = {
            ['ids'] = oid,
            ['sa'] = sa
          },
          ['stat_t'] = 'cbus/read/'..net..'/'..app..'/'..group..'/state',
          ['cmd_t'] = 'cbus/write/'..net..'/'..app..'/'..group..'/switch',
          ['bri_stat_t'] = 'cbus/read/'..net..'/'..app..'/'..group..'/level',
          ['bri_cmd_t'] = 'cbus/write/'..net..'/'..app..'/'..group..'/ramp',
          ['pl_off'] = 'OFF',
          ['on_cmd_type'] = 'brightness',
        }
      elseif dType == 'switch' then
        payload = {
          ['name'] = preferName,
          ['unique_id'] = oid,
          ['ic'] = img,
          ['dev'] = {
            ['ids'] = oid,
            ['sa'] = sa
          },
          ['stat_t'] = 'cbus/read/'..net..'/'..app..'/'..group..'/state',
          ['cmd_t'] = 'cbus/write/'..net..'/'..app..'/'..group..'/switch',
          ['pl_on'] = 'ON',
          ['pl_off'] = 'OFF',
        }
      elseif dType == 'fan' then
        payload = {
          ['name'] = preferName,
          ['unique_id'] = oid,
          ['ic'] = img,
          ['dev'] = {
            ['ids'] = oid,
            ['sa'] = sa
          },
          ['stat_t'] = 'cbus/read/'..net..'/'..app..'/'..group..'/state',
          ['cmd_t'] = 'cbus/write/'..net..'/'..app..'/'..group..'/ramp',
          ['pl_on'] = 'ON',
          ['pl_off'] = 'OFF',
          ['pr_mode_cmd_t'] = 'cbus/write/'..net..'/'..app..'/'..group..'/ramp',
          ['pr_mode_cmd_tpl'] = '{% if value == "low" %} 86 {% elif value == "medium" %} 170 {% elif value == "high" %} 255 {% endif %}',
          ['pr_mode_stat_t'] = 'cbus/read/'..net..'/'..app..'/'..group..'/level',
          ['pr_mode_val_tpl'] = '{% if value == 0 %} OFF {% elif value == 86 %} low {% elif value == 170 %} medium {% elif value == 255 %} high {% endif %}',
          ['pr_modes'] = {'low', 'medium', 'high'}
        }
      elseif dType == 'cover' then
        payload = {
          ['name'] = preferName,
          ['unique_id'] = oid,
          ['ic'] = img,
          ['dev'] = {
            ['ids'] = oid,
            ['sa'] = sa
          },
          ['stat_t'] = 'cbus/read/'..net..'/'..app..'/'..group..'/state',
          ['cmd_t'] = 'cbus/write/'..net..'/'..app..'/'..group..'/ramp',
          ['pos_open'] = 255,
          ['pos_clsd'] = 0,
          ['pl_open'] = 'OPEN',
          ['pl_cls'] = 'CLOSE',
          ['pos_t'] = 'cbus/read/'..net..'/'..app..'/'..group..'/level',
          ['set_pos_t'] = 'cbus/write/'..net..'/'..app..'/'..group..'/ramp',
        }
      end
    
      local j = json.encode(payload)
      local topic = mqttDiscoveryTopic..'/'..dType..'/'..oid..'/config'
    
      if sa == 'changeme' then -- Alter 'changeme' to an area name to remove all topics in that area instead of publishing - for debug
        client:publish(topic, '', 1, true)
      else
        client:publish(topic, j, 1, true)
      end
    end
    
    heartbeat = os.time()
    
    while true do
      cmd = server:receive()
      if cmd and type(cmd) == 'string' then
        parts = string.split(cmd, '/')
        publish(254, tonumber(parts[2]), tonumber(parts[3]), tonumber(parts[4]))
      end
    
      if #unpublished > 0 then
        if notify then
          log('Publishing discovery and current level topics')
          notify = false
        end
        addDiscover(254, unpublished[1].app, unpublished[1].group)
        publish(254, unpublished[1].app, unpublished[1].group, GetCBusLevel(0, unpublished[1].app, unpublished[1].group))
        table.remove(unpublished, 1)
        published = published + 1
        if #unpublished == 0 then log('Publishing completed for '..published..' discovery and current level topics') end
      end
    
      --[[
      Send a heartbeat periodically to port 5433. If execution is disrupted by an error then
      this script will be re-started by the 'MQTT lastlevel' resident script
      --]]
      local stat, err = pcall(function ()
        if os.time() - heartbeat >= 10  and status ~= 2 then
          heartbeat = os.time()
          require('socket').udp():sendto('MQTTsend+'..heartbeat, '127.0.0.1', 5433)
        end
      end)
      if not stat then -- If sending the heartbeat faults then exit the loop - the script will re-start
        log('A fault occurred sending heartbeat. Restarting...')
        do return end
      end
    end
    'MQTT receive' resident, zero sleep:
    Code:
    --[[
    Push MQTT events to CBus
    --]]
    
    logging = false
    
    mqttBroker = '192.168.9.10'
    -- Un-comment and set values for authentication if needed for the broker
    -- mqttUsername = 'YOUR_mqttUsername'
    -- mqttPassword = 'YOUR_mqttPassword'
    mqttClientId = 'shac-receive'
    
    mqttReadTopic = 'cbus/read/'
    mqttWriteTopic = 'cbus/write/#';
    
    lastLevel = {}
    lls = ''
    
    function loadLastLevel()
      local llsTest = storage.get('lastLevel', '')
      if llsTest ~= lls then
        lls = llsTest
        local llt = string.split(lls, ',')
        for _, v in ipairs(llt) do
          parts = string.split(v, '=')
          lastLevel[parts[1]] = tonumber(parts[2])
        end
      end
    end
    
    
    function contains(prefix, text)
      pos = text:find(prefix, 1, true)
      if pos then return pos >= 1 else return false end
    end
    
    
    -- Load MQTT module and create new client
    mqtt = require('mosquitto')
    client = mqtt.new(mqttClientId)
    status = 0
    
    
    -- MQTT callbacks
    
    client.ON_CONNECT = function(success)
      if (success) then
        log('MQTT receive connected')
        status = 1
        local mid = client:subscribe(mqttWriteTopic, 2)
      end
    end
    
    client.ON_DISCONNECT = function(...)
      log('MQTT receive disconnected')
      status = 2
    end
    
    client.ON_MESSAGE = function(mid, topic, payload)
      if logging then log(topic .. ' to ' .. payload) end
    
      loadLastLevel()
    
      local parts = string.split(topic, '/')
      local net = 0
      local app = tonumber(parts[4])
      local group = tonumber(parts[5])
    
      if not parts[6] then
        log('MQTT error: Invalid message format')
    
      elseif parts[6] == 'getall' then
        local mqttP = GetCBusByKW('MQTT', 'or')
        local mqttPs = ''
        for k, v in pairs(mqttP) do mqttPs = mqttPs..v['address'][2]..'/'..v['address'][3] end
    
        local datatable = grp.all()
        for key,value in pairs(datatable) do
          parts = string.split(value.address, '/')
          net = tonumber(parts[1])
          app = tonumber(parts[2])
          group = tonumber(parts[3])
          if contains(parts[2]..'/'..parts[3], mqttPs) then -- only publish groups of interest (keyword 'MQTT')
            if app == tonumber(parts[4]) and parts[3] then
              level = tonumber(value.data)
              state = (level ~= 0) and 'ON' or 'OFF'
              if logging then log(parts[3], app, group, state, level) end
              client:publish(mqttReadTopic .. net .. '/' .. app .. '/' .. group .. '/state', state, 1, true)
              client:publish(mqttReadTopic .. net .. '/' .. app .. '/' .. group .. '/level', level, 1, true)
            end
          end
        end
    
      elseif parts[6] == 'switch' then
        if payload == 'ON' then
          if logging then log('Payload is ON') end
          SetCBusLevel(0, app, group, 255, 0)
        elseif payload == 'OFF' then
          if logging then log('Payload is OFF') end
          SetCBusLevel(0, app, group, 0, 0)
        end
      
      elseif parts[6] == 'measurement' then    -- UNTESTED
        SetCBusMeasurement(0, app, group, payload, 0)
    
      elseif parts[6] == 'ramp' then
        if payload == 'OPEN' then
          if logging then log("Payload is OPEN, so using RAMP instead") end
          payload = '255'
        elseif payload == 'CLOSE' then
          if logging then log("Payload is CLOSE, so using RAMP instead") end
          payload = '0'
        elseif payload == 'STOP' then
          -- Once a blind level has been set for CBus it is set regardless of the current blind position, which is not updated like a ramp, so a stop command is nonsensical
          if logging then log("Payload is STOP, which is incompatible with CBus... ignoring") end
          do return end
        elseif payload == 'ON' then
          if contains('Fan', GetCBusGroupTag(net, app, group)) then
            if logging then log("Payload is 'Fan' ON, so using RAMP instead") end
            payload = '255'
          else
            if logging then log('Payload is ON') end
            SetCBusLevel(0, app, group, 255, 0)
            do return end
          end
        end
        if payload == 'OFF' then
          if logging then log('Payload is OFF') end
          SetCBusLevel(0, app, group, 0, 0)
        else
          local key = '0'..'/'..app..'/'..group
          parts = string.split(payload, ',')
          local lev = tonumber(parts[1])
          local num = math.floor(lev + 0.5)
          if num and num < 256 then
            if logging then log('Payload is RAMP '..payload) end
            local toSet = 0
            local ramp = 0
            if logging and lastLevel[key] then log('Last level '..lastLevel[key]) end
            if lastLevel[key] and num == 255 then
              if contains('Blindzzz', GetCBusGroupTag(net, app, group)) then -- If blind fully open is desirable instead of lastlevel, then change to actually match a 'Blind' tag
                if logging then log("Payload is 'Blind' ramp on, so ignoring lastlevel") end
                toSet = num
              else
                toSet = lastLevel[key]
              end
            else
              toSet = num
            end
            if parts[2] ~= nil then ramp = tonumber(parts[2]) else ramp = 0 end
            SetCBusLevel(0, app, group, toSet, ramp)
          end
        end
      end
    end
    
    
    if mqttUsername then
      client:login_set(mqttUsername, mqttPassword)
    end
    client:connect(mqttBroker, 1883, 25)
    client:loop_forever()
    
    And finally, 'MQTT lastlevel' resident, zero sleep:
    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 = {}
      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
      iterations[remove[i]] = nil
      last[remove[i]] = nil
    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
    
     
    Last edited: May 15, 2022
    ssaunders, May 15, 2022
    #6
    Damaxx likes this.
  7. ssaunders

    ssaunders

    Joined:
    Dec 17, 2008
    Messages:
    231
    Likes Received:
    31
    Location:
    Melbourne
    In a further development, I think this is quite a good generic approach to MQTT discovery.

    In short, I use a series of keywords tagged to each group of interest in the SHAC to allow setting of Home Assistant preferred name, image, type of discovery topic and more. The script polls every ten seconds for changes and applies create/remove/update messages to the MQTT broker.

    If preferred name isn't set it will use name. Default scale = 1, default decimal places = 2, default image and unit = none.

    MQTT send (sleep zero)...

    Code:
    --[[
    Push CBus events to MQTT, and publish discovery topics
    
    Add the keyword 'MQTT' to groups for discovery, plus...
    
      One of  light, fan, cover, sensor or switch
      Plus...
      sa=     Suggested area
      img=    Image
      pn=     Preferred name
      dec=    Decimal places
      unit=   Unit of measurement
      scale=  Multiplier / divider
    
    Examples:
    
    MQTT, light, sa=Outside, pn=Outside Laundry Door Light, img=mdi:lightbulb,
    MQTT, switch, sa=Outside, img=mdi:gate-open,
    MQTT, fan, sa=Hutch, img=mdi:ceiling-fan,
    MQTT, cover, sa=Bathroom 2, img=mdi:blinds,
    MQTT, sensor, sa=Pool, pn=Pool Pool Temperature, unit= °C, dec=1,
    MQTT, sensor, sa=Pool, pn=Pool Level, unit= mm, dec=0, scale=1000,
    --]]
    
    logging = false
    
    socketTimeout = 0.5 -- Timeout should not be any less to ensure reliability as lower values can cause script lock-up for some reason...
    mqttBroker = '192.168.10.21'
    mqttUsername = 'mqtt'
    mqttPassword = 'password'
    mqttClientId = 'shac-send'
    
    mqttReadTopic = 'cbus/read/'
    mqttDiscoveryTopic = 'cbus'
    
    socket = require('socket')
    
    covers = {}
    publishAdj = {}
    unpublished = {}
    toPublish = 0
    published = 0
    notify = true
    status = 0
    
    allgrps = grp.all()
    
    function publishCurrent()
      local mqttP = {}
      mqttP = GetCBusByKW('MQTT', 'or')
      n = 1
      for k, v in pairs(mqttP) do
        app = tonumber(v['address'][2])
        group = tonumber(v['address'][3])
        if v['address'][4] ~= nil then channel = tonumber(v['address'][4]) else channel = nil end
        unpublished[n] = {app=app, group=group, channel=channel}
        n = n + 1
      end
      toPublish = n - 1
      published = 0
      log('Queued '..toPublish..' objects with keyword MQTT for publication')
      status = 3
    end
    
    function begins(prefix, text)
      pos = text:find(prefix, 1, true)
      if pos then return pos == 1 else return false end
    end
    
    function contains(prefix, text)
      pos = text:find(prefix, 1, true)
      if pos then return pos >= 1 else return false end
    end
    
    mqtt = require('mosquitto')
    client = mqtt.new(mqttClientId)
    server = require('socket').udp()
    server:settimeout(socketTimeout)
    server:setsockname('127.0.0.1', 5432)
    
    client.ON_CONNECT = function(success)
      if success then
        log('MQTT send connected')
        status = 1
        publishCurrent()
      end
    end
    
    client.ON_DISCONNECT = function(...)
      log('MQTT send disconnected')
      notify = true
      status = 2
    end
    
    if mqttUsername then
      client:login_set(mqttUsername, mqttPassword)
    end
    client:connect(mqttBroker, 1883, 25)
    client:loop_start()
    
    function publish(net, app, group, level)
      if covers[net..'/'..app..'/'..group] then
        state = 'stopped' -- For CBus blind controllers
      else
        state = (level ~= 0) and 'ON' or 'OFF'
      end
      client:publish(mqttReadTopic..net..'/'..app..'/'..group..'/state', state, 1, true)
      client:publish(mqttReadTopic..net..'/'..app..'/'..group..'/level', level, 1, true)
      if logging then log('Publishing state and level '..mqttReadTopic..net..'/'..app..'/'..group..' to '..state..'/'..level) end
    end
    
    function publishMeasurement(net, app, group, channel, value)
      local adjust = publishAdj[net..'/'..app..'/'..group..'/'..channel]
      if adjust then v = tonumber(string.format('%.'..adjust['dec']..'f', value * adjust['scale'])) else v = value end
      client:publish(mqttReadTopic..net..'/'..app..'/'..group..'_'..channel..'/state', v, 1, true)
      if logging then log('Publishing measurement '..mqttReadTopic..net..'/'..app..'/'..group..'_'..channel..' to '..v) end
    end
    
    
    function addDiscover(net, app, group, channel)
      local name = GetCBusGroupTag(0, app, group)
      if not name then name = '' end
      local pn = name
      local sa = ''
      local img = ''
      local units = ''
      local scale = 1
      local decimals = 2
      local dType = ''
      local preferName = ''
      local tagcache = ''
    
      -- Build an alias to refer to each group
      alias = '0'..'/'..app..'/'..group;  if channel then alias = alias..'/'..channel end
    
      -- Find the keywords (aka tagcache) for the group
      for _, v in ipairs(allgrps) do
        if v['address'] == alias then
          tagcache = v['tagcache']
          break
        end
      end
    
      -- Extract MQTT topic settings
      tags = string.split(tagcache, ', ')
      for _, t in ipairs(tags) do
        tp = string.split(t, '=')
        if tp[2] then
          if tp[1] == 'sa' then sa = tp[2]
          elseif tp[1] == 'pn' then pn = tp[2]
          elseif tp[1] == 'img' then img = tp[2]
          elseif tp[1] == 'unit' then units = tp[2]
          elseif tp[1] == 'dec' then decimals = tonumber(tp[2])
          elseif tp[1] == 'scale' then scale = tonumber(tp[2])
          end
        else
          if tp[1] ~= 'MQTT' then dType = tp[1] end
        end
      end
     
      if logging then log('Publish discovery '..name..' as '..dType..':'..preferName..' in area '..sa) end
    
      -- Build an OID (measurement application gets a channel as well)
      if not channel then
        oid = 'cbus_mqtt_'..net..'_'.. app..'_'..group
      else
        oid = 'cbus_mqtt_'..net..'_'.. app..'_'..group..'_'..channel
      end
    
      -- Build the type-specific payload to publish
      if dType == 'light' then
        payload = {
          ['stat_t'] = 'cbus/read/'..net..'/'..app..'/'..group..'/state',
          ['cmd_t'] = 'cbus/write/'..net..'/'..app..'/'..group..'/switch',
          ['bri_stat_t'] = 'cbus/read/'..net..'/'..app..'/'..group..'/level',
          ['bri_cmd_t'] = 'cbus/write/'..net..'/'..app..'/'..group..'/ramp',
          ['pl_off'] = 'OFF',
          ['on_cmd_type'] = 'brightness',
        }
      elseif dType == 'switch' then
        payload = {
          ['stat_t'] = 'cbus/read/'..net..'/'..app..'/'..group..'/state',
          ['cmd_t'] = 'cbus/write/'..net..'/'..app..'/'..group..'/switch',
          ['pl_on'] = 'ON',
          ['pl_off'] = 'OFF',
        }
      elseif dType == 'fan' then
        payload = {
          ['stat_t'] = 'cbus/read/'..net..'/'..app..'/'..group..'/state',
          ['cmd_t'] = 'cbus/write/'..net..'/'..app..'/'..group..'/ramp',
          ['pl_on'] = 'ON',
          ['pl_off'] = 'OFF',
          ['pr_mode_cmd_t'] = 'cbus/write/'..net..'/'..app..'/'..group..'/ramp',
          ['pr_mode_cmd_tpl'] = '{% if value == "low" %} 86 {% elif value == "medium" %} 170 {% elif value == "high" %} 255 {% endif %}',
          ['pr_mode_stat_t'] = 'cbus/read/'..net..'/'..app..'/'..group..'/level',
          ['pr_mode_val_tpl'] = '{% if value == 0 %} OFF {% elif value == 86 %} low {% elif value == 170 %} medium {% elif value == 255 %} high {% endif %}',
          ['pr_modes'] = {'low', 'medium', 'high'}
        }
      elseif dType == 'cover' then
        payload = {
          ['stat_t'] = 'cbus/read/'..net..'/'..app..'/'..group..'/state',
          ['cmd_t'] = 'cbus/write/'..net..'/'..app..'/'..group..'/ramp',
          ['pos_open'] = 255,
          ['pos_clsd'] = 0,
          ['pl_open'] = 'OPEN',
          ['pl_cls'] = 'CLOSE',
          ['pos_t'] = 'cbus/read/'..net..'/'..app..'/'..group..'/level',
          ['set_pos_t'] = 'cbus/write/'..net..'/'..app..'/'..group..'/ramp',
        }
      elseif dType == 'sensor' then
        payload = {
          ['stat_t'] = 'cbus/read/'..net..'/'..app..'/'..group..'_'..channel..'/state',
        }
      end
    
      -- Add payload common to all
      payload['name'] = pn
      payload['uniq_id'] = oid
      payload['dev'] = { ['ids'] = oid, ['sa'] = sa, ['mf'] = 'Schneider Electric', ['mdl'] = 'CBus' }
      if img ~= '' then payload['ic'] = img end
      if units ~= '' then payload['unit_of_meas'] = units end
    
      local j = json.encode(payload)
      local topic = mqttDiscoveryTopic..'/'..dType..'/'..oid..'/config'
    
      client:publish(topic, j, 1, true)
     
      -- Adjust alias for CBus local network
      alias = '254'..'/'..app..'/'..group
      if channel then alias = alias..'/'..channel end
    
      if decimals ~= 2 or scale ~= 1 then publishAdj[alias] = { ['dec'] = decimals, ['scale'] = scale } end
      if dType == 'cover' then covers[alias] = true end
    end
    
    
    heartbeat = os.time()
    tagcaches = 0
    
    while true do
      cmd = server:receive()
      if cmd and type(cmd) == 'string' then
        parts = string.split(cmd, '/')
        if not parts[5] then
          publish(254, tonumber(parts[2]), tonumber(parts[3]), tonumber(parts[4]))
        else
          publishMeasurement(254, tonumber(parts[2]), tonumber(parts[3]), tonumber(parts[4]), tonumber(parts[5]))
        end
      end
    
      -- Add/update/delete MQTT items
      if os.time() - tagcaches >= 10 and #unpublished == 0 and status == 3 then
        if currTagcache then
          tagcaches = os.time()
          allgrps = grp.all()
          -- Check for differences in keywords set, and for new groups to publish
          newTagcache = {}; for _, v in ipairs(allgrps) do if v['tagcache'] and contains('MQTT', v['tagcache']) then newTagcache[v['address']] = v['tagcache'] end end
          unpublished = {}
          n = 1
          for k, v in pairs(newTagcache) do
            modified = false
            if not currTagcache[k] then modified = true
            elseif currTagcache[k] ~= v then modified = true end
            if modified then
              p = string.split(k, '/')
              app = tonumber(p[2])
              group = tonumber(p[3])
              if p[4] ~= nil then channel = tonumber(p[4]) else channel = nil end
              unpublished[n] = {app=app, group=group, channel=channel}
              n = n + 1
            end
          end
          toPublish = n - 1
          published = 0
          if toPublish > 0 then log('Queued '..toPublish..' objects with keyword MQTT for publication') end
          -- Handle deletions
          for k, v in pairs(currTagcache) do
            if not newTagcache[k] then
              tags = string.split(v, ', ')
              for _, t in ipairs(tags) do
                tp = string.split(t, '=')
                if not tp[2] and tp[1] ~= 'MQTT' then dType = tp[1]; break end
              end
              parts = string.split(k, '/')
              net = 254; app = parts[2]; group = parts[3];
              if parts[4] then oid = 'cbus_mqtt_'..net..'_'.. app..'_'..group..'_'..parts[4] else oid = 'cbus_mqtt_'..net..'_'.. app..'_'..group end
              local topic = mqttDiscoveryTopic..'/'..dType..'/'..oid..'/config'
              log('Remove discovery topic '..topic)
              client:publish(topic, '', 1, true)
              local topic = mqttDiscoveryTopic..'/read/254/'..app..'/'..group;
              client:publish(topic..'/state', '', 1, true)
              client:publish(topic..'/level', '', 1, true)
            end
          end
          currTagcache = newTagcache
        else
          allgrps = grp.all()
          currTagcache = {}; for _, v in ipairs(allgrps) do if v['tagcache'] and contains('MQTT', v['tagcache']) then currTagcache[v['address']] = v['tagcache'] end end
        end
      end
    
      -- Publish any queued additions
      if #unpublished > 0 then
        if notify then
          log('Publishing discovery and current level topics')
          notify = false
        end
        addDiscover(254, unpublished[1].app, unpublished[1].group, unpublished[1].channel)
        if unpublished[1].app ~= 228 then
          publish(254, unpublished[1].app, unpublished[1].group, GetCBusLevel(0, unpublished[1].app, unpublished[1].group))
        else
          publishMeasurement(254, unpublished[1].app, unpublished[1].group, unpublished[1].channel, GetCBusMeasurement(0, unpublished[1].group, unpublished[1].channel))
        end
        table.remove(unpublished, 1)
        published = published + 1
        if #unpublished == 0 then log('Publishing completed for '..published..' discovery and current level topics') end
      end
    
      --[[
      Send a heartbeat periodically to port 5433. If execution is disrupted by an error then
      this script will be re-started by the 'MQTT lastlevel' resident script
      --]]
      local stat, err = pcall(function ()
        if os.time() - heartbeat >= 10  and status ~= 2 then
          heartbeat = os.time()
          require('socket').udp():sendto('MQTTsend+'..heartbeat, '127.0.0.1', 5433)
        end
      end)
      if not stat then -- If sending the heartbeat faults then exit the loop - the script will re-start
        log('A fault occurred sending heartbeat. Restarting...')
        do return end
      end
    end
     
    Last edited: May 22, 2022
    ssaunders, May 22, 2022
    #7
    Timbo likes this.
  8. ssaunders

    ssaunders

    Joined:
    Dec 17, 2008
    Messages:
    231
    Likes Received:
    31
    Location:
    Melbourne
    Probably better, here's my blog post on the subject that will be updated if there are further developments. I've added seamless Philips Hue integration with CBus (so I now get a true "All Off" button from a scene - YAY), and improved a bunch of other aspects.

    https://www.map59.com/home-assistant-cbus-mqtt-and-philips-hue/
     
    ssaunders, May 25, 2022
    #8
    Damaxx likes this.
  9. ssaunders

    Dasman

    Joined:
    May 5, 2011
    Messages:
    36
    Likes Received:
    5
    Location:
    Adelaide
    Dasman, Jun 1, 2022
    #9
  10. ssaunders

    vworp

    Joined:
    Aug 17, 2005
    Messages:
    18
    Likes Received:
    1
    Location:
    Stoke On Trent, UK
    Nice work!
     
    vworp, Jun 4, 2022
    #10
  11. ssaunders

    paddyb

    Joined:
    Aug 9, 2020
    Messages:
    7
    Likes Received:
    1
    Location:
    Ireland
    Hi @ssaunders, Just want to say Hi and to say thanks a million for posting all that info on your blog, amazing stuff!
    You've put a ton of work into that, and have saved me days, if not weeks, of effort!
    I got it all going in about 10 mins, and it has been working perfectly for a week now - Kudos for sharing
     
    paddyb, Jul 11, 2022
    #11
    ssaunders likes this.
  12. ssaunders

    ssaunders

    Joined:
    Dec 17, 2008
    Messages:
    231
    Likes Received:
    31
    Location:
    Melbourne
    A pleasure, and G'day! You might want to check out the changes I made over the past few days, posted only yesterday as an update to that blog article. +Panasonic AC, +ESPHome sensors, plus a significant performance improvement and a crash fix.

    I have 74 MQTT tagged objects, and encounted a crash every evening when I pressed an "all off" button before bed. I think I introduced this bug after my code was posted to the article the last time, so it may not affect you, but the code optimisation I've done is definitely worth checking out as it vastly improves performance for some things. Some functions were taking just under a second to run at my scale, and now execute in around 200ms...

    It also consolidates MQTT send and receive as a single script and lowers traffic by avoiding setting topics if they're already at the same value.

    https://www.map59.com/home-assistant-cbus-mqtt-and-philips-hue/
     
    Last edited: Jul 12, 2022
    ssaunders, Jul 12, 2022
    #12
  13. ssaunders

    ssaunders

    Joined:
    Dec 17, 2008
    Messages:
    231
    Likes Received:
    31
    Location:
    Melbourne
    ssaunders, Aug 7, 2022
    #13
  14. ssaunders

    paddyb

    Joined:
    Aug 9, 2020
    Messages:
    7
    Likes Received:
    1
    Location:
    Ireland
    Many thanks, Steve! I've been following your blog for updates, Github will make it a bit easier to spot the differences between revisions. Really appreciate the continued effort. I've used this method now on a couple of clients installs also and am getting positive results. When you couple this MQTT integration with the relatively new Cloudflared addon in Homeassistant, it provides a fantastic and simple-to-use GUI interface to a C-Bus system, even when the system is installed behind carrier-grade NATs etc. This can only help to aid the longevity of the system as a whole.
     
    Last edited: Aug 8, 2022
    paddyb, Aug 8, 2022
    #14
  15. ssaunders

    ssaunders

    Joined:
    Dec 17, 2008
    Messages:
    231
    Likes Received:
    31
    Location:
    Melbourne
    Hopefully I'm near done with the code at https://github.com/autoSteve/acMqtt for integration of CBus with Home Assistant. It has given, and is still giving me much satisfaction.

    Supported now are:
    • Light
    • Fan
    • Cover
    • Select - set a CBus level to specific values from HA, which I use for blind pre-sets
    • Sensor
    • Switch
    • Binary_sensor
    • Bsensor - a (binary) sensor configurable with custom on/off strings like Motion detected/No motion, or Door open/Door closed
    • Button - trigger level set to trigger things like scenes, or pulse the group for lighting app
    Implemented are lighting, measurement, user parameter and trigger apps on multiple networks, with discoverability by Home Assistant, and a return to last level set for HA 'on' commands.

    Plus bonus Philips Hue integration with CBus, and ESPHome ESP-32 device support in CBus of environment sensors and Panasonic air conditioners.

    Availability with last will/status MQTT topics has now also been added, so if the script stops the devices/entities show in HA as unavailable.

    I love this code. It's made integrating CBus and Home Assistant using an automation controller ultimately so simple and flexible. Thanks to everyone on the CBus forum for their knowledge, tid-bits, hints and suggestions.

    I have licensed it Creative Commons Zero, declaring it public domain. If it's of use, just beer me to say thanks should our paths cross.

    Feel free to hit me up at GitHub for further improvements.
     
    Last edited: Aug 28, 2022
    ssaunders, Aug 28, 2022
    #15
    philthedill, arrikhan and Damaxx like this.
  16. ssaunders

    arrikhan

    Joined:
    Oct 16, 2011
    Messages:
    48
    Likes Received:
    6
    Location:
    Australia
    Hi, I fell in love with your scripts when I ran across this project this weekend. Auto discovery for CBUS in HA is a must and you've helped another soul clean up their HA config... Thankyou!

    I was wondering if your sensor config is exposing CBUS sensors for light values or even just occupancy? I've never had them displayed in HA given their config in CBUS system.
     
    arrikhan, Oct 31, 2022
    #16
    ssaunders likes this.
  17. ssaunders

    ssaunders

    Joined:
    Dec 17, 2008
    Messages:
    231
    Likes Received:
    31
    Location:
    Melbourne
    Bloody great to hear @arrikhan. Glad it's helped.

    I currently use 'sensor' types to display things like pool and spa temperature, rainfall stats, and a near-term forecast. The NAC gets forecast from the Weatherzone API (setting user parameters), and rainfall using a rain sensor that pulses an analogue input. I use the local rainfall to vary irrigation timing, displaying it in HA for no reason whatsoever other than interest.

    The bsensor/binary sensor types I do not personally use, but developed the code for someone else who wanted a motion sensor state indication in HA.
     
    ssaunders, Oct 31, 2022
    #17
    Damaxx and Mr Mark like this.
  18. ssaunders

    philthedill

    Joined:
    Mar 21, 2010
    Messages:
    140
    Likes Received:
    3
    Location:
    Melbourne
    This is simply brilliant - thank you. The instructions are easy to follow and very complete. I now have a couple of switches working 2 ways and some sensors working Cbus to HASS but not vice versa. I am keen to publish my powerwall SOC from HASS to CBus and have created a user variable in CBus and have it visible in MQTT explorer but have failed to publish the sensor value from HASS to MQTT - any hints please?
     
    philthedill, Nov 5, 2022
    #18
  19. ssaunders

    ssaunders

    Joined:
    Dec 17, 2008
    Messages:
    231
    Likes Received:
    31
    Location:
    Melbourne
    My pleasure.

    Some kind of automation in HASS to mirror a powerwall sensor value to a user parameter exposed to HASS via MQTT from a SHAC/NAC?

    Or, you could go get the PowerWall SOC directly using a CBus automation controller in LUA?

    Auth hints: https://forum.logicmachine.net/showthread.php?tid=3308
    API hints: https://github.com/vloschiavo/powerwall2

    I don't have a PowerWall, so can't try things out for you, but the API seems straightforward enough. There's a GET call for state of charge.

    My HUE code makes REST calls, but auth will be different. There may be some hints in that LUA for you if you want to go the PowerWall API via Automation Controller route. I tried to get HA to do the heavy lifting with Philips Hue integration to provide a bridge to CBus. I failed. The HUE LUA code I wrote is the result, where both HA and my NAC talk to the Hue Bridge. So you could do the same with the PowerWall.
     
    ssaunders, Nov 6, 2022
    #19
  20. ssaunders

    philthedill

    Joined:
    Mar 21, 2010
    Messages:
    140
    Likes Received:
    3
    Location:
    Melbourne
    sorry - hit the send button before I started!

    I have put an automation in HASS to publish the sensor value to MQTT broker. I then added a resident script in SHAC to moniitor that parameter and update SHAC when it changes. it works but seems to be "inelegant" compared to some of the other types of devices. "switches", for instance, appear to work 2 ways natively. Is it a characteristic of sensors that it is only one way (which makes sense after all).

    Powerwall drect from SHAC is messy and I have just got it working with HASS (thanks to other good people like you) so will not be taking that route.....

    let's hope I can fumble through with the other stuff I need!
     
    philthedill, Nov 7, 2022
    #20
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.