Integrate Dyson Pure Cool Link

Sorry for not chiming in before, but my Dyson (Camping) setup has been out of reach since August. :slight_smile:
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.

  1. Not 100% sure, but I think one has to do the 2FA Dyson phone app login at least once.
  2. 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
  3. 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
//#############################################################################
1 Like