Задача: Управлять устройствами HDL (диммером и реле) из сети KNX
ПРИМЕЧАНИЕ: Использование LogicMachine позволит вам управлять HDL-устройствами также из сети Modbus, BACnet, или EnOcean.
В настоящий момент реализована поддержка только объектов типа 05.001 scale и битовых.
ШАГ 1 — Нужно добавить программный код функции HDL в LogicMachine
Добавьте пользовательскую библиотеку (проверьте) с именем «HDL» через меню Scripting -> User libraries:
require("apps")
require('json')
require('encdec')
require('socket')
-----------------------------------------------------------------
function data_print(data)
local str = ''
for index, value in pairs(data) do
c = string.format('%02X', value)
str = str .. ' ' .. c
end
return str
end
HDL = {
-- if true then using ip interface else using RS485 interface
use_ip = true,
-- destination ip
dstip = '192.168.1.7',
-- packet constant data
magic = 'HDLMIRACLE',
-- leading code
lcode = string.char(0xAA, 0xAA),
-- source device settings
srcsubnet = 1,
srcdevice = 254,
devicetype = 0xFFFE,
-- command types
cmd = {
scene = 0x0002, -- scene select
scenereply = 0x0003, -- scene select answerback
sequence = 0x001A, -- sequence select
sequencereply = 0x001B, -- sequence select answerback
chanreg = 0x0031, -- single channel regulate
chanregreply = 0x0032, -- single channel regulate answerback
chanstat = 0x0033, -- read status of single channel targets
chanstatreply = 0x0034, -- single channel targets status answerback
universalswitch = 0xE01C,
universalswitchreply = 0xE01D,
curtainswitch = 0xE3E0,
curtainswitchreply = 0xE3E1,
broadcasttemperature = 0xE3E5,
readtemperature = 0xE3E7,
readtemperaturereply = 0xE3E8,
readtemperaturenew = 0x1948,
readtemperaturenewreply = 0x1949,
readstatus = 0x1604, -- Влажность запрос
readstatusreply = 0x1605, -- Влажность ответ
panelreadstatus = 0xE3DA,
panelreadstatusreply = 0xE3DB,
readfloorheatingstatus = 0x1944,
readfloorheatingstatusreply = 0x1945
}
}
function bytes_to_float(b4, b3, b2, b1)
local ffi = require('ffi')
local fl = ffi.new('float[1]')
local ch = ffi.new('unsigned char[4]', b4, b3, b2, b1)
ffi.copy(fl, ch, 4)
return fl[0]
end
HDL.init = function()
local ip, chunk, chunks, data
-- read interface data
vv = io.readproc('if-json')
data = json.pdecode(vv)
if not data or not data.eth0 then error('cannot get interface data') end
-- ip header
HDL.iphdr = ''
-- broadcast address
HDL.bcast = data.eth0.bcast
-- source ip
HDL.srcip = data.eth0.inetaddr
-- split ip address into chunks
chunks = HDL.srcip:split('.')
-- add ip address chunks
for i = 1, 4 do
chunk = tonumber(chunks[i])
HDL.iphdr = HDL.iphdr .. string.char(chunk)
end
end
HDL.decode = function(packet)
tt = {packet:byte(1, packet:len())}
-- log('decode',data_print(tt))
if HDL.use_ip then
pack = packet:sub(15)
local t1 = {}
t1 = HDL.decode_base(pack)
t1.srcip = string.format('%d.%d.%d.%d', packet:byte(1, 4))
return t1
else
return HDL.decode_base(packet)
end
end
HDL.decode_base = function(packet)
local len, data, src, crc
tt = {packet:byte(1, packet:len())}
-- log('decode_base',data_print(tt))
-- leading code
if packet:sub(1, 2) ~= HDL.lcode then return nil, 'lcode' end
-- get data length and check against
len = packet:byte(3)
if len and len + 2 ~= packet:len() then return nil, 'len' end
-- get packet data and check crc
data = packet:sub(3, len)
crc = packet:byte(len + 1) * 0x100 + packet:byte(len + 2)
if encdec.crc16(data) ~= crc then return nil, 'crc' end
-- return parsed packet
return {
srcsubnet = packet:byte(4),
srcdevice = packet:byte(5),
devicetype = (packet:byte(6) * 0x100 + packet:byte(7)),
opcode = (packet:byte(8) * 0x100 + packet:byte(9)),
dstsubnet = packet:byte(10),
dstdevice = packet:byte(11),
additional = packet:sub(12, len)
}
end
HDL.word = function(v)
return string.char(bit.band(bit.rshift(v, 8), 0xFF), bit.band(v, 0xFF))
end
HDL.encode = function(cmd, dstsubnet, dstdevice, extra)
local packet, len, crc, data
log('encode', cmd, dstsubnet, dstdevice, extra)
-- perform init if required
if not HDL.iphdr then HDL.init() end
packet = {}
if HDL.use_ip then
-- start packet: ip, magic and leading code
packet = {HDL.iphdr, HDL.magic, HDL.lcode}
else
packet = {HDL.lcode}
end
-- base data
data =
string.char(HDL.srcsubnet, HDL.srcdevice) .. HDL.word(HDL.devicetype) ..
HDL.word(cmd) .. string.char(dstsubnet, dstdevice)
-- add extra data parameters
if type(extra) == 'string' then data = data .. extra end
-- calculate length and crc
len = string.char(data:len() + 3)
crc = encdec.crc16(len .. data)
table.insert(packet, len)
table.insert(packet, data)
table.insert(packet, HDL.word(crc))
return table.concat(packet)
end
HDL.send = function(packet)
tt = {packet:byte(1, packet:len())}
log('send', data_print(tt))
if HDL.use_ip then
local client = socket.udp()
client:sendto(packet, HDL.dstip, 6000)
else
log('write port', HDL.port:write(packet))
end
end
HDL.sendcmd = function(cmd, dstsubnet, dstdevice, a, b, c)
local extra, packet
extra = string.char(a, b) .. HDL.word(c or 0)
packet = HDL.encode(cmd, dstsubnet, dstdevice, extra)
HDL.send(packet)
end
HDL.sendcmd1 = function(cmd, dstsubnet, dstdevice, a)
local extra, packet
extra = string.char(a)
packet = HDL.encode(cmd, dstsubnet, dstdevice, extra)
HDL.send(packet)
end
HDL.chanreg = function(dstsubnet, dstdevice, chan, value, delay)
if type(value) == 'boolean' then value = value and 255 or 0 end
HDL.sendcmd(HDL.cmd.chanreg, dstsubnet, dstdevice, chan, value, delay)
end
HDL.sequence = function(dstsubnet, dstdevice, area, value)
HDL.sendcmd(HDL.cmd.sequence, dstsubnet, dstdevice, area, value)
end
HDL.scene = function(dstsubnet, dstdevice, area, scene)
HDL.sendcmd(HDL.cmd.scene, dstsubnet, dstdevice, area, scene)
end
HDL.universal_switch = function(dstsubnet, dstdevice, id, value)
HDL.sendcmd(HDL.cmd.universalswitch, dstsubnet, dstdevice, id, channel,
value)
end
HDL.curtain_switch = function(dstsubnet, dstdevice, switch, status)
-- if Curtain no. < 17 then 0 = STOP, 1 = OPEN, 2 = CLOSE
-- if Curtain no. = 17 then 0-100% percent
HDL.sendcmd(HDL.cmd.curtainswitch, dstsubnet, dstdevice, switch, status)
end
HDL.readtemperature = function(dstsubnet, dstdevice, chan)
HDL.sendcmd1(HDL.cmd.readtemperature, dstsubnet, dstdevice, chan, 0)
end
HDL.readtemperaturenew = function(dstsubnet, dstdevice, chan)
HDL.sendcmd1(HDL.cmd.readtemperaturenew, dstsubnet, dstdevice, chan, 0)
end
HDL.readstatus = function(dstsubnet, dstdevice, chan)
HDL.sendcmd1(HDL.cmd.readstatus, dstsubnet, dstdevice, chan, 0)
end
HDL.chanregreply = function(chan, value)
local extra, packet
extra = string.char(chan, 0xF8, value, chan)
packet = HDL.encode(HDL.cmd.chanregreply, 255, 255, extra)
HDL.send(packet)
end
HDL.scenereply = function(area, scene)
local extra, packet
log('scenereply', area, scene)
extra = string.char(area, scene, 1, 1)
packet = HDL.encode(HDL.cmd.scenereply, 255, 255, extra)
HDL.send(packet)
end
HDL.sequencereply = function(area, seq)
local extra, packet
log('sequencereply', area, seq)
extra = string.char(area, seq)
packet = HDL.encode(HDL.cmd.sequencereply, 255, 255, extra)
HDL.send(packet)
end
HDL.universalswitchreply = function(id, value)
local extra, packet
if value > 0 then value = 1 end
-- log({cmd = 'universalswitchreply into', channel =channel, value = value, subnet = packet.srcsubnet,devid = packet.srcdevice})
log('universalswitchreply into', id, value)
extra = string.char(id, value)
packet = HDL.encode(HDL.cmd.universalswitchreply, 255, 255, extra)
log('switchreply', packet)
HDL.send(packet)
end
HDL.curtainswitchreply = function(dstsubnet, dstdevice, switch, status)
-- if Curtain no. < 17 then 0 = STOP, 1 = OPEN, 2 = CLOSE
-- if Curtain no. = 17 then 0-100% percent
HDL.sendcmd(HDL.cmd.curtainswitchreply, dstsubnet, dstdevice, switch, status)
end
HDL.panelreadstatus = function(dstsubnet, dstdevice, chan)
HDL.sendcmd1(HDL.cmd.panelreadstatus, dstsubnet, dstdevice, chan, 0)
end
HDL.parse = function(packet)
-- log('parse')
if packet.opcode == HDL.cmd.chanreg then
channel = packet.additional:byte(1)
value = packet.additional:byte(2)
log('chanreg')
for index, rec in ipairs(hdltoknx) do
if packet.dstsubnet == rec.hdl_net and packet.dstdevice ==
rec.hdl_dev and channel == rec.channel and packet.opcode ==
rec.type then
log('chanreg found', rec, value)
grp.write(rec.knx_obj, value)
HDL.chanregreply(channel, value)
return
end
end
elseif packet.opcode == HDL.cmd.chanregreply then
channel = packet.additional:byte(1)
err = packet.additional:byte(2)
value = packet.additional:byte(3)
log({
cmd = 'chanregreply',
channel = channel,
value = value,
subnet = packet.srcsubnet,
devid = packet.srcdevice
})
for index, rec in ipairs(hdltoknx) do
if packet.srcsubnet == rec.hdl_net and packet.srcdevice ==
rec.hdl_dev and channel == rec.channel and packet.opcode ==
rec.type then
log('chanregreply found', rec, value)
if err == 0xF8 then -- F8 F8 = Success, F5 = Fail
grp.write(rec.knx_obj, value)
end
return
end
end
elseif packet.opcode == HDL.cmd.scene then
area = packet.additional:byte(1)
scene = packet.additional:byte(2)
log('scene', area, scene)
for index, rec in ipairs(hdltoknx) do
if packet.dstsubnet == rec.hdl_net and packet.dstdevice ==
rec.hdl_dev and area == rec.area and packet.opcode == rec.type then
log('scene found', rec.knx_obj, scene)
grp.write(rec.knx_obj, scene)
HDL.scenereply(area, scene)
return
end
end
elseif packet.opcode == HDL.cmd.sequence then
area = packet.additional:byte(1)
seq = packet.additional:byte(2)
log('sequence', area, seq)
for index, rec in ipairs(hdltoknx) do
if packet.dstsubnet == rec.hdl_net and packet.dstdevice ==
rec.hdl_dev and area == rec.area and packet.opcode == rec.type then
log('sequence found', rec.knx_obj, seq)
grp.write(rec.knx_obj, seq)
HDL.sequencereply(area, seq)
return
end
end
elseif packet.opcode == HDL.cmd.universalswitch then
channel = packet.additional:byte(1)
value = packet.additional:byte(2)
-- log('universalswitch debug',packet.additional, #packet.additional, channel, value)
-- log('universalswitch',channel,value, packet.dstsubnet, packet.dstdevice, packet)
log({
cmd = 'universalswitch',
channel = channel,
value = value,
subnet = packet.dstsubnet,
devid = packet.dstdevice
})
for index, rec in ipairs(hdltoknx) do
-- log(packet.dstsubnet == rec.hdl_net,packet.dstdevice , rec.hdl_dev, packet.dstdevice == rec.hdl_dev, channel == rec.channel, packet.opcode == rec.type)
if packet.dstsubnet == rec.hdl_net and packet.dstdevice ==
rec.hdl_dev and channel == rec.channel and packet.opcode ==
rec.type then
log('universalswitch found', rec.knx_obj, value, channel)
grp.write(rec.knx_obj, value)
log(channel, value)
HDL.universalswitchreply(channel, value)
return
end
end
elseif packet.opcode == HDL.cmd.broadcasttemperature then
channel = packet.additional:byte(1)
value = packet.additional:byte(2)
log('broadcasttemperature', channel, value, packet.srcsubnet,
packet.srcdevice)
for index, rec in ipairs(hdltoknx) do
if packet.srcsubnet == rec.hdl_net and packet.srcdevice ==
rec.hdl_dev and packet.opcode == rec.type and channel ==
rec.channel then
log('broadcasttemperature found', rec.knx_obj, value)
grp.write(rec.knx_obj, value)
return
end
end
elseif packet.opcode == HDL.cmd.readtemperaturereply then
channel = packet.additional:byte(1)
value = packet.additional:byte(2)
value = bit.band(value, 63)
sign = bit.band(value, 64)
if sign == 1 then value = -1 * value end
log('readtemperaturereply', channel, value, packet.srcsubnet,
packet.srcdevice)
for index, rec in ipairs(hdltoknx) do
-- if packet.srcsubnet == rec.hdl_net and packet.srcdevice == rec.hdl_dev and packet.opcode == rec.type and channel == rec.channel then
if packet.srcsubnet == rec.hdl_net and packet.srcdevice ==
rec.srcdevice and packet.opcode == rec.type and channel ==
rec.channel then
log('readtemperaturereply found', rec.knx_obj, value)
log('readtemperaturereply')
grp.write(rec.knx_obj, value)
return
end
end
elseif packet.opcode == HDL.cmd.readtemperaturenewreply then
channel = packet.additional:byte(1)
b1 = packet.additional:byte(2)
b2 = packet.additional:byte(3)
b3 = packet.additional:byte(4)
b4 = packet.additional:byte(5)
value = bytes_to_float(b1, b2, b3, b4)
log('readtemperaturenewreply', channel, value, packet.srcsubnet,
packet.srcdevice)
for index, rec in ipairs(hdltoknx) do
if packet.srcsubnet == rec.hdl_net and packet.srcdevice ==
rec.hdl_dev and packet.opcode == rec.type and channel ==
rec.channel then
log('readtemperaturereply found', rec.knx_obj, value)
grp.write(rec.knx_obj, value)
return
end
end
elseif packet.opcode == HDL.cmd.readstatusreply then
channel = packet.additional:byte(1)
b1 = packet.additional:byte(2) -- success or fail (f8 or f5)
b2 = packet.additional:byte(3) -- temp 0-80 (-20-60 C)
b3 = packet.additional:byte(4) -- brightness
b4 = packet.additional:byte(5) -- hum
b5 = packet.additional:byte(6) -- air
b6 = packet.additional:byte(7) -- gas
b7 = packet.additional:byte(8) -- motion sensor
b8 = packet.additional:byte(9) -- dry contact 1
b9 = packet.additional:byte(10) -- dry contact 2
b10 = packet.additional:byte(11) -- UV switch 1
b11 = packet.additional:byte(12) -- UV switch 1
value = packet.additional:byte(5)
-- log({cmd = 'readstatusreply',channel = channel,value ={b1,b2,b3,b4,b5,b6,b7,b8,b9,b10,b11},subnet = packet.srcsubnet,devid = packet.srcdevice})
log({
cmd = 'readstatusreply',
channel = channel,
value = {b4},
subnet = packet.srcsubnet,
devid = packet.srcdevice
})
for index, rec in ipairs(hdltoknx) do
if packet.srcsubnet == rec.hdl_net and packet.srcdevice ==
rec.hdl_dev and packet.opcode == rec.type and channel ==
rec.channel then
log('readstatusreply found', rec.knx_obj, value)
grp.write(rec.knx_obj, value, dt.uint16)
return
end
end
elseif packet.opcode == HDL.cmd.panelreadstatusreply then
values = {}
for i = 1, #packet.additional do
table.insert(values, packet.additional:byte(i))
end
channel = values[1]
value = values[2]
for index, rec in pairs(hdltoknx) do
if packet.srcsubnet == rec.hdl_net and packet.srcdevice ==
rec.hdl_dev and packet.opcode == rec.type and channel ==
rec.channel then
log('panelreadstatusreply found', rec.knx_obj, value)
alert('panelreadstatusreply to knx')
grp.write(rec.knx_obj, value, dt.uint16)
return
end
end
log('panelreadstatusreply', values, packet.srcsubnet, packet.srcdevice)
elseif packet.opcode == HDL.cmd.readfloorheatingstatusreply then
values = {}
for i = 1, #packet.additional do
table.insert(values, packet.additional:byte(i))
end
for index, rec in pairs(hdltoknx) do
if packet.srcsubnet == rec.hdl_net and packet.srcdevice ==
rec.hdl_dev and packet.opcode == rec.type then
log('readfloorheatingstatusreply found', rec.knx_obj, value)
-- alert('readfloorheatingstatusreply to kx')
grp.write(rec.knx_obj, values[rec.channel], dt.uint16)
return
end
end
log('readfloorheatingstatusreply', values, packet.srcsubnet,
packet.srcdevice)
end
end
Найдите строки программы в начале текста библиотеки, где соответствующим переменным присваиваются значения, и скорректируйте их:
HDL = {
-- destination ip
dstip = '192.168.1.7',
lcode = string.char(0xAA, 0xAA),
-- source device settings
srcsubnet = 1,
srcdevice = 254,
devicetype = 0xFFFE,
ШАГ 3 — Нужно добавить резидентную часть программы поддержки функции HDL
Добавьте два резидентных скрипта (проверьте) через меню Scripting -> Resident с любым именем, например «HDL to KNX» и «KNX to HDL», установите параметр Sleep interval = 0 и пока не активируйте скрипт. Как видно из названия, скрипты будут отвечать за одно из направлений связи, т.е. KNX -> HDL и HDL -> KNX. Скрипт «HDLtoKNX» отвечает за связь команд от конкретных устройств с групповыми адресами KNX.
hdltoknx[1] = {
hdl_net = 1, -- сеть
hdl_dev = 3, -- устройство
type = HDL.cmd.chanreg, -- команда
channel = 1, -- канал
knx_obj = '1/1/1' -- адрес KNX
}
Здесь мы задаём связь с устройством по адресу 1.3, его командой chanreg канала 1 с групповым адресом 1/1/1. Обратите внимание, что указывается индекс в таблице — [1], эти индексы должны обязательно идти подряд, иначе не все команды будут обрабатываться. Код резидентного скрипта HDLtoKNX ниже:
require('user.hdl')
hdltoknx = {}
hdltoknx[1] = {
hdl_net = 1,
hdl_dev = 3,
type = HDL.cmd.chanreg,
channel = 1,
knx_obj = '1/1/1'
}
hdltoknx[2] = {
hdl_net = 1,
hdl_dev = 5,
type = HDL.cmd.universalswitch,
channel = 14,
knx_obj = '1/1/2'
}
hdltoknx[3] = {
hdl_net = 1,
hdl_dev = 5,
type = HDL.cmd.readtemperaturereply,
channel = 1,
knx_obj = '1/1/3'
}
-- check for incoming data and update knx
function hdludpread()
local data, packet, address, chan, value, id, datatype, sendvalue
data = hdlclient:receive()
-- read timeout
if not data then return false end
-- log('пакет получен')
packet, err = HDL.decode(data)
if packet == nil then log('ошибка пакета', err) end
chan = packet.additional:byte(1)
value = packet.additional:byte(2)
-- logger = string.format('src: %d.%d.%d dest: %d.%d value: %d cmd: %02X', packet.srcsubnet, packet.srcdevice, chan, packet.dstsubnet, packet.dstdevice,value or 'not value', packet.opcode)
-- log('hdl read',logger)
HDL.parse(packet)
return true
end
-- hdl connection
if hdlclient == nil then
log('init HDL2KNX')
hdlclient = socket.udp()
hdlclient:settimeout(30)
hdlclient:setsockname('*', 6000)
HDL.init()
end
b = true
while b do
b = hdludpread()
os.sleep(0.05)
end
os.sleep(0.3)
-- log('конец скрипта HDL2KNX')
Структура скрипта KNXtoHDL схожа с HDLtoKNX. Мы задаём групповой адрес, значения которого будут отправляться определенной командой на заданный канал заданного устройства.
knxtohdl[1] = {
knx_obj = 'gost-1', -- knx адрес
hdl_net = 1, -- сеть
hdl_dev = 3, -- устройство
type = HDL.cmd.chanreg, -- команда
channel = 1 -- канал
}
require('user.hdl')
knxtohdl = {}
knxtohdl[1] = {
knx_obj = '2/2/2',
hdl_net = 1,
hdl_dev = 3,
type = HDL.cmd.chanreg,
channel = 1
}
knxtohdl[2] = {
knx_obj = '3/3/3',
hdl_net = 1,
hdl_dev = 5,
type = HDL.cmd.universalswitch,
channel = 1
}
knxtohdl[3] = {
knx_obj = '4/4/4',
hdl_net = 1,
hdl_dev = 5,
type = HDL.cmd.readtemperature,
channel = 1
}
function getknxupdate()
-- mydata will be set to a predefined value (e.g. 127) when data was not found
tm = storage.get('hdl_sync_time', 0)
cur_time = os.time()
storage.set('hdl_sync_time', cur_time)
for index, rec in pairs(knxtohdl) do
obj = grp.find(rec.knx_obj)
if obj == nil then log('object not found', rec.knx_obj) end
if obj and obj.updatetime >= tm then
value = nil
log(
'knx obj ' .. rec.knx_obj .. ' value: ' .. tostring(obj.value) ..
' last time: ' .. tm .. ' objtime: ' .. obj.updatetime ..
' curtime: ' .. cur_time)
if rec.type == HDL.cmd.chanreg then
if obj.datatype == dt.bool or obj.datatype == dt.boolean or
obj.datatype == dt.switch then
if obj.value then
value = 100
else
value = 0
end
else
value = knxdatatype.decode(obj.value, dt.scale)
end
log('chanreg', rec.hdl_net, rec.hdl_dev, rec.channel, value,
rec.delay or 0)
HDL.chanreg(rec.hdl_net, rec.hdl_dev, rec.channel, value,
rec.delay or 0)
return
elseif rec.type == HDL.cmd.scene then
value = knxdatatype.decode(obj.value, dt.scale)
log('scene', rec.hdl_net, rec.hdl_dev, rec.area, value)
HDL.scene(rec.hdl_net, rec.hdl_dev, rec.area, value)
return
elseif rec.type == HDL.cmd.sequence then
log('sequence', rec.hdl_net, rec.hdl_dev, rec.area)
HDL.sequence(rec.hdl_net, rec.hdl_dev, rec.area, 1)
elseif rec.type == HDL.cmd.readtemperature then
log('readtemperature', rec.hdl_net, rec.hdl_dev, rec.channel)
HDL.readtemperature(rec.hdl_net, rec.hdl_dev, rec.channel)
return
elseif rec.type == HDL.cmd.readtemperaturenew then
log('readtemperaturenew', rec.hdl_net, rec.hdl_dev, rec.channel)
HDL.readtemperaturenew(rec.hdl_net, rec.hdl_dev, rec.channel)
return
elseif rec.type == HDL.cmd.universalswitch then
if obj.value == true then
value = 255
else
value = 0
end
log('universalswitch', rec.hdl_net, rec.hdl_dev, rec.channel,
value, rec.delay)
HDL.universal_switch(rec.hdl_net, rec.hdl_dev, rec.channel,
value)
return
end
end
end
end
getknxupdate()
os.sleep(0.3)
-- log('конец скрипта KNX2HDL')
После того, как вы создадите и сконфигурируете оба скрипта, можете их активировать и вся связь будет работать в автоматическом режиме.