Sorry for not chiming in before, but my Dyson (Camping) setup has been out of reach since August.
Here is what worked this summer: (sampled from various sources, not my original work, thanks this thread & the internet!). Collected for a one-post summing-up source.
- Not 100% sure, but I think one has to do the 2FA Dyson phone app login at least once.
- Use the WiFi password from the sticker on you unit. I will use my actual values here so you can verify. Consider that low risk. WiFi password yvatwlfs
- Now run the following python hashing program:
import base64
import hashlib
# Ask for the password
pwd = input("Product WiFi Password (e.g.: adgjsfhk):")
# Transfer password to hash version
hash = hashlib.sha512()
hash.update(pwd.encode('utf-8'))
pwd_hash = base64.b64encode(hash.digest()).decode('utf-8')
# Print out password hash
print(pwd_hash)
pi@raspberrypi:~ $ python dysonhash.py
Product WiFi Password (e.g.: adgjsfhk):yvatwlfs
Zu6rHzM9mOVdqy3FKLiPfUzWL5/gfCXn8R4G3eJ99wv6tEhf56+kIbcRGyXMMi0Cz6h5Dvl1DdEvhFAcR8wHUQ==
pi@raspberrypi:~ $
Then, you dyson.items file becomes: (remember: the fan is the broker!)
Bridge mqtt:broker:dyson455broker [ host="192.168.1.7", port="1883", secure=false, username="PT4-EU-PDA6549A", password="Zu6rHzM9mOVdqy3FKLiPfUzWL5/gfCXn8R4G3eJ99wv6tEhf56+kIbcRGyXMMi0Cz6h5Dvl1DdEvhFAcR8wHUQ==", qos=1 ]
{
Thing topic dyson455 {
Channels:
Type string : dyson_status [ stateTopic="455/PT4-EU-PDA6549A/status/current", postCommand="true" ]
Type string : dyson_request [ commandTopic="455/PT4-EU-PDA6549A/command", postCommand="true" ]
}
}
.things file:
// Dyson Pure Cool Link Items
String Dyson455_Topic_Status_Current "[%s]" {channel="mqtt:topic:dyson455broker:dyson455:dyson_status"}
String Dyson455_Request "[%s]" {channel="mqtt:topic:dyson455broker:dyson455:dyson_request"}
//VIRTUAL ITEMS
Number Dyson455_Tact "Temperature [%.2f °C]" <temperature>
Number Dyson455_Hact "Humidity [%.0f %%]" <humidity>
Number Dyson455_Pact "P-Value [%d]" <smoke>
Number Dyson455_Vact "V-Value [%d]" <smoke>
Number Dyson455_Fmod "Mode" <switch>
Switch Dyson455_Rhtm "Contin. Measurement" <line>
Number Dyson455_Fnsp "Fan Speed [%d]" <flow>
Number Dyson455_Qtar "Quality Target" <smoke>
Switch Dyson455_Oson "Turning" <incline>
Switch Dyson455_Nmod "Night Mode" <moon>
Number Dyson455_Filf "Remaining Filter Hours [%d h]" <sewerage>
Number Dyson455_Fnst "Fan Activity [%d]" <fan>
Number Dyson455_Hmod "Heater Mode" <switch>
Number Dyson455_Hmax "Target Temperature [%.1f °C]" <temperature>
Number Dyson455_Hsta "Heating Activity [%d]" <radiator>
Switch Dyson455_Ffoc "Heater Focus" <fan>
.sitemap
Frame label="Dyson vifte" {
Switch item=Dyson455_Fmod label="Mode" mappings=[0="OFF", 1="FAN", 2="AUTO"]
Setpoint item=Dyson455_Fnsp label="Fan Speed [%d]" minValue=1 maxValue=10 step=1 visibility=[Dyson455_Fmod > 0]
Default item=Dyson455_Oson label="Turning" visibility=[Dyson455_Fmod > 0]
Default item=Dyson455_Ffoc label="Air flow Focus" visibility=[Dyson455_Fmod > 0]
Switch item=Dyson455_Hmod label="Heater Mode" mappings=[0="COOL", 1="HEAT"] visibility=[Dyson455_Fmod > 0]
Setpoint item=Dyson455_Hmax label="Target Temperature [%.0f °C]" minValue=1 maxValue=37 step=1 visibility=[Dyson455_Fmod > 0]
Switch item=Dyson455_Qtar label="Quality Target" mappings=[1="LOW", 3="AVERAGE", 4="HIGH"] visibility=[Dyson455_Fmod > 0]
Default item=Dyson455_Tact label="Temperature [%.1f °C]" visibility=[Dyson455_Fmod > 0]
Default item=Dyson455_Hact label="Humidity [%.0f %%]" visibility=[Dyson455_Fmod > 0]
Default item=Dyson455_Pact label="P-Value [%d]" visibility=[Dyson455_Fmod > 0]
Default item=Dyson455_Vact label="V-Value [%d]" visibility=[Dyson455_Fmod > 0]
Default item=Dyson455_Rhtm label="Contin. Measurement" visibility=[Dyson455_Fmod > 0]
Default item=Dyson455_Nmod label="Night Mode" visibility=[Dyson455_Fmod > 0]
Default item=Dyson455_Filf label="Remaining Filter Hours" visibility=[Dyson455_Fmod > 0]
Default item=Dyson455_Fnst label="Fan Activity [%d]" visibility=[Dyson455_Fmod > 0]
Default item=Dyson455_Hsta label="Heater Activity [%d]" visibility=[Dyson455_Fmod > 0]
}
.rules
import java.text.SimpleDateFormat
//import java.util.TimeZone
// Rules for Dyson Pure Cool Link (455)
var SimpleDateFormat simpleDF = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS")
//val DateTimeType simpleDF = DateTimeType.valueOf("yyyy-MM-dd'T'HH:mm:ss.SSS")
// Start of STATE-SET msg
var String startStateSetCmdStr = "{\"msg\":\"STATE-SET\",\"time\":"
// Variable to store String used as command
var String cmdStr
// TOPIC "455/<id>/status/current" ############################################
// msg "ENVIRONMENTAL-CURRENT-SENSOR-DATA" ############
// msg "CURRENT-STATE" ################################
// msg "STATE-CHANGE" #################################
rule "455/<id>/status/current"
when
Item Dyson455_Topic_Status_Current received update
then
val String content = Dyson455_Topic_Status_Current.state.toString()
val String msg = transform("JSONPATH", "$.msg", content)
//logWarn("DysonUpdate",msg)
if (msg == "ENVIRONMENTAL-CURRENT-SENSOR-DATA") {
val String tact_str = transform("JSONPATH", "$.data.tact", content)
if (tact_str != "OFF") {
var double tact = Float.parseFloat(tact_str)
tact = (tact - 2731.5)/10
postUpdate(Dyson455_Tact, tact as Number)
}
val String hact_str = transform("JSONPATH", "$.data.hact", content)
if (hact_str != "OFF") {
val int hact = Integer.parseInt(hact_str)
postUpdate(Dyson455_Hact, hact as Number)
}
val int pact = Integer.parseInt(transform("JSONPATH", "$.data.pact", content))
postUpdate(Dyson455_Pact, pact as Number)
val String vact_str = transform("JSONPATH", "$.data.vact", content)
if (vact_str != "INIT") {
val int vact = Integer.parseInt(vact_str)
postUpdate(Dyson455_Vact, vact as Number)
}
} else if (msg == "CURRENT-STATE") {
val String fmod_str = transform("JSONPATH", "$.product-state.fmod", content)
switch fmod_str {
case "AUTO": postUpdate(Dyson455_Fmod, 2 as Number)
case "FAN": postUpdate(Dyson455_Fmod, 1 as Number)
case "OFF": postUpdate(Dyson455_Fmod, 0 as Number)
}
if (transform("JSONPATH", "$.product-state.rhtm", content) == "ON") {
// logWarn("Postupdate","ON")
postUpdate(Dyson455_Rhtm, ON)
} else {
//logWarn("Postupdate","OFF")
postUpdate(Dyson455_Rhtm, OFF)
}
val String fnsp = transform("JSONPATH", "$.product-state.fnsp", content)
if (fnsp == "AUTO") {
postUpdate(Dyson455_Fnsp, 11 as Number)
} else {
postUpdate(Dyson455_Fnsp, Integer.parseInt(fnsp) as Number)
}
val int qtar = Integer.parseInt(transform("JSONPATH", "$.product-state.qtar", content))
postUpdate(Dyson455_Qtar, qtar as Number)
if (transform("JSONPATH", "$.product-state.oson", content) == "ON") {
postUpdate(Dyson455_Oson, ON)
} else {
postUpdate(Dyson455_Oson, OFF)
}
if (transform("JSONPATH", "$.product-state.nmod", content) == "ON") {
postUpdate(Dyson455_Nmod, ON)
} else {
postUpdate(Dyson455_Nmod, OFF)
}
val int filf = Integer.parseInt(transform("JSONPATH", "$.product-state.filf", content))
postUpdate(Dyson455_Filf, filf as Number)
if (transform("JSONPATH", "$.product-state.fnst", content) == "FAN") {
postUpdate(Dyson455_Fnst, 1)
} else {
postUpdate(Dyson455_Fnst, 0)
}
val String hmod_str = transform("JSONPATH", "$.product-state.hmod", content)
switch hmod_str {
case "AUTO": postUpdate(Dyson455_Hmod, 2 as Number)
case "HEAT": postUpdate(Dyson455_Hmod, 1 as Number)
case "OFF": postUpdate(Dyson455_Hmod, 0 as Number)
}
val String hmax_str = transform("JSONPATH", "$.product-state.hmax", content)
if (hmax_str != "OFF") {
var double hmax = Float.parseFloat(hmax_str)
hmax = (hmax - 2731.5)/10
postUpdate(Dyson455_Hmax, hmax as Number)
}
if (transform("JSONPATH", "$.product-state.hsta", content) == "HEAT") {
postUpdate(Dyson455_Hsta, 1)
} else {
postUpdate(Dyson455_Hsta, 0)
}
if (transform("JSONPATH", "$.product-state.ffoc", content) == "ON") {
postUpdate(Dyson455_Ffoc, ON)
} else {
postUpdate(Dyson455_Ffoc, OFF)
}
} else if (msg == "STATE-CHANGE") {
val String fmod_str = transform("JSONPATH", "$.product-state.fmod[1]", content)
switch fmod_str {
case "AUTO": postUpdate(Dyson455_Fmod, 2 as Number)
case "FAN": postUpdate(Dyson455_Fmod, 1 as Number)
case "OFF": postUpdate(Dyson455_Fmod, 0 as Number)
}
if (transform("JSONPATH", "$.product-state.rhtm[1]", content) == "ON") {
postUpdate(Dyson455_Rhtm, ON)
} else {
postUpdate(Dyson455_Rhtm, OFF)
}
val String fnsp = transform("JSONPATH", "$.product-state.fnsp[1]", content)
if (fnsp == "AUTO") {
postUpdate(Dyson455_Fnsp, 11 as Number)
} else {
postUpdate(Dyson455_Fnsp, Integer.parseInt(fnsp) as Number)
}
val int qtar = Integer.parseInt(transform("JSONPATH", "$.product-state.qtar[1]", content))
postUpdate(Dyson455_Qtar, qtar as Number)
if (transform("JSONPATH", "$.product-state.oson[1]", content) == "ON") {
postUpdate(Dyson455_Oson, ON)
} else {
postUpdate(Dyson455_Oson, OFF)
}
if (transform("JSONPATH", "$.product-state.nmod[1]", content) == "ON") {
postUpdate(Dyson455_Nmod, ON)
} else {
postUpdate(Dyson455_Nmod, OFF)
}
val int filf = Integer.parseInt(transform("JSONPATH", "$.product-state.filf[1]", content))
postUpdate(Dyson455_Filf, filf as Number)
if (transform("JSONPATH", "$.product-state.fnst[1]", content) == "FAN") {
postUpdate(Dyson455_Fnst, 1)
} else {
postUpdate(Dyson455_Fnst, 0)
}
val String hmod_str = transform("JSONPATH", "$.product-state.hmod[1]", content)
switch hmod_str {
case "AUTO": postUpdate(Dyson455_Hmod, 2 as Number)
case "HEAT": postUpdate(Dyson455_Hmod, 1 as Number)
case "OFF": postUpdate(Dyson455_Hmod, 0 as Number)
}
val String hmax_str = transform("JSONPATH", "$.product-state.hmax[1]", content)
if (hmax_str != "OFF") {
var double hmax = Float.parseFloat(hmax_str)
hmax = (hmax - 2731.5)/10
postUpdate(Dyson455_Hmax, hmax as Number)
}
if (transform("JSONPATH", "$.product-state.hsta[1]", content) == "HEAT") {
postUpdate(Dyson455_Hsta, 1)
} else {
postUpdate(Dyson455_Hsta, 0)
}
if (transform("JSONPATH", "$.product-state.ffoc[1]", content) == "ON") {
postUpdate(Dyson455_Ffoc, ON)
} else {
postUpdate(Dyson455_Ffoc, OFF)
}
}
end
//#############################################################################
// REQUEST "455/<id>/command"##################################################
// msg "REQUEST-CURRENT-STATE" ########################
rule "Request Current State every 30 seconds"
when
Time cron "0/30 * * * * ?"
then
// Create current TimeStamp as UTC
//simpleDF.setTimeZone(TimeZone.getTimeZone("UTC"))
var String timeStampUtc = simpleDF.format(new java.util.Date())
cmdStr = "{\"msg\":\"REQUEST-CURRENT-STATE\","
cmdStr = cmdStr + " \"time\":\"" + timeStampUtc + "Z\"}"
//logWarn("DysonUpdateRequest",cmdStr)
Dyson455_Request.sendCommand(cmdStr)
end
// msg "SET-STATE" ####################################
rule "455/<id>/command SET-STATE fmod"
when
Item Dyson455_Fmod received command
then
// Create current TimeStamp as UTC
////simpleDF.setTimeZone(TimeZone.getTimeZone("UTC"))
val String timeStampUtc = simpleDF.format(new java.util.Date())
// Cast fmod to String representation
var String fmod = "OFF"
switch receivedCommand {
case 2: fmod = "AUTO"
case 1: fmod = "FAN"
}
// Create JSON for commanding and send it
cmdStr = startStateSetCmdStr + "\"" + timeStampUtc + "Z\","
cmdStr = cmdStr + "\"mode-reason\":\"LAPP\","
cmdStr = cmdStr + "\"data\":{\"fmod\":\"" + fmod + "\"}}"
//logWarn("Dyson455_Request",cmdStr)
Dyson455_Request.sendCommand(cmdStr)
end
rule "455/<id>/command SET-STATE fnsp"
when
Item Dyson455_Fnsp received command
then
// Create current TimeStamp as UTC
//simpleDF.setTimeZone(TimeZone.getTimeZone("UTC"))
var String timeStampUtc = simpleDF.format(new java.util.Date())
// Cast fnsp to String representation
val String fnsp = receivedCommand.toString()
// Create JSON for commanding and send it
cmdStr = startStateSetCmdStr + "\"" + timeStampUtc + "Z\","
cmdStr = cmdStr + "\"mode-reason\":\"LAPP\","
cmdStr = cmdStr + "\"data\":{\"fnsp\":\"" + fnsp + "\"}}"
logWarn("DysonFanSpeedChange",fnsp)
Dyson455_Request.sendCommand(cmdStr)
end
rule "455/<id>/command SET-STATE qtar"
when
Item Dyson455_Qtar received command
then
// Create current TimeStamp as UTC
////simpleDF.setTimeZone(TimeZone.getTimeZone("UTC"))
val String timeStampUtc = simpleDF.format(new java.util.Date())
// Cast qtar to String representation
val String qtar = receivedCommand.toString()
// Create JSON for commanding and send it
cmdStr = startStateSetCmdStr + "\"" + timeStampUtc + "Z\","
cmdStr = cmdStr + "\"mode-reason\":\"LAPP\","
cmdStr = cmdStr + "\"data\":{\"qtar\":\""+ qtar + "\"}}"
Dyson455_Request.sendCommand(cmdStr)
end
rule "455/<id>/command SET-STATE oson"
when
Item Dyson455_Oson received command
then
// Create current TimeStamp as UTC
////simpleDF.setTimeZone(TimeZone.getTimeZone("UTC"))
val String timeStampUtc = simpleDF.format(new java.util.Date())
// Cast oson to String representation
var String oson = "OFF"
if (receivedCommand == ON) oson = "ON"
// Create JSON for commanding and send it
cmdStr = startStateSetCmdStr + "\"" + timeStampUtc + "Z\","
cmdStr = cmdStr + "\"mode-reason\":\"LAPP\","
cmdStr = cmdStr + "\"data\":{\"oson\":\"" + oson + "\"}}"
Dyson455_Request.sendCommand(cmdStr)
end
rule "455/<id>/command SET-STATE rhtm"
when
Item Dyson455_Rhtm received command
then
// Create current TimeStamp as UTC
////simpleDF.setTimeZone(TimeZone.getTimeZone("UTC"))
val String timeStampUtc = simpleDF.format(new java.util.Date())
// Cast rhtm and fmod to String representation
var String fmod = "OFF"
switch Dyson455_Fmod.state {
case 2: fmod = "AUTO"
case 1: fmod = "FAN"
}
var String rhtm = "OFF"
if (receivedCommand == ON) rhtm = "ON"
// Create JSON for commanding and send it
cmdStr = startStateSetCmdStr + "\"" + timeStampUtc + "Z\","
cmdStr = cmdStr + "\"mode-reason\":\"LAPP\","
cmdStr = cmdStr + "\"data\":{\"fmod\":\"" + fmod + "\""
cmdStr = cmdStr + "\"rhtm\":\"" + rhtm + "\"}}"
Dyson455_Request.sendCommand(cmdStr)
end
rule "455/<id>/command SET-STATE nmod"
when
Item Dyson455_Nmod received command
then
// Create current TimeStamp as UTC
////simpleDF.setTimeZone(TimeZone.getTimeZone("UTC"))
val String timeStampUtc = simpleDF.format(new java.util.Date())
// Cast nmod to String representation
var String nmod = "OFF"
if (receivedCommand == ON) nmod = "ON"
// Create JSON for commanding and send it
cmdStr = startStateSetCmdStr + "\"" + timeStampUtc + "Z\","
cmdStr = cmdStr + "\"mode-reason\":\"LAPP\","
cmdStr = cmdStr + "\"data\":{\"nmod\":\"" + nmod + "\"}}"
Dyson455_Request.sendCommand(cmdStr)
end
rule "455/<id>/command SET-STATE hmod"
when
Item Dyson455_Hmod received command
then
// Create current TimeStamp as UTC
////simpleDF.setTimeZone(TimeZone.getTimeZone("UTC"))
val String timeStampUtc = simpleDF.format(new java.util.Date())
// Cast hmod to String representation
var String hmod = "OFF"
switch receivedCommand {
case 1: hmod = "HEAT"
}
// Create JSON for commanding and send it
cmdStr = startStateSetCmdStr + "\"" + timeStampUtc + "Z\","
cmdStr = cmdStr + "\"mode-reason\":\"LAPP\","
cmdStr = cmdStr + "\"data\":{\"hmod\":\"" + hmod + "\"}}"
Dyson455_Request.sendCommand(cmdStr)
end
rule "455/<id>/command SET-STATE hmax"
when
Item Dyson455_Hmax received command
then
// Create current TimeStamp as UTC
////simpleDF.setTimeZone(TimeZone.getTimeZone("UTC"))
val String timeStampUtc = simpleDF.format(new java.util.Date())
// Cast hmax to String representation
var double hmax = Float.parseFloat(receivedCommand.toString())
hmax = (hmax * 10) + 2731.5
val int hmax_int = Integer.parseInt(Math::round(hmax).toString())
// Create JSON for commanding and send it
cmdStr = startStateSetCmdStr + "\"" + timeStampUtc + "Z\","
cmdStr = cmdStr + "\"mode-reason\":\"LAPP\","
cmdStr = cmdStr + "\"data\":{\"hmax\":\"" + hmax_int.toString() + "\"}}"
Dyson455_Request.sendCommand(cmdStr)
end
rule "455/<id>/command SET-STATE ffoc"
when
Item Dyson455_Ffoc received command
then
// Create current TimeStamp as UTC
////simpleDF.setTimeZone(TimeZone.getTimeZone("UTC"))
val String timeStampUtc = simpleDF.format(new java.util.Date())
// Cast ffoc to String representation
var String ffoc = "OFF"
if (receivedCommand == ON) ffoc = "ON"
// Create JSON for commanding and send it
cmdStr = startStateSetCmdStr + "\"" + timeStampUtc + "Z\","
cmdStr = cmdStr + "\"mode-reason\":\"LAPP\","
cmdStr = cmdStr + "\"data\":{\"ffoc\":\"" + ffoc + "\"}}"
Dyson455_Request.sendCommand(cmdStr)
end
//#############################################################################