CoolMaster VRV

CoolMaster VRV integration with LogicMachine #

Task #

The following script will show how to integrate CoolMaster VRV controller with LogicMachine over RS-232 serial port or TCP. The following VRV systems are supported: Daikin, Fujitsu, Gree, Hitachi, LG, Mitsubishi, Mitsubishi Heavy, Panasonic, Samsung, Sanyo, Toshiba.

RS-232 integration #

If your LogicMachine does not have a RS-232 port, you can integrate it through USB to RS232 converter based on CP210x, FT232, PL2303, MCT U232 chips.

Objects #

All objects must have a VRV tag set.

Set object names using the following scheme:

  • 101 on/off – binary
  • 101 temp – 2 byte floating point
  • 101 mode – 1 byte (0 = cool, 1 = dry, 2 = heat)
  • 101 fspeed – 1 byte (0 = low, 1 = medium, 2 = high, 3 = auto)
  • all on/off – binary object to turn all blocks on and off at once

You can create status objects by adding status to the object name, for example:

  • 101 on/off status
  • 101 temp status

Resident script #

Add the following resident script with 0 interval. Replace /dev/RS232 with the respective name of the port on your LM.

if not cm then
  require('serial')
  require('genohm-scada.eibdgm')

  -- knx and coolmaster mapping
  knxtocm = {}
  cmtoknx = {}

  -- value mapping
  modetotext = { [0] = 'cool', [1] = 'dry', [2] = 'heat' }
  texttomode = { cool = 0, dry = 1, heat = 2 }
  speedtotext = { [0] = 'l', [1] = 'm', [2] = 'h', [3] = 'a' }
  texttospeed = { low = 0, med = 1, high = 2, auto = 3 }

  -- get all tagged objects and set mapping
  for _, object in ipairs(grp.tag('VRV')) do
    local address, fn, stat = unpack(object.name:split(' '))

    if address ~= 'all' then
      address = tonumber(address)
    end

    -- status object
    if stat then
      if not cmtoknx[ address ] then
        cmtoknx[ address ] = {}
      end

      cmtoknx[ address ][ fn ] = {
        id = object.id,
        value = grp.getvalue(object.id),
      }
    -- control object
    else
      knxtocm[ object.id ] = {
        address = address,
        fn = fn,
      }
    end
  end

  function updateknx(address, fn, value, datatype)
    local object = cmtoknx[ address ] and cmtoknx[ address ][ fn ] or nil

    -- object not found
    if not object then
      return
    end

    -- no value or same value, no update required
    if value == nil or object.value == value then
      return
    end

    -- save new value and write to knx
    object.value = value
    grp.write(object.id, value, datatype)
  end

  function parseline(line)
    local address, status, setpoint, temp, speed, mode

    address = line:sub(1, 3)
    address = tonumber(address)

    -- address is not a number, cannot parse line
    if not address then
      return
    end

    -- on/off status
    status = line:sub(5, 6):lower() == 'on'
    updateknx(address, 'on/off', status, dt.bool)

    -- setpoint is integer
    setpoint = line:sub(9, 10)
    setpoint = tonumber(setpoint)
    updateknx(address, 'temp', setpoint, dt.float16)

    -- room temp is float, separated by comma
    temp = line:sub(13, 17):gsub(',', '.')
    temp = tonumber(temp)
    updateknx(address, 'temp_room', temp, dt.float16)

    -- speed: low, med, high, auto
    speed = line:sub(20, 23):lower():trim()
    updateknx(address, 'fspeed', texttospeed[ speed ], dt.uint8)

    -- mode: cool, heat, fan, dry, auto
    mode = line:sub(25, 28):lower():trim()
    updateknx(address, 'mode', texttomode[ mode ], dt.uint8)
  end

  -- read single line from serial
  function readline()
    local line, timeout, char, err

    line = {}
    timeout = 10

    -- read until timeout or full line
    while timeout > 0 do
      -- get single char from serial
      char, err = cm:read(1, 0.1)

      -- read timeout
      if not char then
        timeout = timeout - 1
      -- end-of-line
      elseif char == '\n' then
        break
      -- ignore carriage return
      elseif char ~= '\r' then
        table.insert(line, char)
      end
    end

    -- read ok
    if timeout > 0 then
      return table.concat(line)
    end
  end

  -- read current status
  function readstat()
    local line
    cm:flush()
    cm:write('stat\r\n')

    -- read until error or end of stat
    while true do
      line = readline()

      -- timeout or end occured
      if not line or line == 'OK' then
        break
      end

      parseline(line)
    end
  end

  -- send single cmd to coolmaster
  function writecmd(cmd)
    local result

    cm:flush()
    cm:write(cmd .. '\r\n')

    readline() -- command echo
    result = readline() -- command result
    readline() -- new line prompt

    return result
  end

  -- handle group writes
  function eventhandler(event)
    local object, cmd, value, param

    object = knxtocm[ event.dstraw ]
    -- knx object not mapped, ignore
    if not object then
      return
    end

    -- on/off - boolean
    if object.fn == 'on/off' then
      value = knxdatatype.decode(event.datahex, dt.bool)
      cmd = value and 'on' or 'off'
    -- setpoint - floating point
    elseif object.fn == 'temp' then
      value = knxdatatype.decode(event.datahex, dt.float16)
      param = string.format('%.2f', value)
      cmd = 'temp'
    -- mode (fan, dry, auto)
    elseif object.fn == 'mode' then
      value = knxdatatype.decode(event.datahex, dt.uint8)
      cmd = modetotext[ value ]
    -- speed (low, medium, high, auto)
    elseif object.fn == 'fspeed' then
      value = knxdatatype.decode(event.datahex, dt.uint8)
      param = speedtotext[ value ]

      if param then
        cmd = 'fspeed'
      end
    end

    -- got valid command
    if cmd then
      -- all allows only on/off
      if object.address == 'all' then
        cmd = 'all' .. cmd
      -- append address to command
      else
        cmd = cmd .. ' ' .. object.address
      end

      -- append additional parameter if set
      if param then
        cmd = cmd .. ' ' .. param
      end

      writecmd(cmd, true)
    end
  end

  -- coolmaster serial connection
  cm = serial.open('/dev/RS232', { baudrate = 9600 })

  -- knx connection
  client = eibdgm:new()
  client:sethandler('groupwrite', eventhandler)

  -- start-up time
  sec, usec = os.microtime()
end

-- handle knx
client:step()

-- read stats every 3 seconds
diff = os.udifftime(sec, usec)
if diff < 0 or diff >= 3 then
  readstat()
  sec, usec = os.microtime()
end

TCP integration #

Objects #

All objects must have a 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 the object name, for 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 the address of an A/C unit which are both programmed on the 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.

if not client then
  require('socket')
  require('genohm-scada.eibdgm')

  IP = '192.168.3.20'
  PORT = 10102

  -- knx  coolmaster mapping
  knxtocm = {}
  cmtoknx = {}

  function reverse(src)
    local res = {}

    for k, v in ipairs(src) do
      res[ v ] = k - 1
    end

    return res
  end

  -- integertext mode/fspeed value mapping
  modetotext = { 'cool', 'heat', 'fan', 'dry', 'auto' }
  texttomode = reverse(modetotext)
  speedtotext = { 'low', 'med', 'high', 'top', 'auto' }
  texttospeed = reverse(speedtotext)

  -- get all tagged objects and set mapping
  for _, object in ipairs(grp.tag('VRV')) do
    local address, fn, stat = unpack(object.name:split(' '))

    -- status object
    if stat then
      if not cmtoknx[ address ] then
        cmtoknx[ address ] = {}
      end

      cmtoknx[ address ][ fn ] = {
        id = object.id,
        value = grp.getvalue(object.id),
      }
    -- control object
    else
      knxtocm[ object.id ] = {
        address = address,
        fn = fn,
      }
    end
  end

  function checkerror(res, err)
    if not res then
      sock:close()
      sock = nil
      alert('CoolMaster error: %s', tostring(err))
    end

    return res, err
  end

  function connect()
    sock = socket.tcp()
    sock:settimeout(5)

    return checkerror(sock:connect(IP, PORT))
  end

  function send(data)
    if not sock then
      connect()
    end

    if sock then
      checkerror(sock:send(data))
    end
  end

  -- read single line from socket
  function readline()
    if not sock then
      connect()
    end

    if sock then
      return checkerror(sock:receive())
    end
  end

  function updateknx(address, fn, value, datatype)
    local object = cmtoknx[ address ] and cmtoknx[ address ][ fn ] or nil
    -- object not found
    if not object then
      return
    end

    -- no value or same value, no update required
    if value == nil or object.value == value then
      return
    end

    -- save new value and write to knx
    object.value = value
    grp.write(object.id, value, datatype)
  end

  function parseline(line)
    local address, status, setpoint, temp, speed, mode

    line = line:gsub('>', '')
    address = line:sub(1, 6)

    -- address is invalid, cannot parse line
    if address:sub(1, 1) ~= 'L' then
      return
    end

    -- on/off status
    status = line:sub(8, 9):lower() == 'on'
    updateknx(address, 'on/off', status, dt.bool)

    -- setpoint is integer
    setpoint = line:sub(12, 13)
    setpoint = tonumber(setpoint)
    updateknx(address, 'setpoint', setpoint, dt.float16)

    -- room temp is float, separated by comma
    temp = line:sub(16, 17)
    temp = tonumber(temp)
    updateknx(address, 'temp', temp, dt.float16)

    -- speed: low, med, high, auto
    speed = line:sub(20, 23):lower():trim()
    updateknx(address, 'fspeed', texttospeed[ speed ], dt.uint8)

    -- mode: cool, heat, fan, dry, auto
    mode = line:sub(25, 28):lower():trim()
    updateknx(address, 'mode', texttomode[ mode ], dt.uint8)
  end

  -- read current status
  function readstat()
    send('ls\r\n')

    -- read until error or end of stat
    while true do
      local line = readline()

      -- timeout or end occured
      if not line or line == 'OK' then
        break
      end

      parseline(line)
    end
  end

  -- handle group writes
  function eventhandler(event)
    local object, cmd, value, param

    object = knxtocm[ event.dstraw ]
    -- knx object not mapped, ignore
    if not object then
      return
    end

    -- on/off - boolean
    if object.fn == 'on/off' then
      value = knxdatatype.decode(event.datahex, dt.bool)
      cmd = value and 'on' or 'off'
    -- setpoint - floating point
    elseif object.fn == 'setpoint' then
      value = knxdatatype.decode(event.datahex, dt.float16)
      param = string.format('%.1f', value)
      cmd = 'temp'
    -- mode (fan, dry, auto)
    elseif object.fn == 'mode' then
      value = knxdatatype.decode(event.datahex, dt.uint8)
      cmd = modetotext[ value + 1 ]
    -- speed (low, medium, high, auto)
    elseif object.fn == 'fspeed' then
      value = knxdatatype.decode(event.datahex, dt.uint8)
      param = speedtotext[ value + 1 ]

      if param then
        cmd = 'fspeed'
      end
    end

    -- got valid command
    if cmd then
      -- append address to command
      cmd = cmd .. ' ' .. object.address

      -- append additional parameter if set
      if param then
        cmd = cmd .. ' ' .. param
      end

      send(cmd .. '\r\n')

      readline() -- command echo
      result = readline() -- command result
      readline() -- new line prompt
    end
  end

  -- knx connection
  client = eibdgm:new({ timeout = 1 })
  client:sethandler('groupwrite', eventhandler)

  -- start-up time
  sec, usec = os.microtime()
end

-- handle knx
client:step()

-- read stats every 3 seconds
diff = os.udifftime(sec, usec)
if diff < 0 or diff >= 3 then
  readstat()
  sec, usec = os.microtime()
end