Posts: 448
Threads: 93
Joined: Jun 2015
Reputation:
6
Resident script:23: attempt to call field 'sha512' (a nil value)
stack traceback:
Sha512 is missing in my encdec.
Posts: 7733
Threads: 42
Joined: Jun 2015
Reputation:
446
Posts: 448
Threads: 93
Joined: Jun 2015
Reputation:
6
(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 Accountassword 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"]
Posts: 7733
Threads: 42
Joined: Jun 2015
Reputation:
446
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?
Posts: 448
Threads: 93
Joined: Jun 2015
Reputation:
6
(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
Posts: 7733
Threads: 42
Joined: Jun 2015
Reputation:
446
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
Posts: 448
Threads: 93
Joined: Jun 2015
Reputation:
6
many thanks, connecting with this output works!
Posts: 448
Threads: 93
Joined: Jun 2015
Reputation:
6
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()
Posts: 448
Threads: 93
Joined: Jun 2015
Reputation:
6
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
Posts: 448
Threads: 93
Joined: Jun 2015
Reputation:
6
Posts: 7733
Threads: 42
Joined: Jun 2015
Reputation:
446
Posts: 448
Threads: 93
Joined: Jun 2015
Reputation:
6
20.08.2020, 08:33
(This post was last modified: 20.08.2020, 08:34 by gjniewenhuijse.)
(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?
Posts: 7733
Threads: 42
Joined: Jun 2015
Reputation:
446
You don't need this package, a newer version is already included in RC3
Posts: 448
Threads: 93
Joined: Jun 2015
Reputation:
6
(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?
Posts: 7733
Threads: 42
Joined: Jun 2015
Reputation:
446
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
Posts: 448
Threads: 93
Joined: Jun 2015
Reputation:
6
(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
Posts: 448
Threads: 93
Joined: Jun 2015
Reputation:
6
02.10.2021, 08:54
(This post was last modified: 02.10.2021, 08:55 by gjniewenhuijse.)
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?
Posts: 7733
Threads: 42
Joined: Jun 2015
Reputation:
446
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',
Posts: 448
Threads: 93
Joined: Jun 2015
Reputation:
6
(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?
Posts: 448
Threads: 93
Joined: Jun 2015
Reputation:
6
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))
|