LogicMachine Forum
Dyson Pure Hot+Cool - Printable Version

+- LogicMachine Forum (https://forum.logicmachine.net)
+-- Forum: LogicMachine eco-system (https://forum.logicmachine.net/forumdisplay.php?fid=1)
+--- Forum: Gateway (https://forum.logicmachine.net/forumdisplay.php?fid=10)
+--- Thread: Dyson Pure Hot+Cool (/showthread.php?tid=1907)

Pages: 1 2


RE: Dyson Pure Hot+Cool - gjniewenhuijse - 19.07.2019

Resident script:23: attempt to call field 'sha512' (a nil value)
stack traceback:

Sha512 is missing in my encdec.


RE: Dyson Pure Hot+Cool - admin - 19.07.2019

Try this one: https://dl.openrb.com/pkg/luaencdec_20180912_mxs.ipk


RE: Dyson Pure Hot+Cool - gjniewenhuijse - 24.09.2019

(15.07.2019, 13:27)admin Wrote: Send GET request to https://api.cp.dyson.com/v1/provisioningservice/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/libpurecoollink/blob/master/libpurecoollink/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"]



RE: Dyson Pure Hot+Cool - admin - 26.09.2019

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?


RE: Dyson Pure Hot+Cool - gjniewenhuijse - 27.09.2019

(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


RE: Dyson Pure Hot+Cool - admin - 01.10.2019

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/showthread.php?tid=2057&pid=12807#pid12807
Save it as user library named aes


RE: Dyson Pure Hot+Cool - gjniewenhuijse - 01.10.2019

many thanks, connecting with this output works!


RE: Dyson Pure Hot+Cool - gjniewenhuijse - 04.10.2019

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()



RE: Dyson Pure Hot+Cool - gjniewenhuijse - 09.10.2019

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



RE: Dyson Pure Hot+Cool - gjniewenhuijse - 30.01.2020

(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


RE: Dyson Pure Hot+Cool - admin - 30.01.2020

https://dl.openrb.com/pkg/libmosquitto_1.6.3-1_mxs.ipk
https://dl.openrb.com/pkg/luamosquitto_0.3-5_mxs.ipk


RE: Dyson Pure Hot+Cool - gjniewenhuijse - 20.08.2020

(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?


RE: Dyson Pure Hot+Cool - admin - 20.08.2020

You don't need this package, a newer version is already included in RC3


RE: Dyson Pure Hot+Cool - gjniewenhuijse - 20.08.2020

(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?


RE: Dyson Pure Hot+Cool - admin - 20.08.2020

You can reinstall the same FW and all packages will be reverted. You also need updated AES library: https://forum.logicmachine.net/showthread.php?tid=2057&pid=17735#pid17735


RE: Dyson Pure Hot+Cool - gjniewenhuijse - 20.08.2020

(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/showthread.php?tid=2057&pid=17735#pid17735

thanks, problem solved


RE: Dyson Pure Hot+Cool - gjniewenhuijse - 02.10.2021

.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/userregistration/authenticate?country=NL

and also the runCommand doesn't nothing.

What goes wrong? do i need to install extra libraries?


RE: Dyson Pure Hot+Cool - admin - 04.10.2021

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',



RE: Dyson Pure Hot+Cool - gjniewenhuijse - 04.10.2021

(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?


RE: Dyson Pure Hot+Cool - gjniewenhuijse - 12.10.2021

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))