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.

Dyson Pure Hot+Cool
#21
Resident script:23: attempt to call field 'sha512' (a nil value)
stack traceback:

Sha512 is missing in my encdec.
Reply
#22
Try this one: https://dl.openrb.com/pkg/luaencdec_20180912_mxs.ipk
Reply
#23
(15.07.2019, 13:27)admin Wrote: Send GET request to https://api.cp.dyson.com/v1/provisioning...e/manifest after the first one to get device list. You need to decode the first result using JSON and then pass AccountTongueassword values as Basic auth. See last example in HTTP docs: http://w3.impa.br/~diego/software/luasocket/http.html

Yes i found why there was no return data. I have to use the v2 api for newer devices:

Code:
https = require 'ssl.https'
require('json')
socket.http.TIMEOUT = 5

local cUsername = 'xxxx@xxxxxx.com'
local cPassword = 'xxxxxxxx'

-- get session info
local cBody1 = '{"Email":"' .. cUsername .. '","Password":"' .. cPassword ..'"}'
local cReq1 = {}
local cUrl1 = 'https://api.cp.dyson.com/v1/userregistration/authenticate?country=NL'
result1 = https.request({
    url = cUrl1,
    method = 'POST',
    headers = {
      ['content-length'] = #cBody1,
      ['content-type'] = 'application/json'
    },
    source = ltn12.source.string(cBody1),
    sink = ltn12.sink.table(cReq1)
})
if result1 and cReq1 then
  cReq1 = table.concat(cReq1)
  cReq1 = json.pdecode(cReq1)
  cAccount = nil
  cPassword = nil
  if cReq1 then
    if cReq1.Account and cReq1.Password then
      cAccount = cReq1.Account
      cPassword = cReq1.Password
    end
  end
  --log(cAccount,cPassword)
else
  log(result1)
end

mime = require("mime")
local cAuth = "Basic " .. mime.b64(cAccount..":"..cPassword)
local cReq2 = {}
local cUrl2 = 'https://api.cp.dyson.com/v2/provisioningservice/manifest'   -- v2 for new devices
result2, c, h = https.request({
  url = cUrl2,
  method = 'GET',
  headers = {
      ['authorization'] = cAuth
  },
  sink = ltn12.sink.table(cReq2)
})
--log(result2,c,h)
log(cReq2)

But the next question, how to decode the password to use it in my mqtt client. Original code from https://github.com/CharlesBlonde/libpure...k/utils.py:
Code:
def decrypt_password(encrypted_password):

    """Decrypt password.



    :param encrypted_password: Encrypted password

    """

    key = b'\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f\x10' \

          b'\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f '

    init_vector = b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' \

                  b'\x00\x00\x00\x00'

    cipher = AES.new(key, AES.MODE_CBC, init_vector)

    json_password = json.loads(unpad(

        cipher.decrypt(base64.b64decode(encrypted_password)).decode('utf-8')))

    return json_password["apPasswordHash"]
Reply
#24
Do you have this encrypted password in plain text? It should be encoded with base64. Can you send it via PM so I can test the decoding locally?
Reply
#25
(26.09.2019, 08:07)admin Wrote: Do you have this encrypted password in plain text? It should be encoded with base64. Can you send it via PM so I can test the decoding locally?

send by PM
Reply
#26
Thanks, here's decryption function, data contents is what you sent me via PM.
Code:
data = '...'

-- create key
key = {}
for i = 1, 32 do
  key[ #key + 1 ] = i
end
key = string.char(unpack(key))

-- decrypt data
data = require('encdec').base64dec(data)
aes = require('user.aes')
aes_256_cbc, err = aes:new(key, nil, aes.cipher(256, 'cbc'), { iv = string.rep('\0', 16) }, nil, 0)
data = aes_256_cbc:decrypt(data)

-- unpad data
len = string.byte(data, #data) + 1
data = data:sub(1, -len)

-- decode json
data, err = require('json').pdecode(data)

if type(data) == 'table' then
  pass = data.apPasswordHash
end

log(pass)

Use aes.lua from this post: https://forum.logicmachine.net/showthrea...7#pid12807
Save it as user library named aes
Reply
#27
many thanks, connecting with this output works!
Reply
#28
Yes it works, i can switch the Dyson off, with the follow command (code is not perfect, but you can use it as start for yourself):

Code:
require('user.nit_dyson')   
runCommand(5,CMD_OFF)

My library
Code:
--[[
use getDevices('email@email.com','password') to get the encrypted password and fill it in dysons[] so you can use this offline.
--]]

-- product types
DYSON_PURE_COOL_LINK_TOUR = '475'
DYSON_PURE_COOL_LINK_DESK = '469'
DYSON_PURE_HOT_COOL_LINK_TOUR = '455'
DYSON_360_EYE = 'N223'
DYSON_PURE_HOT_COOL_2018 = '527' --HP04 Dyson Pure Hot+Cool 2018 (244289-01)

CMD_OFF = '{"fpwr":"OFF"}'

-- all dyson devices
dysons =     {} -- room(not used), ip, port(default 1883), type, username/serial, password
dysons[5] = {'Room', '192.168.x.xxx', 1883, DYSON_PURE_HOT_COOL_2018, 'A1B-EU-KNAXXXXX', 'verylongencryptedpasswordfromgetdevices'}


-- get devices from web portal
function getDevices(iPortalUser, iPortalPwd)
  https = require 'ssl.https'
  require('json')
  socket.http.TIMEOUT = 5

  local cUsername = iPortalUser
  local cPassword = iPortalPwd

  -- get session info
  local cBody1 = '{"Email":"' .. cUsername .. '","Password":"' .. cPassword ..'"}'
  local cReq1 = {}
  local cUrl1 = 'https://api.cp.dyson.com/v1/userregistration/authenticate?country=NL'
  result1 = https.request({
      url = cUrl1,
      method = 'POST',
      headers = {
        ['content-length'] = #cBody1,
        ['content-type'] = 'application/json'
      },
      source = ltn12.source.string(cBody1),
      sink = ltn12.sink.table(cReq1)
  })
  if result1 and cReq1 then
    cReq1 = table.concat(cReq1)
    cReq1 = json.pdecode(cReq1)
    cAccount = nil
    cPassword = nil
    if cReq1 then
      if cReq1.Account and cReq1.Password then
        cAccount = cReq1.Account
        cPassword = cReq1.Password
      end
    end
    --log(cAccount,cPassword)
  else
    log(result1)
  end

  mime = require("mime")
  local cAuth = "Basic " .. mime.b64(cAccount..":"..cPassword)
  local cReq2 = {}
  local cUrl2 = 'https://api.cp.dyson.com/v2/provisioningservice/manifest'   -- v2 for new devices
  result2, c, h = https.request({
    url = cUrl2,
    method = 'GET',
    headers = {
      ['authorization'] = cAuth
    },
    sink = ltn12.sink.table(cReq2)
  })
  --log(result2,c,h)
  log(cReq2)
end


-- decrypt password
function decryptPwd(iPwd)
  data = iPwd
  -- create key
  key = {}
  for i = 1, 32 do
    key[ #key + 1 ] = i
  end
  key = string.char(unpack(key))

  -- decrypt data
  data = require('encdec').base64dec(data)
  aes = require('user.aes')
  aes_256_cbc, err = aes:new(key, nil, aes.cipher(256, 'cbc'), { iv = string.rep('\0', 16) }, nil, 0)
  data = aes_256_cbc:decrypt(data)

  -- unpad data
  len = string.byte(data, #data) + 1
  data = data:sub(1, -len)

  -- decode json
  data, err = require('json').pdecode(data)

  if type(data) == 'table' then
    pass = data.apPasswordHash
  end
 
  --log(pass)
  return pass
end

-- run dyson command
function runCommand(IDevice, iCmd)
  broker = dysons[IDevice][2]
  port = dysons[IDevice][3]
  producttype = dysons[IDevice][4]
  username = dysons[IDevice][5]
  password = dysons[IDevice][6]
 
  -- after this initialization, you'll have to use the hashed password to connect to the fan. The hashed password is a base64 encoded of the sha512 of the password
  passwordDec = decryptPwd(password)

  topicCmd     = producttype .. '/' .. username .. '/command'

  mqtt = require('mosquitto')
  client = mqtt.new()

  client.ON_CONNECT = function(status, rc, err)
    if status then
      payload = '{"msg":"STATE-SET", "time":"'..os.date("%Y-%m-%dT%H:%M:%SZ")..'", "data":'..iCmd..'}'
      client:publish(topicCmd, payload)
    end
  end

  client.ON_PUBLISH = function()
    client:disconnect()
  end

  client:login_set(username, passwordDec)
  client:connect(broker, port)
  client:loop_forever() 
end

You can also make an event handler like:
Code:
require('user.nit_dyson')

broker = '192.168.x.xx'
port = 1883 -- default port is 1883
username = 'A1B-EU-KNAXXXXX' -- device serial
password = 'longpassword'
passwordDec = decryptPwd(password)
producttype = DYSON_PURE_HOT_COOL_2018

topic         = producttype .. '/' .. username .. '/status/current'
topicCmd     = producttype .. '/' .. username .. '/command'

mqtt = require('mosquitto')
client = mqtt.new()

client.ON_CONNECT = function(status, rc, err)
  if status then
    log('connect ok')
    client:subscribe(topic)
   
    --payload = '{"msg":"STATE-SET", "time":"'..os.date("%Y-%m-%dT%H:%M:%SZ")..'", "data":{"fpwr":"OFF"}}'
    --client:publish(topicCmd, payload)
   
  else
    log('connect error', rc, err)
  end
end

client.ON_MESSAGE = function(mid, topic, data)
  log('message', topic, data)
end

client.ON_PUBLISH = function()
  --log('publish')
    --client:disconnect()
end

client.ON_LOG = function()
  --log('log')
end

client.ON_DISCONNECT = function()
  --log('disconnect')
end

client:login_set(username, passwordDec)
client:connect(broker, port)
client:loop_forever()
Reply
#29
more codes:
Code:
CMD_ON         = '{"fpwr":"ON"}'
CMD_OFF     = '{"fpwr":"OFF"}'
CMD_AUTO_ON     = '{"auto":"ON"}'
CMD_AUTO_OFF     = '{"auto":"OFF"}'
CMD_NIGHT_ON     = '{"nmod":"ON"}'
CMD_NIGHT_OFF    = '{"nmod":"OFF"}'
CMD_HEAT_ON     = '{"hmod":"HEAT"}'
CMD_HEAT_OFF     = '{"hmod":"OFF"}'
-- not sure if its right, but it works
function CMD_HEAT(iTemp) -- integer celcius
    return '{"hmax":"'..2932+((iTemp-20)*10)..'"}' --2932=20 2952=22 2982=25 3012=28 enz
end
Reply
#30
(12.07.2019, 10:00)admin Wrote: https://dl.openrb.com/pkg/libmosquitto_1.6.3-1_mxs.ipk
https://dl.openrb.com/pkg/luamosquitto_0.3-2_mxs.ipk

where to find this ipk file: https://dl.openrb.com/pkg/luamosquitto_0.3-2_mxs.ipk
Reply
#31
https://dl.openrb.com/pkg/libmosquitto_1.6.3-1_mxs.ipk
https://dl.openrb.com/pkg/luamosquitto_0.3-5_mxs.ipk
Reply
#32
(19.07.2019, 12:28)admin Wrote: Try this one: https://dl.openrb.com/pkg/luaencdec_20180912_mxs.ipk
 Where to find the latest version for LM 2020.07 RC3 firwmare
and also the latest mosquitto packages?
Reply
#33
You don't need this package, a newer version is already included in RC3
Reply
#34
(20.08.2020, 08:34)admin Wrote: You don't need this package, a newer version is already included in RC3

oops, i overwitten this package, do you have the latest one? with the latest one i received also an error.

So no need for luaencdec_20180912_mxs.ipk and no need for the mosquitto packages?
Reply
#35
You can reinstall the same FW and all packages will be reverted. You also need updated AES library: https://forum.logicmachine.net/showthrea...5#pid17735
Reply
#36
(20.08.2020, 08:42)admin Wrote: You can reinstall the same FW and all packages will be reverted. You also need updated AES library: https://forum.logicmachine.net/showthrea...5#pid17735

thanks, problem solved
Reply
#37
.lua   dyson.lua (Size: 5.04 KB / Downloads: 4)

After upgrading to a LM5 (HW: LM5 Lite + Ext (i.MX6) SW: 20210806) my code doesn't work

in getDevices there's no response from: https://api.cp.dyson.com/v1/userregistra...country=NL

and also the runCommand doesn't nothing.

What goes wrong? do i need to install extra libraries?
Reply
#38
I've tested your code and there's Cloudflare's 1020 "Access Denied" error. I've tried using different user agent headers but the request is still blocked. I'm not sure whether it is possible to access the authentication using a script now. There can also be a location-based blocking. You can try adding the user-agent header after content-type and check yourself:
Code:
['content-type'] = 'application/json',
['user-agent'] = 'Mozilla/5.0',
Reply
#39
(04.10.2021, 06:41)admin Wrote: I've tested your code and there's Cloudflare's 1020 "Access Denied" error. I've tried using different user agent headers but the request is still blocked. I'm not sure whether it is possible to access the authentication using a script now. There can also be a location-based blocking. You can try adding the user-agent header after content-type and check yourself:
Code:
['content-type'] = 'application/json',
['user-agent'] = 'Mozilla/5.0',

I checked this option already but it doesn't work. I see in the Dyson app that you have you enter your emailadres, after that you have to signin with a password and a code send by email. Maybe thats the change?

Another problem is that i can't send message to the Dyson on my local lan with mqtt (with the runCommand function). Any idea why this stops working after the HW en SW upgrade?
Reply
#40
Many thanks to admin to make it working again

Code:
--[[
https://www.npmjs.com/package/homebridge-dyson-pure-cool
https://github.com/lukasroegner/homebridge-dyson-pure-cool

9-10-2021
Sniff passwords with an Android phone and the Dyson link app and Netcapture (also sniff SSL traffic).

You receive data like this and fill them inside the dyson devices (theres a X between username/serial and the password):
.MQTT.<paho3949609190773A1B-EU-MKA0000AXn0MYFEYNRMCk7n9nCxW/F3bDFGQuLtBjQy3W4rg46ZqPc0+wkqqSVTdSVXXXXQnxc2NqgbQPABzbCe81xWQ==
--]]

local json = require('json')
debug = false

-- product types
DYSON_PURE_COOL_LINK_TOUR = '475'
DYSON_PURE_COOL_LINK_DESK = '469'
DYSON_PURE_HOT_COOL_LINK_TOUR = '455'
DYSON_360_EYE = 'N223'
DYSON_PURE_HOT_COOL_2018 = '527' --HP04 Dyson Pure Hot+Cool 2018 (244289-01)

CMD_ON         = { fpwr = 'ON' }
CMD_OFF     = { fpwr = 'OFF' }
CMD_AUTO_ON     = { auto = 'ON' }
CMD_AUTO_OFF     = { auto = 'OFF' }
CMD_NIGHT_ON     = { nmod = 'ON' }
CMD_NIGHT_OFF    = { nmod = 'OFF' }
CMD_HEAT_ON     = { hmod = 'HEAT' }
CMD_HEAT_OFF     = { hmod = 'OFF' }
function CMD_HEAT(iTemp) -- in graden celcius
    return ({ hmax = tostring(2932+((iTemp-20)*10)) }) --2912=18 2932=20 2952=22 2982=25 3012=28 enz
end

-- all dyson devices
dysons =     {} -- room(not used), ip, port(default 1883), type, username/serial, password
dysons[1] = {'Room 1      ', '192.168.0.14', 1883, DYSON_PURE_HOT_COOL_2018, 'A1B-EU-MKA0000A', 'n0MYFEYNRMCk7n9nCxW/F3bDFGQuLtBjQy3W4rg46ZqPc0+wkqqSVTdSVXXXXQnxc2NqgbQPABzbCe81xWQ=='}

-- debug logger
function debugLog(iLog)
  if debug then
    log(iLog)
  end
end

-- send data to dyson
function mqsend(ip, username, password, prefix, cmddata)
  local function datetime()
    return os.date('!%Y-%m-%dT%H:%M:%SZ')
  end

  local ts, tu = os.microtime()
  local clientid = 'lm-' .. ts .. '-' .. tu
  local mq = require('mosquitto').new(clientid)

  mq:login_set(username, password)

  mq.ON_CONNECT = function(res, ...)
    debugLog('mqtt connect status', res, ...)

    if res then
      local topic = prefix .. '/command'
      local cmd = {
        f = topic,
        time = datetime(),
        msg = 'STATE-SET',
        data = cmddata,
        ['mode-reason'] = 'LAPP',
      }

      mq:publish(topic, json.encode(cmd), 1)
    else
      mq:disconnect()
    end
  end

  mq.ON_PUBLISH = function(mid, rc)
    debugLog('published', mid, rc)

    mq:disconnect()
  end

  mq.ON_DISCONNECT = function(...)
    debugLog('mqtt disconnect', ...)
  end

  mq.ON_LOG = function(...)
    debugLog(...)
  end

  local res, rc, errno = mq:connect(ip)

  if not res then
    debugLog('connect failed', rc, errno)
    return
  end

  -- process mqtt messages
  for i = 1, 20 do
    local res, err = mq:loop()
    if not res then
      break
    end
  end
end


-- receive current dyson state
function mqstatus(ip, username, password, prefix)
  local function datetime()
    return os.date('!%Y-%m-%dT%H:%M:%SZ')
  end

  local ts, tu = os.microtime()
  local clientid = 'lm-' .. ts .. '-' .. tu
  local mq = require('mosquitto').new(clientid)
  local count = 0
  local status = {}

  mq:login_set(username, password)

  mq.ON_CONNECT = function(res, ...)
    log('mqtt connect status', res, ...)

    if res then
      local cmd = {
        time = datetime(),
        msg = 'REQUEST-CURRENT-STATE',
      }

      mq:subscribe(prefix .. '/status/current')
      mq:publish(prefix .. '/command', json.encode(cmd))
    else
      mq:disconnect()
    end
  end

  mq.ON_MESSAGE = function(rc, topic, payload)
    payload = json.pdecode(payload)

    if type(payload) ~= 'table' then
      return
    end

    local data

    if payload.msg == 'CURRENT-STATE' then
      data = payload['product-state']
    elseif payload.msg == 'ENVIRONMENTAL-CURRENT-SENSOR-DATA' then
      data = payload['data']
    end

    if type(data) == 'table' then
      for key, value in pairs(data) do
        if value:match('^%d+$') then
          value = tonumber(value)
        end

        status[ key ] = value
      end

      count = count + 1
    end

    if count == 2 then
      mq:disconnect()
    end
  end

  mq.ON_DISCONNECT = function(...)
    debugLog('mqtt disconnect', ...)
  end

  local res, rc, errno = mq:connect(ip)

  if not res then
    debugLog('connect failed', rc, errno)
    return
  end

  -- process mqtt messages
  for i = 1, 20 do
    local res, err = mq:loop()
    if not res then
      break
    end
  end

  debugLog(status)
  return status
end


-- run dyson command
function runCommand(IDevice, iCmd)
  broker = dysons[IDevice][2]
  port = dysons[IDevice][3]
  producttype = dysons[IDevice][4]
  username = dysons[IDevice][5]
  password = dysons[IDevice][6]
 
  mqsend(
    broker,
    username,
    password,
    producttype..'/'..username,
    iCmd
    )
end
-- runCommand(1,CMD_OFF)

-- get dyson command
function getState(IDevice)
  broker = dysons[IDevice][2]
  port = dysons[IDevice][3]
  producttype = dysons[IDevice][4]
  username = dysons[IDevice][5]
  password = dysons[IDevice][6]
 
  return mqstatus(
            broker,
            username,
            password,
            producttype..'/'..username
                 )
end
-- log(getState(1))
Reply


Forum Jump: