Example: CoolMasterNet integration

Task

CoolMasterNet device uses TCP instead of RS-232 for communication (as showed in this example), so we’ve updated the script to work with IP.

Create objects on LogicMachine

All objects must have VRV tag set.

Set object names, using the following scheme (full address and function):
L1.101 on/off – 1-bit, unit on/off control
L1.101 setpoint – unit setpoint, 2-byte floating point
L1.101 mode – unit mode, 1-byte (0 = cool, 1 = heat, 2 = fan, 3 = dry, 4 = auto)
L1.101 fspeed – fan speed, 1-byte (0 = low, 1 = medium, 2 = high, 3 = top, 4 = auto)
L1 on/off – 1-bit, on/off control for all units on a given line
L* on/off – 1-bit, on/off control for all units on all lines

You can create status objects by adding “status” to object name, example:
L1.101 on/off status
L1.101 setpoint status
L1.101 mode status
L1.101 fspeed status
L1.101 temp status – room temperature, 2-byte floating point

Note! L1 corresponds to “Line 1” and 101 corresponds to address of A/C unit which are both programmed on CoolMaster device.

Resident script

Create a resident script with 0 sleep interval, change 192.168.3.20 to CoolMasterNet device IP. Status information is polled every 3 seconds.

Source code    
  1. if not client then
  2.   require('socket')
  3.   require('genohm-scada.eibdgm')
  4.  
  5.   IP = '192.168.3.20'
  6.   PORT = 10102
  7.  
  8.   -- knx coolmaster mapping
  9.   knxtocm = {}
  10.   cmtoknx = {}
  11.  
  12.   function reverse(src)
  13.     local res = {}
  14.  
  15.     for k, v in ipairs(src) do
  16.       res[ v ] = k - 1
  17.     end
  18.  
  19.     return res
  20.   end
  21.  
  22.   -- integertext mode/fspeed value mapping
  23.   modetotext = { 'cool', 'heat', 'fan', 'dry', 'auto' }
  24.   texttomode = reverse(modetotext)
  25.   speedtotext = { 'low', 'med', 'high', 'top', 'auto' }
  26.   texttospeed = reverse(speedtotext)
  27.  
  28.   -- get all tagged objects and set mapping
  29.   for _, object in ipairs(grp.tag('VRV')) do
  30.     local address, fn, stat = unpack(object.name:split(' '))
  31.  
  32.     -- status object
  33.     if stat then
  34.       if not cmtoknx[ address ] then
  35.         cmtoknx[ address ] = {}
  36.       end
  37.  
  38.       cmtoknx[ address ][ fn ] = {
  39.         id = object.id,
  40.         value = grp.getvalue(object.id),
  41.       }
  42.     -- control object
  43.     else
  44.       knxtocm[ object.id ] = {
  45.         address = address,
  46.         fn = fn,
  47.       }
  48.     end
  49.   end
  50.  
  51.   function checkerror(res, err)
  52.     if not res then
  53.       sock:close()
  54.       sock = nil
  55.       alert('CoolMaster error: %s', tostring(err))
  56.     end
  57.  
  58.     return res, err
  59.   end
  60.  
  61.   function connect()
  62.     sock = socket.tcp()
  63.     sock:settimeout(5)
  64.  
  65.     return checkerror(sock:connect(IP, PORT))
  66.   end
  67.  
  68.   function send(data)
  69.     if not sock then
  70.       connect()
  71.     end
  72.  
  73.     if sock then
  74.       checkerror(sock:send(data))
  75.     end
  76.   end
  77.  
  78.   -- read single line from socket
  79.   function readline()
  80.     if not sock then
  81.       connect()
  82.     end
  83.  
  84.     if sock then
  85.       return checkerror(sock:receive())
  86.     end
  87.   end
  88.  
  89.   function updateknx(address, fn, value, datatype)
  90.     local object = cmtoknx[ address ] and cmtoknx[ address ][ fn ] or nil
  91.     -- object not found
  92.     if not object then
  93.       return
  94.     end
  95.  
  96.     -- no value or same value, no update required
  97.     if value == nil or object.value == value then
  98.       return
  99.     end
  100.  
  101.     -- save new value and write to knx
  102.     object.value = value
  103.     grp.write(object.id, value, datatype)
  104.   end
  105.  
  106.   function parseline(line)
  107.     local address, status, setpoint, temp, speed, mode
  108.  
  109.     line = line:gsub('>', '')
  110.     address = line:sub(1, 6)
  111.  
  112.     -- address is invalid, cannot parse line
  113.     if address:sub(1, 1) ~= 'L' then
  114.       return
  115.     end
  116.  
  117.     -- on/off status
  118.     status = line:sub(8, 9):lower() == 'on'
  119.     updateknx(address, 'on/off', status, dt.bool)
  120.  
  121.     -- setpoint is integer
  122.     setpoint = line:sub(12, 13)
  123.     setpoint = tonumber(setpoint)
  124.     updateknx(address, 'setpoint', setpoint, dt.float16)
  125.  
  126.     -- room temp is float, separated by comma
  127.     temp = line:sub(16, 17)
  128.     temp = tonumber(temp)
  129.     updateknx(address, 'temp', temp, dt.float16)
  130.  
  131.     -- speed: low, med, high, auto
  132.     speed = line:sub(20, 23):lower():trim()
  133.     updateknx(address, 'fspeed', texttospeed[ speed ], dt.uint8)
  134.  
  135.     -- mode: cool, heat, fan, dry, auto
  136.     mode = line:sub(25, 28):lower():trim()
  137.     updateknx(address, 'mode', texttomode[ mode ], dt.uint8)
  138.   end
  139.  
  140.   -- read current status
  141.   function readstat()
  142.     send('ls\r\n')
  143.  
  144.     -- read until error or end of stat
  145.     while true do
  146.       local line = readline()
  147.  
  148.       -- timeout or end occured
  149.       if not line or line == 'OK' then
  150.         break
  151.       end
  152.  
  153.       parseline(line)
  154.     end
  155.   end
  156.  
  157.   -- handle group writes
  158.   function eventhandler(event)
  159.     local object, cmd, value, param
  160.  
  161.     object = knxtocm[ event.dstraw ]
  162.     -- knx object not mapped, ignore
  163.     if not object then
  164.       return
  165.     end
  166.  
  167.     -- on/off - boolean
  168.     if object.fn == 'on/off' then
  169.       value = knxdatatype.decode(event.datahex, dt.bool)
  170.       cmd = value and 'on' or 'off'
  171.     -- setpoint - floating point
  172.     elseif object.fn == 'setpoint' then
  173.       value = knxdatatype.decode(event.datahex, dt.float16)
  174.       param = string.format('%.1f', value)
  175.       cmd = 'temp'
  176.     -- mode (fan, dry, auto)
  177.     elseif object.fn == 'mode' then
  178.       value = knxdatatype.decode(event.datahex, dt.uint8)
  179.       cmd = modetotext[ value + 1 ]
  180.     -- speed (low, medium, high, auto)
  181.     elseif object.fn == 'fspeed' then
  182.       value = knxdatatype.decode(event.datahex, dt.uint8)
  183.       param = speedtotext[ value + 1 ]
  184.  
  185.       if param then
  186.         cmd = 'fspeed'
  187.       end
  188.     end
  189.  
  190.     -- got valid command
  191.     if cmd then
  192.       -- append address to command
  193.       cmd = cmd .. ' ' .. object.address
  194.  
  195.       -- append additional parameter if set
  196.       if param then
  197.         cmd = cmd .. ' ' .. param
  198.       end
  199.  
  200.       send(cmd .. '\r\n')
  201.  
  202.       readline() -- command echo
  203.       result = readline() -- command result
  204.       readline() -- new line prompt
  205.     end
  206.   end
  207.  
  208.   -- knx connection
  209.   client = eibdgm:new({ timeout = 1 })
  210.   client:sethandler('groupwrite', eventhandler)
  211.  
  212.   -- start-up time
  213.   sec, usec = os.microtime()
  214. end
  215.  
  216. -- handle knx
  217. client:step()
  218.  
  219. -- read stats every 3 seconds
  220. diff = os.udifftime(sec, usec)
  221. if diff < 0 or diff >= 3 then
  222.   readstat()
  223.   sec, usec = os.microtime()
  224. end