Logic Machine Forum
Twinkly script - Printable Version

+- Logic Machine Forum (https://forum.logicmachine.net)
+-- Forum: LogicMachine eco-system (https://forum.logicmachine.net/forumdisplay.php?fid=1)
+--- Forum: Scripting (https://forum.logicmachine.net/forumdisplay.php?fid=8)
+--- Thread: Twinkly script (/showthread.php?tid=3049)



Twinkly script - gjniewenhuijse - 07.12.2020

I like to connect my Twinkly christmas leds to the LM.

I found a LUA example for Fibaro (see attached Twinkly.LUA), but i can't get the example working.

My code:
Code:
--[[ documentation
api: https://xled-docs.readthedocs.io/en/latest/rest_api.html
mqtt: https://xled-docs.readthedocs.io/en/latest/msqtt_api.html
--]]

-- init
if not init then
  require('socket.http')
  require('ltn12')
  require('json')
  socket.http.TIMEOUT = 5
  host = "192.168.x.xx"
    authToken = nil
  init = true
end

local api = {
    base = "/xled/v1/",
    endpoints = {
        login = "login",
        verify = "verify",
        mode = "led/mode",
        deviceName = "device_name",
        reset = "led/reset",
        movieConfig = "led/movie/config",
        movie = "led/movie/full",
        gestalt = "gestalt",
        brightness = "led/out/brightness"
    }
}

function getTwinklyResponseData(endPoint, body, content_type)
    local content_type = content_type or "application/json"
    local url = "http://" .. host .. api.base .. api.endpoints[endPoint]
  local method = "GET"
  if body ~= nil then
    method = "POST"
    if type(body) == "table" and content_type == "application/json" then
      body = json.encode(body)
    end
  end

  local response_body = {}
  local payload = body
  log('input', url, method, payload)
  local res, code, response_headers, status = socket.http.request
    {
      url = url,
      method = method,
      headers =
      {
        ["Content-Type"] = content_type,
        ["X-Auth-Token"] = authToken
      },
      source = ltn12.source.string(payload),
      sink = ltn12.sink.table(response_body)
    }
  log(res, code, response_headers, status)
  log('payload', payload)
  log('response_body', response_body)
  if res and code == 200 then
    return true
  else
    return false
  end
end


log('start')
if getTwinklyResponseData("gestalt") then
  getTwinklyResponseData("login",{challenge = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\a"})
end
log('einde')

my data response from "gestalt" looks right with the correct device information, but the "login" gives as result:
Code:
* arg: 1
  * number: 1
* arg: 2
  * number: 200
* arg: 3
  * table:
   ["server"]
    * string: esp-httpd/0.5
   ["content-type"]
    * string: application/json
   ["connection"]
    * string: close
* arg: 4
  * string: HTTP/1.1 200 OK
thats looks also right, but my response_data is: 
Code:
* arg: 1
  * string: response_body
* arg: 2
  * table:
   [1]
    * string: {"code":1106}
and thats not right i think.

Who can help me?


RE: Twinkly script - admin - 07.12.2020

The challenge = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\a" part seems incorrect, try the one from the docs:
{challenge = "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8="}

You can also try MQTT instead of HTTP, the integration seems easier for MQTT.


RE: Twinkly script - gjniewenhuijse - 07.12.2020

(07.12.2020, 10:02)admin Wrote: The challenge = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\a" part seems incorrect, try the one from the docs:
{challenge = "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8="}

same code 1106 response

-- i see this on https://labs.f-secure.com/blog/twinkly-twinkly-little-star/
Once the application knows the IP address of the lights, it authenticates with them, receives an authentication token and retrieves information about the device.

The authentication process, although a good idea, is flawed. First, the application makes a POST request to the endpoint '/xled/v1/login' with a base64 encoded 32 bit random number. The lights respond with an authentication token, how long it will be valid for, and a base64 encoded response to the challenge. This response is based on the random challenge number, the MAC address of the lights and a shared secret. The phone application sets the authentication token as a HTTP header and sends the received challenge response back to the lights on the endpoint '/xled/v1/verify'. This finalises the authentication allowing for authenticated endpoints to be called.


RE: Twinkly script - admin - 07.12.2020

You are missing the Content-Length header for the POST request. This should help:
Code:
function getTwinklyResponseData(endPoint, body, content_type)
  local content_type = content_type or "application/json"
  local url = "http://" .. host .. api.base .. api.endpoints[endPoint]
  local method = "GET"
  local cl
  if body ~= nil then
    method = "POST"
    if type(body) == "table" and content_type == "application/json" then
      body = json.encode(body)
    end
    cl = #body
  end

  local response_body = {}

  local res, code, response_headers, status = socket.http.request({
    url = url,
    method = method,
    headers =
    {
      ["Content-Type"] = content_type,
      ["Content-Length"] = cl,
      ["X-Auth-Token"] = authToken
    },
    source = ltn12.source.string(body),
    sink = ltn12.sink.table(response_body)
  })

  if res then
    return code == 200
  end
end



RE: Twinkly script - gjniewenhuijse - 07.12.2020

(07.12.2020, 11:14)admin Wrote: You are missing the Content-Length header for the POST request. This should help:
Code:
function getTwinklyResponseData(endPoint, body, content_type)
  local content_type = content_type or "application/json"
  local url = "http://" .. host .. api.base .. api.endpoints[endPoint]
  local method = "GET"
  local cl
  if body ~= nil then
    method = "POST"
    if type(body) == "table" and content_type == "application/json" then
      body = json.encode(body)
    end
    cl = #body
  end

  local response_body = {}

  local res, code, response_headers, status = socket.http.request({
    url = url,
    method = method,
    headers =
    {
      ["Content-Type"] = content_type,
      ["Content-Length"] = cl,
      ["X-Auth-Token"] = authToken
    },
    source = ltn12.source.string(body),
    sink = ltn12.sink.table(response_body)
  })

  if res then
    return code == 200
  end
end

yes, thanks


RE: Twinkly script - gjniewenhuijse - 07.12.2020

Thanks admin for all the work. Here my first working version
Code:
--[[ documentation
api: https://xled-docs.readthedocs.io/en/latest/rest_api.html
mqtt: https://xled-docs.readthedocs.io/en/latest/msqtt_api.html
fibaro example: https://forum.fibaro.com/topic/52767-qa-twinkly-lights-on-off/
--]]

host = "xxx.xxx.x.xx"  -- enter your Twinkly ip

-- debuggen
debug = false
function debugLog(iLog)
  if debug then log(iLog) end
end

-- init
if not init then
  require('socket.http')
  require('ltn12')
  require('json')
  socket.http.TIMEOUT = 5
  challenceToken = "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8="
  authToken = nil
  init = true
end

local api = {
    base = "/xled/v1/",
    endpoints = {
        login = "login",
        verify = "verify",
        mode = "led/mode",
        deviceName = "device_name",
        reset = "led/reset",
        movieConfig = "led/movie/config",
        movie = "led/movie/full",
        gestalt = "gestalt",
        brightness = "led/out/brightness"
    }
}


function getTwinklyResponseData(endPoint, body, func, content_type)
    debugLog(endPoint)
      content_type = content_type or "application/json"
    local url = "http://" .. host .. api.base .. api.endpoints[endPoint]
    local method = "GET"
      local cl
      if body ~= nil then
        method = "POST"
        if type(body) == "table" and content_type == "application/json" then
            body = json.encode(body)
        end
            cl = #body
    end

      debugLog({url, method, content_type, cl, authToken, body})
      local response_body = {}
    local res, code, response_headers, status = socket.http.request({
      url = url,
      method = method,
      headers =
      {
        ["Content-Type"] = content_type,
        ["Content-Length"] = cl,
        ["X-Auth-Token"] = authToken
      },
      source = ltn12.source.string(body),
      sink = ltn12.sink.table(response_body)
    })
      debugLog({res, code, response_headers, status, response_body})
      if res then
        if code == 200 then
            func(json.decode(table.concat(response_body))) 
        else
            func(nil, response_body)
        end
      else
        func(nil, {error = code})
      end
end


function connect(func_succ, func_err)
  getTwinklyResponseData("gestalt", nil,
  function(gestalt, err)
      if err == nil then
          getTwinklyResponseData(
              "login",
              {challenge = challenceToken},
              function(login, err)
                  if err == nil then
                      authToken = login.authentication_token
                      getTwinklyResponseData(
                          "verify",
                          {},
                          function(verify, err)
                              if err == nil then
                                  func_succ(gestalt)
                              else
                                  if func_err then
                                      func_err(err)
                                  else
                                      debugLog(json.encode(err))
                                  end
                              end
                          end
                      )
                  else
                      if func_err then
                          func_err(err)
                      else
                          debugLog(json.encode(err))
                      end
                  end
              end
          )
      else
          if func_err then
              func_err(err)
          else
              debugLog(json.encode(err))
          end
      end
  end
  )
end


function setMode(mode, func)
  if authToken == nil then
      debugLog("Device is not intialized!")
  end
  getTwinklyResponseData(
    "mode",
    {mode = mode},
    function(response, err)
      if err == nil then
          if func then
            func(response)
        end
      else
          debugLog(json.encode(err))
      end
    end
  )
end


function getMode(func)
  if authToken == nil then
      debugLog("Device is not intialized!")
  end
  getTwinklyResponseData(
    "mode",
    nil,
    function(response, err)
      if err == nil then
        if func then
            func(response)
        end
      else
          debugLog(json.encode(err))
      end
    end
  )
end


function setBrightness(value, func)
  if authToken == nil then
      debugLog("Device is not intialized!")
  end
  getTwinklyResponseData(
      "brightness",
    {mode = "enabled", type = "A", value = value},
    function(response, err)
      if err == nil then
        if func then
            func(response)
        end
      else
          debugLog(json.encode(err))
      end
    end
  )
end


function getBrightness(func)
  if authToken == nil then
      debugLog("Device is not intialized!")
  end
  getTwinklyResponseData(
    "brightness",
    nil,
    function(response, err)
      if err == nil then
        if func then
            func(response)
        end
      else
          debugLog(json.encode(err))
      end
    end
  )
end


------------------ some function tests ------------------


-- set mode
connect(
  function(data)
    setMode(
      "off", -- off, movie
      function(data)
        debugLog(json.encode(data)) 
      end
    )
  end
)


-- get current mode
connect(
  function(data)
    getMode(
      function(data)
        debugLog(json.encode(data)) 
      end
      )
  end
)

-- set brightness
connect(
  function(data)
    setBrightness(
      10,
      function(data)
          debugLog(json.encode(data))
      end
    )
  end
)

-- get current brightness
connect(
  function(data)
    getBrightness(
      function(data)
        debugLog(json.encode(data)) 
      end
    )
  end
)