Logic Machine Forum
MQTT - KNX integration with direct bindings - Printable Version

+- Logic Machine Forum (https://forum.logicmachine.net)
+-- Forum: LogicMachine eco-system (https://forum.logicmachine.net/forumdisplay.php?fid=1)
+--- Forum: Scripting (https://forum.logicmachine.net/forumdisplay.php?fid=8)
+--- Thread: MQTT - KNX integration with direct bindings (/showthread.php?tid=2671)



MQTT - KNX integration with direct bindings - myg - 02.06.2020

Based on several examples from this forum of working with MQTT, i've decided i need more universal configuration-based binding to integrate my Zigbee devices into KNX network through zigbee2mqtt bridge. With script below it is also possible to bind mqtt endpoints together directly without need to write event scripts.

You also need eventloop script in your user library. Feel free to use/modify any code

1) Create user script "user.mqtt2knx"  below and modify your MQTT bindings using supplied examples. You'll need to restart resident script each time you modify bindings.

Code:
package.preload['mqtt2knx.transformers'] = (function(...)
    local transformers = {
      onoff_to_bool = function(val)
        val = val:lower()
        return val == "on" and true or false
      end,

      bool_to_onoff = function(val)
        return val and "on" or "off"
      end,

      const = function(n)
        return function() return n end
      end,

      nosend = function() return nil end,

      identity = function(x) return x end,
      }
   
    return transformers
  end)

package.preload['mqtt2knx.bindings'] = (function(...)
    local trans = require('mqtt2knx.transformers')
    local ONOFF_TO_BOOL = trans.onoff_to_bool
    local BOOL_TO_ONOFF = trans.bool_to_onoff
    local CONST = trans.const
    local NOSEND = trans.nosend
    local IDENTITY = trans.identity

    -- examples of MQTT bindings (in this case with zigbee2mqtt)
    local m2k_bindings = {
      -- relay on/off bi-directional binding via "state" attribute
      {
        mqtt = 'relay[state]',
        knx = '1/1/1',
        to_knx = ONOFF_TO_BOOL,
        to_mqtt = BOOL_TO_ONOFF
      },
     
      -- Simple sensor one-directional binding
      {
        mqtt = 'door[contact]',
        knx = '1/1/1',
      },
      -- Motion sensor with force override
      -- KNX force overwrite needed if object is sending same message on activation
      {
        mqtt = 'motion[occupancy]',
        knx = '1/1/1',
        force_overwrite = true,
      },
     
      -- button click example, button can send click=single|double|long values
      {
        mqtt = 'button[click=single]',
        knx = '1/1/1',
        to_knx = CONST(true),
        force_overwrite = true
      },

      -- sync with KNX and at the same time directly toggle MQTT relay on button click
      {
        mqtt = 'button[click=double]',
        knx = '1/1/1',
        to_knx = CONST(true),
        force_overwrite = true,
        -- send constant value directly to another MQTT device
        mqtt_bind = 'relay[state=toggle]'
      }, 
     
      -- direct binding of water sensor with water protection relay without KNX mapping
      {
        mqtt = 'water_leak[water_leak]',
        mqtt_bind = 'relay[state]',
        bind_conv = BOOL_TO_ONOFF,
      },
    }

    return m2k_bindings
  end)

package.preload['mqtt2knx.maplist'] = (function(...)
    local maplist = {}
    maplist.__index = maplist
   
    function maplist.new()
      return setmetatable({}, maplist)
    end
   
    function maplist:add(key, value)
      local l = self[key]
      if(not l) then
        self[key] = { value }
      else
        l[#l+1] = value
      end
    end
   
    function maplist:each_match(key, callback)
      local l = self[key]
      if(l) then
        for _, v in ipairs(l) do
          callback(v)
        end
      end
    end
   
    return maplist
  end)

package.preload['mqtt2knx.converter'] = (function(...)
    local query_regex = '^([%w_]+)(%[?([%w_]*)=?([%w_]*)%]?)'

    local function parse_mqtt_query(mqtt_prefix, query)
      local topic, _, prop, val = string.match(query, query_regex)
      if(val == '') then val = nil end

      return {
          topic = mqtt_prefix..'/'..topic,
          property = prop,
          value = val
        }
    end

    local function knx_value_selector(addr)
      local dt = grp.find(addr).datatype
 
      return function(event)
        return knxdatatype.decode(event.datahex, dt)
      end     
    end

    local function knx_msg_converter(addr, fn_valueconv, force_overwrite)
      return function(value)
        value = fn_valueconv(value)
        if(value ~= nil) then
          return {
            type = 'knx',
            addr = addr,
            value = value,
            force_overwrite = force_overwrite
          }
        end
      end
    end

    local function mqtt_value_selector(queryobj)
      return function(msg)
        local val = msg[queryobj.property]
          -- property value filter, used in multi-state objects like button with states click=[single|double|long]
        local vf = queryobj.value

        if ((val ~= nil) and (vf == nil or vf == val)) then
          return val
        end
      end
    end

    local function mqtt_msg_converer(queryobj, fn_valueconv)
      return function(value)
        value = fn_valueconv(value)
        if(value ~= nil) then
          return {
            type = 'mqtt',
            topic = queryobj.topic..'/set',
            value = {
              [queryobj.property] = value
            }
          }
        end
      end
    end

    local function create_converter(selector, msg_converter)
      return function(msg)
        local value = selector(msg)
        if(value ~= nil) then
          return msg_converter(value)
        end
      end
    end

    return {
      parse_query = parse_mqtt_query,
      knx_value = knx_value_selector,
      knx_msg = knx_msg_converter,
      mqtt_value = mqtt_value_selector,
      mqtt_msg = mqtt_msg_converer,
      create = create_converter,
    }

  end)


package.preload['mqtt2knx'] = (function(...)
    local trans = require('mqtt2knx.transformers')
    local converter = require('mqtt2knx.converter')
    local maplist = require('mqtt2knx.maplist')
    local bindings = require('mqtt2knx.bindings')

    local m2k = {}
    m2k.__index = m2k

    local function normalize(mqtt_prefix, cfg)
      local o = {
        mqtt = converter.parse_query(mqtt_prefix, cfg.mqtt),
        knx = cfg.knx,
        to_knx = cfg.to_knx or trans.identity,
        -- most mqtt endpoints are sensors which don't recieve value
        to_mqtt = cfg.to_mqtt or trans.nosend,
        force_overwrite = cfg.force_overwrite or false,
        -- direct binding to another mqtt object
        mqtt_bind = cfg.mqtt_bind and converter.parse_query(mqtt_prefix, cfg.mqtt_bind) or nil,
        bind_conv = cfg.bind_conv or trans.identity
      }
      if(o.mqtt_bind and o.mqtt_bind.value ~= nil) then
        -- when target selector have parameter value set, use it as const
        o.bind_conv = trans.const(o.mqtt_bind.value)
      end

      return o
    end


    function m2k.create(mqtt_prefix)
      local o = setmetatable({}, m2k)
      o.list = maplist.new()

      local list = o.list

      for _, b in ipairs(bindings) do
        local cfg = normalize(mqtt_prefix, b)
       
        local mqtt_val = converter.mqtt_value(cfg.mqtt)
        if(cfg.knx) then
          local mqtt_msg = converter.mqtt_msg(cfg.mqtt, cfg.to_mqtt)
          local knx_val = converter.knx_value(cfg.knx)
          local knx_msg = converter.knx_msg(cfg.knx, cfg.to_knx, cfg.force_overwrite)
         
          -- create back and fourth converters
          list:add(cfg.mqtt.topic, converter.create(mqtt_val, knx_msg))
          list:add(cfg.knx, converter.create(knx_val, mqtt_msg))
        end

        if(cfg.mqtt_bind) then
          -- direct mqtt2mqtt bind
          local bind_msg = converter.mqtt_msg(cfg.mqtt_bind, cfg.bind_conv)
          list:add(cfg.mqtt.topic, converter.create(mqtt_val, bind_msg))
        end
      end

      return o
    end

    function m2k:convert(key, msg)
      local res = {}
      self.list:each_match(key, function(fn_convert)
        res[#res + 1] = fn_convert(msg)
      end)
      return res
    end

    function m2k:has_key(key)
      return self.list[key] ~= nil
    end

    return m2k
  end)

return require('mqtt2knx')


2) create resident script (timeout doesn't matter), update with your MQTT broker config and MQTT topic


Code:
local socket = require('socket')
-- create mapper for zigbee/device_name topics
local mapper = require('user.mqtt2knx').create('zigbee')
local JSON = require('json')
local eventloop = require('user.eventloop')

local mqtt_cfg = {
  broker = 'IP',
  login = 'USERNAME',
  pwd = 'PASSWORD',
  id = 'CLIENT_ID',
  topics = 'zigbee/+',
  }
local mqtt_client


function mqtt_onmessage(mid, topic, payload)
  local json = JSON.pdecode(payload)
  if(not json) then
    log('mqtt payload is not a json', payload)
    return
  end
 
  local messages = mapper:convert(topic, json)
  send_messages(messages)
end

function localbus_onmessage(event)
  if(event.sender == 'mq') then
    return
  end

  local messages = mapper:convert(event.dst, event)
  send_messages(messages)
end

function localbus_onread(event)
  -- if it's registered mqtt object then send back cached response
  if(mapper:has_key(event.dst)) then
  local value = grp.getvalue(event.dst)
    --log('reading knx (src/dst/val)', event.src, event.dst, value)
    grp.response(event.dst, value, event.datatype)
  end
end

function send_messages(list)
  for _, m in ipairs(list) do
    if(m.type == 'knx') then
      if(m.force_overwrite or grp.getvalue(m.addr) ~= m.value) then
        --log('sending to knx', m.addr, m.value)
        grp.sender = 'mq'
        grp.write(m.addr, m.value)
      end
    elseif(m.type == 'mqtt') then
      --log('sending to mqtt', m.topic, m.value)
      mqtt_client:publish(m.topic, JSON.encode(m.value))
    end
  end
end

function setup()
  local loop = eventloop.get()
 
  local mqtt = loop.create_mqtt(mqtt_cfg, mqtt_onmessage)
  local bus = loop.create_localbus({ groupwrite = localbus_onmessage, groupread = localbus_onread})
 
  loop:add(mqtt)
  loop:add(bus)
 
  mqtt_client = mqtt.client
end



if(not mqtt_client) then
  setup()
end
log('starting mqtt2knx loop')
eventloop.get():run_loop()



RE: MQTT - KNX integration with direct bindings - ralwet - 03.06.2020

Thank you!! Smile