Example: Casambi integration with LogicMacine

Task

This example will do Casambi device integration into LogicMachine5 over TCP/IP and add a possibility to interact with Casambi devices from KNX, Modbus, Bacnet and other protocols supported in LM5.

Step by step guide

1. Create a user library named websocket and put user.websocket.lua file contents into it.

Source code    
  1. local bit = require('bit')
  2. local ssl = require('ssl')
  3. local socket = require('socket')
  4. local encdec = require('encdec')
  5. local parse_url = require('socket.url').parse
  6.  
  7. local bxor = bit.bxor
  8. local bor = bit.bor
  9. local band = bit.band
  10. local lshift = bit.lshift
  11. local rshift = bit.rshift
  12. local ssub = string.sub
  13. local sbyte = string.byte
  14. local schar = string.char
  15. local tinsert = table.insert
  16. local tconcat = table.concat
  17. local mmin = math.min
  18. local mfloor = math.floor
  19. local mrandom = math.random
  20. local base64enc = encdec.base64enc
  21. local sha1 = encdec.sha1
  22. local unpack = unpack
  23. local CONTINUATION = 0
  24. local TEXT = 1
  25. local BINARY = 2
  26. local CLOSE = 8
  27. local PING = 9
  28. local PONG = 10
  29. local guid = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
  30.  
  31. local read_n_bytes = function(str, pos, n)
  32. pos = pos or 1
  33. return pos+n, string.byte(str, pos, pos + n - 1)
  34. end
  35.  
  36. local read_int8 = function(str, pos)
  37. return read_n_bytes(str, pos, 1)
  38. end
  39.  
  40. local read_int16 = function(str, pos)
  41. local new_pos,a,b = read_n_bytes(str, pos, 2)
  42. return new_pos, lshift(a, 8) + b
  43. end
  44.  
  45. local read_int32 = function(str, pos)
  46. local new_pos,a,b,c,d = read_n_bytes(str, pos, 4)
  47. return new_pos,
  48. lshift(a, 24) +
  49. lshift(b, 16) +
  50. lshift(c, 8 ) +
  51. d
  52. end
  53.  
  54. local write_int8 = schar
  55.  
  56. local write_int16 = function(v)
  57. return schar(rshift(v, 8), band(v, 0xFF))
  58. end
  59.  
  60. local write_int32 = function(v)
  61. return schar(
  62. band(rshift(v, 24), 0xFF),
  63. band(rshift(v, 16), 0xFF),
  64. band(rshift(v, 8), 0xFF),
  65. band(v, 0xFF)
  66. )
  67. end
  68.  
  69. local generate_key = function()
  70. math.randomseed(os.time())
  71.  
  72. local r1 = mrandom(0,0xfffffff)
  73. local r2 = mrandom(0,0xfffffff)
  74. local r3 = mrandom(0,0xfffffff)
  75. local r4 = mrandom(0,0xfffffff)
  76. local key = write_int32(r1)..write_int32(r2)..write_int32(r3)..write_int32(r4)
  77. return base64enc(key)
  78. end
  79.  
  80. local bits = function(...)
  81. local n = 0
  82. for _,bitn in pairs{...} do
  83. n = n + 2^bitn
  84. end
  85. return n
  86. end
  87.  
  88. local bit_7 = bits(7)
  89. local bit_0_3 = bits(0,1,2,3)
  90. local bit_0_6 = bits(0,1,2,3,4,5,6)
  91.  
  92. -- TODO: improve performance
  93. local xor_mask = function(encoded,mask,payload)
  94. local transformed,transformed_arr = {},{}
  95. -- xor chunk-wise to prevent stack overflow.
  96. -- sbyte and schar multiple in/out values
  97. -- which require stack
  98. for p=1,payload,2000 do
  99. local last = mmin(p+1999,payload)
  100. local original = {sbyte(encoded,p,last)}
  101. for i=1,#original do
  102. local j = (i-1) % 4 + 1
  103. transformed[i] = bxor(original[i],mask[j])
  104. end
  105. local xored = schar(unpack(transformed,1,#original))
  106. tinsert(transformed_arr,xored)
  107. end
  108. return tconcat(transformed_arr)
  109. end
  110.  
  111. local encode_header_small = function(header, payload)
  112. return schar(header, payload)
  113. end
  114.  
  115. local encode_header_medium = function(header, payload, len)
  116. return schar(header, payload, band(rshift(len, 8), 0xFF), band(len, 0xFF))
  117. end
  118.  
  119. local encode_header_big = function(header, payload, high, low)
  120. return schar(header, payload)..write_int32(high)..write_int32(low)
  121. end
  122.  
  123. local encode = function(data,opcode,masked,fin)
  124. local header = opcode or 1-- TEXT is default opcode
  125. if fin == nil or fin == true then
  126. header = bor(header,bit_7)
  127. end
  128. local payload = 0
  129. if masked then
  130. payload = bor(payload,bit_7)
  131. end
  132. local len = #data
  133. local chunks = {}
  134. if len < 126 then
  135. payload = bor(payload,len)
  136. tinsert(chunks,encode_header_small(header,payload))
  137. elseif len 0
  138. local opcode = band(header,bit_0_3)
  139. local mask = band(payload,bit_7) > 0
  140. payload = band(payload,bit_0_6)
  141. if payload > 125 then
  142. if payload == 126 then
  143. if #encoded < 2 then
  144. return nil,2-#encoded
  145. end
  146. pos,payload = read_int16(encoded,1)
  147. elseif payload == 127 then
  148. if #encoded < 8 then
  149. return nil,8-#encoded
  150. end
  151. pos,high = read_int32(encoded,1)
  152. pos,low = read_int32(encoded,pos)
  153. payload = high*2^32 + low
  154. if payload < 0xffff or payload > 2^53 then
  155. assert(false,'INVALID PAYLOAD '..payload)
  156. end
  157. else
  158. assert(false,'INVALID PAYLOAD '..payload)
  159. end
  160. encoded = ssub(encoded,pos)
  161. bytes = bytes + pos - 1
  162. end
  163. local decoded
  164. if mask then
  165. local bytes_short = payload + 4 - #encoded
  166. if bytes_short > 0 then
  167. return nil,bytes_short
  168. end
  169. local m1,m2,m3,m4
  170. pos,m1 = read_int8(encoded,1)
  171. pos,m2 = read_int8(encoded,pos)
  172. pos,m3 = read_int8(encoded,pos)
  173. pos,m4 = read_int8(encoded,pos)
  174. encoded = ssub(encoded,pos)
  175. local mask = {
  176. m1,m2,m3,m4
  177. }
  178. decoded = xor_mask(encoded,mask,payload)
  179. bytes = bytes + 4 + payload
  180. else
  181. local bytes_short = payload - #encoded
  182. if bytes_short > 0 then
  183. return nil,bytes_short
  184. end
  185. if #encoded > payload then
  186. decoded = ssub(encoded,1,payload)
  187. else
  188. decoded = encoded
  189. end
  190. bytes = bytes + payload
  191. end
  192. return decoded,fin,opcode,encoded_bak:sub(bytes+1),mask
  193. end
  194.  
  195. local encode_close = function(code,reason)
  196. if code then
  197. local data = write_int16(code)
  198. if reason then
  199. data = data..tostring(reason)
  200. end
  201. return data
  202. end
  203. return ''
  204. end
  205.  
  206. local decode_close = function(data)
  207. local _,code,reason
  208. if data then
  209. if #data > 1 then
  210. _,code = read_int16(data,1)
  211. end
  212. if #data > 2 then
  213. reason = data:sub(3)
  214. end
  215. end
  216. return code,reason
  217. end
  218.  
  219. local sec_websocket_accept = function(sec_websocket_key)
  220. local enc = sha1(sec_websocket_key..guid, true)
  221. return base64enc(enc)
  222. end
  223.  
  224. local http_headers = function(request)
  225. local headers = {}
  226. if not request:match('.*HTTP/1%.1') then
  227. return headers
  228. end
  229. request = request:match('[^\r\n]+\r\n(.*)')
  230. for line in request:gmatch('[^\r\n]*\r\n') do
  231. local name,val = line:match('([^%s]+)%s*:%s*([^\r\n]+)')
  232. if name and val then
  233. name = name:lower()
  234. if not name:match('sec%-websocket') then
  235. val = val:lower()
  236. end
  237. if not headers[name] then
  238. headers[name] = val
  239. else
  240. headers[name] = headers[name]..','..val
  241. end
  242. elseif line ~= '\r\n' then
  243. assert(false,line..'('..#line..')')
  244. end
  245. end
  246. return headers,request:match('\r\n\r\n(.*)')
  247. end
  248.  
  249. local upgrade_request = function(req, key, protocol)
  250. local format = string.format
  251. local lines = {
  252. format('GET %s HTTP/1.1',req.path or ''),
  253. format('Host: %s',req.host),
  254. 'Upgrade: websocket',
  255. 'Connection: Upgrade',
  256. format('Sec-WebSocket-Key: %s',key),
  257. 'Sec-WebSocket-Version: 13',
  258. }
  259. if protocol then
  260. tinsert(lines, format('Sec-WebSocket-Protocol: %s', protocol))
  261. end
  262. if req.port and req.port ~= 80 then
  263. lines[2] = format('Host: %s:%d',req.host,req.port)
  264. end
  265. if req.userinfo then
  266. local auth = format('Authorization: Basic %s', base64enc(req.userinfo))
  267. tinsert(lines, auth)
  268. end
  269. tinsert(lines,'\r\n')
  270. return tconcat(lines,'\r\n')
  271. end
  272.  
  273. local receive = function(self)
  274. if self.state ~= 'OPEN' and not self.is_closing then
  275. return nil,nil,false,1006,'wrong state'
  276. end
  277. local first_opcode
  278. local frames
  279. local bytes = 3
  280. local encoded = ''
  281. local clean = function(was_clean,code,reason)
  282. self.state = 'CLOSED'
  283. self:sock_close()
  284. if self.on_close then
  285. self:on_close()
  286. end
  287. return nil,nil,was_clean,code,reason or 'closed'
  288. end
  289. while true do
  290. local chunk,err = self:sock_receive(bytes)
  291. if err then
  292. if err == 'timeout' then
  293. return nil,nil,false,1006,err
  294. else
  295. return clean(false,1006,err)
  296. end
  297. end
  298. encoded = encoded..chunk
  299. local decoded,fin,opcode,_,masked = decode(encoded)
  300. if masked then
  301. return clean(false,1006,'Websocket receive failed: frame was not masked')
  302. end
  303. if decoded then
  304. if opcode == CLOSE then
  305. if not self.is_closing then
  306. local code,reason = decode_close(decoded)
  307. -- echo code
  308. local msg = encode_close(code)
  309. local encoded = encode(msg,CLOSE,true)
  310. local n,err = self:sock_send(encoded)
  311. if n == #encoded then
  312. return clean(true,code,reason)
  313. else
  314. return clean(false,code,err)
  315. end
  316. else
  317. return decoded,opcode
  318. end
  319. end
  320. if not first_opcode then
  321. first_opcode = opcode
  322. end
  323. if not fin then
  324. if not frames then
  325. frames = {}
  326. elseif opcode ~= CONTINUATION then
  327. return clean(false,1002,'protocol error')
  328. end
  329. bytes = 3
  330. encoded = ''
  331. tinsert(frames,decoded)
  332. elseif not frames then
  333. return decoded,first_opcode
  334. else
  335. tinsert(frames,decoded)
  336. return tconcat(frames),first_opcode
  337. end
  338. else
  339. assert(type(fin) == 'number' and fin > 0)
  340. bytes = fin
  341. end
  342. end
  343. end
  344.  
  345. local send = function(self,data,opcode)
  346. if self.state ~= 'OPEN' then
  347. return nil,false,1006,'wrong state'
  348. end
  349. local encoded = encode(data,opcode or TEXT,true)
  350. local n,err = self:sock_send(encoded)
  351. if n ~= #encoded then
  352. return nil,self:close(1006,err)
  353. end
  354. return true
  355. end
  356.  
  357. local close = function(self,code,reason)
  358. if self.state ~= 'OPEN' then
  359. return false,1006,'wrong state'
  360. end
  361. if self.state == 'CLOSED' then
  362. return false,1006,'wrong state'
  363. end
  364. local msg = encode_close(code or 1000,reason)
  365. local encoded = encode(msg,CLOSE,true)
  366. local n,err = self:sock_send(encoded)
  367. local was_clean = false
  368.  
  369. code = 1005
  370. reason = ''
  371.  
  372. if n == #encoded then
  373. self.is_closing = true
  374. local rmsg,opcode = self:receive()
  375. if rmsg and opcode == CLOSE then
  376. code,reason = decode_close(rmsg)
  377. was_clean = true
  378. end
  379. else
  380. reason = err
  381. end
  382. self:sock_close()
  383. if self.on_close then
  384. self:on_close()
  385. end
  386. self.state = 'CLOSED'
  387. return was_clean,code,reason or ''
  388. end
  389.  
  390. local DEFAULT_PORTS = {ws = 80, wss = 443}
  391.  
  392. local connect = function(self,ws_url,ssl_params)
  393. if self.state ~= 'CLOSED' then
  394. return nil,'wrong state',nil
  395. end
  396. local parsed = parse_url(ws_url)
  397.  
  398. if parsed.scheme ~= 'wss' and parsed.scheme ~= 'ws' then
  399. return nil, 'bad protocol'
  400. end
  401.  
  402. if not parsed.port then
  403. parsed.port = DEFAULT_PORTS[ parsed.scheme ]
  404. end
  405.  
  406. -- Preconnect (for SSL if needed)
  407. local _,err = self:sock_connect(parsed.host, parsed.port)
  408. if err then
  409. return nil,err,nil
  410. end
  411.  
  412. if parsed.scheme == 'wss' then
  413. if type(ssl_params) ~= 'table' then
  414. ssl_params = {
  415. protocol = 'tlsv1',
  416. options = {'all', 'no_sslv2', 'no_sslv3'},
  417. verify = 'none',
  418. }
  419. end
  420. ssl_params.mode = 'client'
  421.  
  422. self.sock = ssl.wrap(self.sock, ssl_params)
  423. self.sock:dohandshake()
  424. elseif parsed.scheme ~= 'ws' then
  425. return nil, 'bad protocol'
  426. end
  427. local key = generate_key()
  428. local req = upgrade_request(parsed, key, self.protocol)
  429. local n,err = self:sock_send(req)
  430. if n ~= #req then
  431. return nil,err,nil
  432. end
  433. local resp = {}
  434. repeat
  435. local line,err = self:sock_receive('*l')
  436. resp[#resp+1] = line
  437. if err then
  438. return nil,err,nil
  439. end
  440. until line == ''
  441. local response = tconcat(resp,'\r\n')
  442. local headers = http_headers(response)
  443. local expected_accept = sec_websocket_accept(key)
  444. if headers['sec-websocket-accept'] ~= expected_accept then
  445. local msg = 'Websocket Handshake failed: Invalid Sec-Websocket-Accept (expected %s got %s)'
  446. return nil,msg:format(expected_accept,headers['sec-websocket-accept'] or 'nil'),headers
  447. end
  448. self.state = 'OPEN'
  449. return true,headers['sec-websocket-protocol'],headers
  450. end
  451.  
  452. local extend = function(obj)
  453. obj.state = 'CLOSED'
  454. obj.receive = receive
  455. obj.send = send
  456. obj.close = close
  457. obj.connect = connect
  458.  
  459. return obj
  460. end
  461.  
  462. local client_copas = function(timeout)
  463. local copas = require('copas')
  464. local self = {}
  465.  
  466. self.sock_connect = function(self,host,port)
  467. self.sock = socket.tcp()
  468. self.sock:settimeout(timeout or 5)
  469. local _,err = copas.connect(self.sock,host,port)
  470. if err and err ~= 'already connected' then
  471. self.sock:close()
  472. return nil,err
  473. end
  474. end
  475.  
  476. self.sock_send = function(self,...)
  477. return copas.send(self.sock,...)
  478. end
  479.  
  480. self.sock_receive = function(self,...)
  481. return copas.receive(self.sock,...)
  482. end
  483.  
  484. self.sock_close = function(self)
  485. self.sock:close()
  486. end
  487.  
  488. self = extend(self)
  489. return self
  490. end
  491.  
  492. local client_sync = function(timeout)
  493. local self = {}
  494.  
  495. self.sock_connect = function(self,host,port)
  496. self.sock = socket.tcp()
  497. self.sock:settimeout(timeout or 5)
  498. local _,err = self.sock:connect(host,port)
  499. if err then
  500. self.sock:close()
  501. return nil,err
  502. end
  503. end
  504.  
  505. self.sock_send = function(self,...)
  506. return self.sock:send(...)
  507. end
  508.  
  509. self.sock_receive = function(self,...)
  510. return self.sock:receive(...)
  511. end
  512.  
  513. self.sock_close = function(self)
  514. self.sock:close()
  515. end
  516.  
  517. self = extend(self)
  518. return self
  519. end
  520.  
  521. local client = function(mode, timeout)
  522. if mode == 'copas' then
  523. return client_copas(timeout)
  524. else
  525. return client_sync(timeout)
  526. end
  527. end
  528.  
  529. return {
  530. client = client,
  531. CONTINUATION = CONTINUATION,
  532. TEXT = TEXT,
  533. BINARY = BINARY,
  534. CLOSE = CLOSE,
  535. PING = PING,
  536. PONG = PONG
  537. }

2. Create a user library named casambi and put user.casambi.lua file contents into it.

Source code    
  1. local _M = {}
  2.  
  3. local ws = require('user.websocket')
  4. local json = require('json')
  5. local socket = require('socket')
  6. local http = require('socket.http')
  7. local wire = os.time()
  8. local wsconn, credentials, closed
  9. local lb, lbfd, timer, tmfd, wsfd
  10. local mapping = {}
  11. local values = {}
  12.  
  13. http.TIMEOUT = 5
  14.  
  15. local function enckey(key, value)
  16. return {
  17. [ key ] = { value = value }
  18. }
  19. end
  20.  
  21. local encoders = {
  22. onoff = function(value)
  23. return enckey('Dimmer', value)
  24. end,
  25. dimmer = function(value)
  26. return {
  27. Dimmer = { value = value / 255 }
  28. }
  29. end,
  30. color = function(value)
  31. local rgb = string.format('rgb(%d, %d, %d)',
  32. bit.band(bit.rshift(value, 16), 0xFF),
  33. bit.band(bit.rshift(value, 8), 0xFF),
  34. bit.band(value, 0xFF)
  35. )
  36.  
  37. return {
  38. RGB = { rgb = rgb },
  39. Colorsource = { source = 'RGB' },
  40. }
  41. end,
  42. cct = function(value)
  43. return {
  44. ColorTemperature = { value = value },
  45. Colorsource = { source = 'TW' },
  46. }
  47. end,
  48. casarolloup = function(value)
  49. return enckey('Hoch', value)
  50. end,
  51. casarollodown = function(value)
  52. return enckey('Runter', value)
  53. end,
  54. ligacurtainup = function(value)
  55. return enckey('UP', value)
  56. end,
  57. ligacurtaindown = function(value)
  58. return enckey('DOWN', value)
  59. end,
  60. ligacurtainmaxup = function(value)
  61. return enckey('MAX UP', value)
  62. end,
  63. ligacurtainmaxdown = function(value)
  64. return enckey('MAX DOWN', value)
  65. end,
  66. }
  67.  
  68. local decoders = {
  69. onoff = function(control)
  70. return control.value > 0, dt.bool
  71. end,
  72. dimmer = function(control)
  73. return math.round(control.value * 255), dt.uint8
  74. end,
  75. color = function(control)
  76. local r, g, b = control.rgb:match('(%d+),%s*(%d+),%s*(%d+)')
  77. local v = tonumber(r or 0) * 0x10000 + tonumber(g or 0) * 0x100 + tonumber(b or 0)
  78. return v, dt.rgb
  79. end,
  80. cct = function(control)
  81. return control.value, dt.uint16
  82. end
  83. }
  84.  
  85. local function eventcb(event)
  86. if not wsconn then
  87. return
  88. elseif event.sender == grp.sender then
  89. return
  90. end
  91.  
  92. local props = mapping.control[ event.dst ]
  93. if not props then
  94. return
  95. end
  96.  
  97. local id, method
  98.  
  99. if props.sceneid then
  100. id = props.sceneid
  101. method = 'controlScene'
  102. elseif props.groupid then
  103. id = props.groupid
  104. method = 'controlGroup'
  105. else
  106. id = props.id
  107. method = 'controlUnit'
  108. end
  109.  
  110. local value = tonumber(event.datahex, 16) or 0
  111. local message = {
  112. id = id,
  113. wire = wire,
  114. method = method,
  115. }
  116.  
  117. if props.sceneid or props.groupid then
  118. message.level = props.type == 'onoff' and value or (value / 255)
  119. else
  120. message.targetControls = encoders[ props.type ](value)
  121. end
  122.  
  123. local payload = json.encode(message)
  124.  
  125. -- log('tx', payload)
  126.  
  127. wsconn:send(payload)
  128. end
  129.  
  130. local function request(endpoint, payload)
  131. local method, body
  132.  
  133. local headers = {}
  134. headers['X-Casambi-Key'] = credentials.api_key
  135.  
  136. if credentials.session_id then
  137. headers['X-Casambi-Session'] = credentials.session_id
  138. end
  139.  
  140. if payload then
  141. method = 'POST'
  142.  
  143. headers['Content-Type'] = 'application/json'
  144. headers['Content-Length'] = #payload
  145.  
  146. body = json.encode(payload)
  147. else
  148. method = 'GET'
  149. end
  150.  
  151. local res, code = http.request({
  152. url = 'https://door.casambi.com/' .. endpoint,
  153. method = method,
  154. headers = headers,
  155. body = body,
  156. })
  157.  
  158. if res and code == 200 then
  159. res = json.pdecode(res)
  160. end
  161.  
  162. return res, code
  163. end
  164.  
  165. local function getusersession()
  166. local payload = {
  167. email = credentials.email,
  168. password = credentials.user_password
  169. }
  170.  
  171. return request('v1/users/session/', payload)
  172. end
  173.  
  174. local function getnetworksession()
  175. local payload = {
  176. email = credentials.email,
  177. password = credentials.network_password
  178. }
  179.  
  180. return request('v1/networks/session/', payload)
  181. end
  182.  
  183. _M.getnetworkunits = function()
  184. local endpoint = 'v1/networks/' .. credentials.network_id .. '/units'
  185. return request(endpoint)
  186. end
  187.  
  188. local ping = json.encode({
  189. method = 'ping',
  190. wire = wire
  191. })
  192.  
  193. local function wscontrol(prefix, control)
  194. local ctype = control.type:lower()
  195. local key = prefix .. '_' .. ctype
  196. local addr = mapping.status[ key ]
  197.  
  198. if addr then
  199. local value, dpt = decoders[ ctype ](control)
  200.  
  201. if values[ addr ] ~= value then
  202. grp.write(addr, value, dpt)
  203. values[ addr ] = value
  204. end
  205. end
  206. end
  207.  
  208. local function wsparse(data)
  209. data = json.pdecode(data)
  210.  
  211. if type(data) ~= 'table' then
  212. return
  213. end
  214.  
  215. local status = data.wireStatus
  216. if status then
  217. if status ~= 'openWireSucceed' then
  218. log('casambi: websocket error: ' .. tostring(data.message or status))
  219. wsconn:close()
  220. closed = true
  221. end
  222.  
  223. return
  224. end
  225.  
  226. if data.method ~= 'unitChanged' then
  227. return
  228. end
  229.  
  230. local prefix
  231.  
  232. if type(data.groupId) == 'number' and data.groupId > 0 then
  233. prefix = 'group_' .. data.groupId
  234. else
  235. prefix = 'id_' .. data.id
  236. end
  237.  
  238. for _, control in ipairs(data.controls) do
  239. wscontrol(prefix, control)
  240.  
  241. if control.type == 'Dimmer' then
  242. control.type = 'onoff'
  243. wscontrol(prefix, control)
  244. end
  245. end
  246. end
  247.  
  248. local function wsconnect()
  249. wsconn = ws.client('sync', 10)
  250. wsconn.protocol = credentials.api_key
  251.  
  252. local res, err = wsconn:connect('wss://door.casambi.com/v1/bridge/')
  253. if not res then
  254. log('casambi: websocket connection failed, error: ' .. tostring(err))
  255. return nil, err
  256. end
  257.  
  258. wsfd = socket.fdmaskset(wsconn.sock:getfd(), 'r')
  259.  
  260. wsconn.on_close = function()
  261. log('casambi: websocket connection closed')
  262. closed = true
  263. end
  264.  
  265. local open = json.encode({
  266. method = 'open',
  267. id = credentials.network_id,
  268. session = credentials.session_id,
  269. wire = wire,
  270. type = 1 -- fixed client type
  271. })
  272.  
  273. wsconn:send(open)
  274.  
  275. return true
  276. end
  277.  
  278. _M.setcredentials = function(cred)
  279. credentials = cred
  280. end
  281.  
  282. _M.setmapping = function(mode, map)
  283. if mode == 'control' then
  284. mapping.control = map
  285. elseif mode == 'status' then
  286. mapping.status = {}
  287.  
  288. for addr, props in pairs(map) do
  289. local key
  290.  
  291. if props.groupid then
  292. key = 'group_' .. props.groupid
  293. else
  294. key = 'id_' .. props.id
  295. end
  296.  
  297. key = key .. '_' .. props.type
  298.  
  299. mapping.status[ key ] = addr
  300. end
  301. end
  302. end
  303.  
  304. _M.init = function()
  305. lb = require('localbus').new()
  306. lb:sethandler('groupwrite', eventcb)
  307. lb:sethandler('groupresponse', eventcb)
  308.  
  309. lbfd = socket.fdmaskset(lb:getfd(), 'r')
  310.  
  311. timer = require('timerfd').new(60)
  312. tmfd = socket.fdmaskset(timer:getfd(), 'r')
  313. end
  314.  
  315. local function initsession()
  316. local res, err
  317.  
  318. res, err = getusersession()
  319. if type(res) ~= 'table' then
  320. return nil, err, 'user'
  321. end
  322.  
  323. credentials.session_id = res.sessionId
  324.  
  325. res, err = getnetworksession()
  326. if type(res) ~= 'table' then
  327. return nil, err, 'network'
  328. end
  329.  
  330. credentials.network_id = next(res)
  331.  
  332. return true
  333. end
  334.  
  335. _M.initsession = initsession
  336.  
  337. local function connect()
  338. local res, err, state = initsession()
  339.  
  340. if not res then
  341. log('casambi: get ' .. state .. ' session failed, error: ' .. tostring(err))
  342. return
  343. end
  344.  
  345. return wsconnect()
  346. end
  347.  
  348. local function loop()
  349. local res, lbstat, tmstat, wsstat
  350.  
  351. if wsconn then
  352. res, lbstat, tmstat, wsstat = socket.selectfds(120, lbfd, tmfd, wsfd)
  353. else
  354. res, lbstat, tmstat = socket.selectfds(120, lbfd, tmfd)
  355. end
  356.  
  357. if not res then
  358. wsconn:close()
  359. closed = true
  360. return
  361. end
  362.  
  363. if lbstat then
  364. lb:step()
  365. end
  366.  
  367. if tmstat then
  368. timer:read()
  369.  
  370. if wsconn then
  371. wsconn:send(ping)
  372. end
  373. end
  374.  
  375. if wsstat then
  376. local data = wsconn:receive()
  377.  
  378. -- log('rx', data)
  379.  
  380. if data then
  381. wsparse(data)
  382. end
  383. end
  384. end
  385.  
  386. _M.run = function()
  387. if closed then
  388. wsconn = nil
  389. closed = nil
  390. end
  391.  
  392. if wsconn then
  393. loop()
  394. elseif not connect() then
  395. os.sleep(10)
  396. end
  397. end
  398.  
  399. return _M

3. Create a resident script with 0 seconds sleep time (use resident.lua as a template):

Source code    
  1. if not casambi then
  2. casambi = require('user.casambi')
  3.  
  4. local control = {
  5. -- on/off control for device with id 2
  6. ['1/1/1'] = {
  7. id = 2,
  8. type = 'onoff'
  9. },
  10. -- 0..100% control for group with id 1
  11. ['1/1/3'] = {
  12. groupid = 1,
  13. type = 'dimmer'
  14. }
  15. }
  16.  
  17. local status = {
  18. -- on/off status for device with id 2
  19. ['1/1/2'] = {
  20. id = 2,
  21. type = 'onoff'
  22. },
  23. -- 0..100% status for group with id 1
  24. ['1/1/4'] = {
  25. groupid = 1,
  26. type = 'dimmer'
  27. }
  28. }
  29.  
  30. casambi.setmapping('control', control)
  31. casambi.setmapping('status', status)
  32.  
  33. -- Casambi access credentials
  34. casambi.setcredentials({
  35. api_key = 'API_KEY',
  36. email = 'user@example.com',
  37. network_password = 'NET_PASS',
  38. user_password = 'USER_PASS',
  39. })
  40.  
  41. casambi.init()
  42. end
  43.  
  44. casambi.run()
  • a. Provide all credentials (api key, email, network password and user password) in casambi.setcredentials({…})
  • b. Fill control and status mapping as needed. One group address can control a single Casambi device (id field must be set) or a single Casambi group (groupid field must be set).
    The type field specifies control or status type. Note that group control is only supported by onoff and dimmer types.
    Available control/status types and according object data types:
    – onoff = 1 bit boolean
    – dimmer = 1 byte scale
    – color = 3 bytes RGB
    – cct = 2 bytes unsigned
  • c. When changing control/status mapping or credentials make sure to do a full resident script restart via disable/enable.
  • d. Any connection or credentials errors will be visible in the Logs tab.
  •  
    4. getnetworkunits.lua script can be used to get all network and group IDs. Provide credentials and run the script once. Network structure will be visible in Logs entry.
     

    Example:

    * table:
    [“2”]
    * table:
    [“address”]
    * string: 8e2f6de63640
    [“firmwareVersion”]
    * string: 26.40
    [“position”]
    * number: 0
    [“id”]
    * number: 2
    [“type”]
    * string: Luminaire
    [“fixtureId”]
    * number: 1000
    [“name”]
    * string: CBU-ASD (0/1-10)
    [“groupId”]
    * number: 0

    Network has a device with ID 2. It does not belong to any group (groupId is 0).