30.03.2021, 08:17
(29.03.2021, 20:05)Paddyb Wrote: Hi, just wondering if anyone ever found a solution for this? I have a need to control some somfy blinds on a new project... Or alternatively is there any kind of gateway software/hardware to convert somfy tahoma to Mqtt, making control trivial from LM...
EDIT: So far I have this solution, but it involves a few steps... It's actually just the blinds I need to control rather than Tahoma itself. If the blind motors are the newer Somfy IO control models, It seems like the KLF-200 Gateway mentioned elsewhere on this form (a velux product but is IO-homecontrol compatible, so will pair with the somfy motors also) might work in conjunction with this docker image that exposes it via MQTT..
It needs a bit more hardware than I'd like (to run the docker container...maybe a din rail mounted RPI...) but it might be easier than figuring out the KLF200s API, or converting the Home Assistant Python or Node JS examples to LUA... (which is beyond my coding abilities)
If anyone has a more elegant solution please shout!
Hi, I'm currently trying to work with KLF-200, but it's kinda early version. Currently it authorizes with KLF-200 and recognizes next confirmations and notifications: GW_PASSWORD_ENTER_CFM, GW_GET_STATE_CFM, GW_GET_ALL_NODES_INFORMATION_CFM, GW_GET_ALL_NODES_INFORMATION_CFM, GW_GET_ALL_NODES_INFORMATION_NTF, GW_GET_NODE_INFORMATION_NTF, GW_GET_ALL_NODES_INFORMATION_FINISHED_NTF and GW_COMMANDS_SEND_CFM. Also there's a function for GW_COMMAND_SEND_REQ which controls devices.
Code:
require('ssl')
function slip(str)
if type(str) == 'string' then
str_table = {}
str:gsub('.', function(c) table.insert(str_table, string.byte(c)) end)
else
str_table = str
end
if str_table[1] ~= 192 then -- to slip
temp_table = {192}
for _, v in ipairs(str_table) do
if v == 0xc0 then
table.insert(temp_table, 0xdb)
table.insert(temp_table, 0xdc)
elseif v == 0xdb then
table.insert(temp_table, 0xdb)
table.insert(temp_table, 0xdd)
else
table.insert(temp_table, v)
end
end
table.insert(temp_table, 192)
else -- from slip
temp_table = {}
for i, v in ipairs(str_table) do
if v == 0xdb then
if str_table[i+1] == 0xdc then
table.insert(temp_table, 0xc0)
elseif str_table[i+1] == 0xdd then
table.insert(temp_table, 0xdb)
end
elseif v ~= 0xc0 then
table.insert(temp_table, v)
end
end
end
return temp_table
end
function create_frame(cmd, data, data_len)
b1 = bit.band(cmd,0xff)
b2 = bit.band(bit.rshift(cmd,8),0xff)
frame = {}
if type(data) == 'number' then
data = '' .. string.char(data)
end
if type(data) == 'string' then
data:gsub('.', function(c) table.insert(frame,string.byte(c)) end) -- Data
end
if data_len then
for i = #frame, data_len-1 do
table.insert(frame, 0)
end
end
--log('added data', frame)
frame = {b2, b1, unpack(frame)} -- Cmd
--log('added cmd', frame)
frame = {#frame+1, unpack(frame)} -- Len
--log('added len', frame)
frame = {0, unpack(frame)} -- Protocol ID
table.insert(frame, bit.bxor(unpack(frame))) -- Checksum
frame = slip(frame)
--log('added SLIP', frame)
str = string.char(unpack(frame))
loghex(str)
return str
end
function parse_frame(frame)
if type(frame) == 'string' then
frame = slip(frame)
end
protocolId = frame[1]
lenFrame = frame[2]
data_offset = 4
cmd = bit.lshift(frame[3], 8) + frame[4]
if cmd == 0x3001 then
log('GW_PASSWORD_ENTER_CFM', frame[5])
elseif cmd == 0x000d then
log('GW_GET_STATE_CFM', frame[data_offset + 1], frame[data_offset + 1])
elseif cmd == 0x203 then
log('GW_GET_ALL_NODES_INFORMATION_CFM', frame[data_offset + 1], frame[data_offset + 2])
elseif cmd == 0x201 then
log('GW_GET_NODE_INFORMATION_CFM', frame[data_offset + 1], frame[data_offset + 2])
elseif cmd == 0x204 then
str = string.char(unpack(frame))
node_id = frame[data_offset + 1]
name = str:sub(data_offset + 5, data_offset + 68)
position = bit.lshift(frame[data_offset + 86], 8) + frame[data_offset + 87]
fp1 = bit.lshift(frame[data_offset + 90], 8) + frame[data_offset + 91]
fp2 = bit.lshift(frame[data_offset + 92], 8) + frame[data_offset + 93]
fp3 = bit.lshift(frame[data_offset + 94], 8) + frame[data_offset + 95]
fp4 = bit.lshift(frame[data_offset + 96], 8) + frame[data_offset + 97]
if position == 0xf7ff then
position = 'nan'
end
if fp1 == 0xf7ff then
fp1 = 'nan'
end
if fp2 == 0xf7ff then
fp2 = 'nan'
end
if fp3 == 0xf7ff then
fp3 = 'nan'
end
if fp4 == 0xf7ff then
fp4 = 'nan'
end
log('GW_GET_ALL_NODES_INFORMATION_NTF', node_id, position, fp1, fp2, fp3, fp4, name)
elseif cmd == 0x210 then
str = string.char(unpack(frame))
node_id = frame[data_offset + 1]
name = str:sub(data_offset + 5, data_offset + 68)
position = bit.lshift(frame[data_offset + 86], 8) + frame[data_offset + 87]
fp1 = bit.lshift(frame[data_offset + 90], 8) + frame[data_offset + 91]
fp2 = bit.lshift(frame[data_offset + 92], 8) + frame[data_offset + 93]
fp3 = bit.lshift(frame[data_offset + 94], 8) + frame[data_offset + 95]
fp4 = bit.lshift(frame[data_offset + 96], 8) + frame[data_offset + 97]
if position == 0xf7ff then
position = 'nan'
end
if fp1 == 0xf7ff then
fp1 = 'nan'
end
if fp2 == 0xf7ff then
fp2 = 'nan'
end
if fp3 == 0xf7ff then
fp3 = 'nan'
end
if fp4 == 0xf7ff then
fp4 = 'nan'
end
log('GW_GET_NODE_INFORMATION_NTF', node_id, position, fp1, fp2, fp3, fp4, name)
elseif cmd == 0x205 then
log('GW_GET_ALL_NODES_INFORMATION_FINISHED_NTF')
elseif cmd == 0x301 then
sessionId = bit.lshift(frame[data_offset + 1], 8) + frame[data_offset + 2]
status = frame[data_offset + 3]
log('GW_COMMANDS_SEND_CFM', sessionId, status)
else
log('Unknown command', string.format('%x', cmd))
loghex(string.char(unpack(frame)))
end
end
function split_frames(frame)
chars = {}
frame:gsub('.', function(c) table.insert(chars, string.byte(c)) end)
frames_table = {}
flag = true
start, stop = 0, 0
for i, v in ipairs(chars) do
if v == 192 then
if flag then
start = i
flag = not flag
else
stop = i
flag = not flag
end
end
if stop > start then
table.insert(frames_table, frame:sub(start, stop))
end
end
for i, v in ipairs(frames_table) do
parse_frame(v)
end
end
cert = '/home/ftp/velux-cert.pem'
if not sock then
ip = '192.168.0.112'
port = 51200
params = {
mode = 'client',
protocol = 'tlsv12',
certificate = cert
}
log('start')
sock = require('socket').tcp()
log('connect', sock:connect(ip, port))
sock:settimeout(1)
peer = ssl.wrap(sock, params)
log('handshake', peer:dohandshake())
peer:settimeout(1)
--log(peer:info())
--loghex(create_frame(0x3000, 'UR4nb2Apqh', 32))
peer:send(create_frame(0x3000, 'UR4nb2Apqh', 32)) -- password for WiFi
last_send = os.time()
end
function create_command(sessionid, user, priority, paramater, fpi1, fpi2, functionalArray, indexArrayCount, indexArray, priotiryLock, pl_0_3, pl_4_7, lockTime)
data = {}
b1 = bit.band(sessionid,0xff)
b2 = bit.band(bit.rshift(sessionid,8),0xff)
table.insert(data, b2)
table.insert(data, b1)
table.insert(data, user) -- default = 1
table.insert(data, priority) -- default = 3
table.insert(data, paramater) -- default = 0
table.insert(data, fpi1) -- default = 0
table.insert(data, fpi2) -- default = 0
b1 = bit.band(functionalArray,0xff)
b2 = bit.band(bit.rshift(functionalArray,8),0xff)
table.insert(data, b2)
table.insert(data, b1)
for i = 10, 41 do
table.insert(data, 0)
end
table.insert(data, indexArrayCount) -- default = 1
table.insert(data, indexArray) -- node id
for i = 44, 62 do
table.insert(data, 0)
end
table.insert(data, priotiryLock) -- default = 0
table.insert(data, pl_0_3) -- default = not used with priotiryLock = 0
table.insert(data, pl_4_7) -- default = not used with priotiryLock = 0
table.insert(data, lockTime) -- default = not used with priotiryLock = 0
return string.char(unpack(data))
end
a, b, c, d = peer:receive('*a')
--log(a, b, c, d)
if #c > 0 then
loghex(c)
split_frames(c)
end
--[[
0x0200 GW_GET_NODE_INFORMATION_REQ Request extended information of one specific actuator node.
0x0201 GW_GET_NODE_INFORMATION_CFM Acknowledge to GW_GET_NODE_INFORMATION_REQ.
0x0210 GW_GET_NODE_INFORMATION_NTF Acknowledge to GW_GET_NODE_INFORMATION_REQ.
0x0202 GW_GET_ALL_NODES_INFORMATION_REQ Request extended information of all nodes.
0x0203 GW_GET_ALL_NODES_INFORMATION_CFM Acknowledge to GW_GET_ALL_NODES_INFORMATION_REQ
0x0204 GW_GET_ALL_NODES_INFORMATION_NTF Acknowledge to GW_GET_ALL_NODES_INFORMATION_REQ. Holds node information
0x0205 GW_GET_ALL_NODES_INFORMATION_FINISHED_NTF Acknowledge to GW_GET_ALL_NODES_INFORMATION_REQ. No more nodes.
0x0206 GW_SET_NODE_VARIATION_REQ Set node variation.
0x0207 GW_SET_NODE_VARIATION_CFM Acknowledge to GW_SET_NODE_VARIATION_REQ.
0x0208 GW_SET_NODE_NAME_REQ Set node name.
0x0209 GW_SET_NODE_NAME_CFM Acknowledge to GW_SET_NODE_NAME_REQ.
0x020C GW_NODE_INFORMATION_CHANGED_NTF Information has been updated.
0x0211 GW_NODE_STATE_POSITION_CHANGED_NTF Information has been updated.
0x020D GW_SET_NODE_ORDER_AND_PLACEMENT_REQ Set search order and room placement.
0x020E GW_SET_NODE_ORDER_AND_PLACEMENT_CFM Acknowledge to GW_SET_NODE_ORDER_AND_PLACEMENT_REQ.
0x0300 GW_COMMAND_SEND_REQ Send activating command direct to one or more io-homecontrol nodes.
0x0301 GW_COMMAND_SEND_CFM Acknowledge to GW_COMMAND_SEND_REQ.
0x0302 GW_COMMAND_RUN_STATUS_NTF Gives run status for io-homecontrol node.
0x0303 GW_COMMAND_REMAINING_TIME_NTF Gives remaining time before io-homecontrol node enter target position.
0x0304 GW_SESSION_FINISHED_NTF Command send, Status request, Wink, Mode or Stop session is finished.
0x0305 GW_STATUS_REQUEST_REQ Get status request from one or more io-homecontrol nodes.
0x0306 GW_STATUS_REQUEST_CFM Acknowledge to GW_STATUS_REQUEST_REQ.
0x0307 GW_STATUS_REQUEST_NTF Acknowledge to GW_STATUS_REQUEST_REQ. Status request from one or more io-homecontrol nodes.
0x0308 GW_WINK_SEND_REQ Request from one or more io-homecontrol nodes to Wink.
0x0309 GW_WINK_SEND_CFM Acknowledge to GW_WINK_SEND_REQ
0x030A GW_WINK_SEND_NTF Status info for performed wink request.
--]]
status = grp.getvalue('32/1/7')
if status and os.time() % 10 == 0 then
log('cmd')
raw_cmd = create_command(1, 1, 3, 0, 0, 0, 0xc900 + 1000 + 100 * 10, 1, 0, 0, 0, 0, 0)
-- absolute = % * 512
-- relative = 0xC900 + 1000 + +-% * 10
-- stop = 0xD200
peer:send(create_frame(0x300, raw_cmd)) -- GW_COMMANDS_SEND_REQ
last_send = os.time()
grp.write('32/1/7', false)
end
if os.time() % 10 == 0 then
--peer:send(create_frame(0x0200, 0)) -- GW_GET_ALL_NODES_INFORMATION_REQ
--last_send = os.time()
end
if os.time() - last_send > 14 * 60 then -- ping for keeping connection live
peer:send( create_frame(0x000c) )
last_time = os.time()
end