This forum uses cookies
This forum makes use of cookies to store your login information if you are registered, and your last visit if you are not. Cookies are small text documents stored on your computer; the cookies set by this forum can only be used on this website and pose no security risk. Cookies on this forum also track the specific topics you have read and when you last read them. Please confirm that you accept these cookies being set.

Somfy Gateway
#1
Hi,

Have anybody made an gateway to Somfy Tahoma ? im looking into how to control some indoor sunscreens.
Whats the best solution, API or?

BR Even Sundgot.
Reply
#2
+1

Intrested too for help integrating Somfy Open API with LM.

https://developer.somfy.com/apis-docs

PS: Can provide credentials if needed

BR
Geroege
Reply
#3
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!
Reply
#4
(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
Reply
#5
Wow! nice, I will take some time and try to understand your code, and will read it in conjunction with the official API documentation... At first glance, I understand more of what your code is doing rather than understanding the instructions in the Velux docs!... Your code is probably my rosetta stone! Thanks for posting, really appreciate it
Reply
#6
(30.03.2021, 11:30)Paddyb Wrote: Wow! nice, I will take some time and try to understand your code, and will read it in conjunction with the official API documentation... At first glance, I understand more of what your code is doing rather than understanding the instructions in the Velux docs!... Your code is probably my rosetta stone! Thanks for posting, really appreciate it

Don't forget to upload velux-cert to LM with FTP. You can take it from Node JS github or get it by yourself with instruction.
Reply
#7
(30.03.2021, 11:30)Paddyb Wrote: Wow! nice, I will take some time and try to understand your code, and will read it in conjunction with the official API documentation... At first glance, I understand more of what your code is doing rather than understanding the instructions in the Velux docs!... Your code is probably my rosetta stone! Thanks for posting, really appreciate it

I added some commands, getting statuses and moved functions to a library. Now it uses storage to save statuses, nodes and queue of commands. There's examples for event scripts but some comments in Russian, sorry =)

Attached Files
.lua   event example bit.lua (Size: 474 bytes / Downloads: 26)
.lua   event example byte.lua (Size: 145 bytes / Downloads: 18)
.lua   resident handler.lua (Size: 1.55 KB / Downloads: 23)
.lua   user.velux.lua (Size: 10.49 KB / Downloads: 23)
Reply
#8
(01.04.2021, 13:29)Snoolik Wrote:
(30.03.2021, 11:30)Paddyb Wrote: Wow! nice, I will take some time and try to understand your code, and will read it in conjunction with the official API documentation... At first glance, I understand more of what your code is doing rather than understanding the instructions in the Velux docs!... Your code is probably my rosetta stone! Thanks for posting, really appreciate it

I added some commands, getting statuses and moved functions to a library. Now it uses storage to save statuses, nodes and queue of commands. There's examples for event scripts but some comments in Russian, sorry =)

Great work @Snoolik! I have tried to get it working now. Seems its working 99%... I get Unknown command 211.. In the docs I cannot find 211 0xD3 or something siliar. It's not in the list in user.velux.lua either.. Any idea what this is? This command is from Velux to client right? I'll try to plough trough the python version from Velux to see if I can sort it out.

I'm running firmware 0.2.0.0.71.0.

Update:

Seems I was a little bit hasty.. its 0x211 and its GW_NODE_STATE_POSITION_CHANGED_NTF. Handler is not implemented. I will look into it.

best regards
Thomas

Attached Files Thumbnail(s)
   
Reply


Forum Jump: