Bridge for Vis Nav#4
Open
daknightuk wants to merge 1 commit into
Open
Conversation
What the command does (the full flow) 1) First run: it asks for local MQTT broker settings When you run: opendyson visnav …it tries to load a saved “VisNav settings” config. If none exists, it prompts: MQTT Server (IP/host or tcp://host:port) MQTT Username (optional) MQTT Password (optional) Those are the details for your local broker (e.g. Mosquitto, EMQX, etc.) — this is the broker Home Assistant is usually subscribed to. 2) It connects to your local MQTT broker It then uses those settings to open a long-running connection to your MQTT broker. If the connection fails (wrong IP, wrong creds, broker down), it re-prompts so you can fix it. Once connected, it stays connected and will auto-reconnect if the connection drops. 3) It logs into Dyson cloud (using existing opendyson flow) It calls: funcs.Login() which uses the same OTP workflow as the normal opendyson login If login fails, it still continues in case there’s a cached token that’s still valid (so you’re not blocked unnecessarily) 4) It pulls your Dyson devices, filters RB03 only It calls: funcs.GetDevices() Then filters to: GetModel() == "RB03" (Vis Nav) So if you have multiple Dysons, it only bridges the robot vacs. 5) It subscribes to Dyson’s IoT MQTT topics for each robot For each RB03 device it switches to IoT mode: cd.SetMode(devices.ModeIoT) Then it subscribes to cloud topics. Ideally it wildcard-subscribes to the whole device namespace: 277/<SERIAL>/# (command/status/fault etc) If the wildcard subscribe isn’t supported (depends on library/device), it falls back to subscribing each topic explicitly: 277/<SERIAL>/status 277/<SERIAL>/fault 277/<SERIAL>/command 6) Every time a message arrives, it forwards it to your local broker When a Dyson cloud message comes in: it logs it as IOT IN it remaps the topic to a clean local topic under Dyson/<SERIAL>/... it publishes the same payload to your local broker it logs it as MQTT OUT So you can literally see bridging working message-by-message. Topic remapping example Cloud: 277/<SERIAL>/status Local: Dyson/<SERIAL>/status Payload stays identical — the cloud JSON is not edited. 7) It also publishes “easy HA” retained topics For status messages (the ones you care about most), it extracts a few key values: newstate / oldstate / state (depending on message type) batteryChargeLevel Then it publishes three extra retained topics: Dyson/<SERIAL>/state → retained JSON summary (displayState + old/new/current + battery) Dyson/<SERIAL>/battery → retained battery percent as a plain string number Dyson/<SERIAL>/state_text → retained plain state string (INACTIVE_CHARGED, etc.) “Retained” matters because when HA restarts, it gets the latest known state immediately, not only after the next vacuum event. 8) (Optional) It publishes Home Assistant MQTT Discovery If you keep the discovery code enabled, it also publishes retained config messages under: homeassistant/sensor/.../config That makes HA auto-create entities for battery/state without you writing YAML. How the stored settings are protected (encryption explanation) Your saved local MQTT broker settings contain credentials, so we don’t store them in plain text. What gets stored MQTT server MQTT username MQTT password How it’s stored (high-level) It uses symmetric encryption with AES-GCM: AES = fast, industry-standard symmetric cipher GCM = authenticated encryption mode (it both encrypts and detects tampering) So it gives you: confidentiality (can’t read it without the key) integrity (if someone edits the encrypted file, decryption fails) What files exist There are two files: Encrypted settings blob (ciphertext) Example name: visnav.enc Encryption key (32 bytes) Example name: visnav.key How the encryption works in practice When saving: Generate (or load) a 32-byte key Create a random nonce (unique per save) AES-GCM encrypts the JSON settings using key + nonce It writes: [nonce][ciphertext] to visnav.enc When loading: Read the key from visnav.key Read visnav.enc Split out nonce and ciphertext AES-GCM decrypts and verifies integrity JSON decode back into settings If either file is missing or tampered with, decryption fails and you’ll be prompted again (or you can use --reset). Why the key is separate This is standard for “lightweight local protection”: It prevents casual reading of credentials from one file. Someone would need both files to decrypt. Important reality check: if an attacker has full access to the same machine/account where both files live, they can potentially grab both and decrypt. This is still useful protection for: accidental leaks (copying config files) someone opening a file and seeing passwords basic at-rest security For stronger protection you’d integrate OS keyrings (Windows Credential Manager, macOS Keychain, Linux Secret Service), but that’s heavier. How a user uses it (real-world steps) Setup once Make sure you already can log in with opendyson: opendyson login (OTP happens here) Run the bridge: opendyson visnav Enter your local broker details (first run only) That’s it. Everyday usage Just run: opendyson visnav It: connects to your broker using saved encrypted settings logs into Dyson using cached token starts forwarding messages continuously Reset settings if you change broker / password opendyson visnav --reset Why it’s useful (the “why”) 1) Makes Dyson cloud data usable locally Dyson’s IoT data lives in their cloud MQTT. Your automations and dashboards usually live locally (Home Assistant, Node-RED). This bridges that gap. 2) Clean, predictable topic structure Everything is under: Dyson/<SERIAL>/... So your HA automations are stable and scalable (multiple robots don’t collide). 3) “Retained” state avoids “unknown” after restarts HA loves retained topics. Your sensors show the last known battery/state immediately after HA reboot. 4) Debuggable Because it prints: what came in from cloud what got published locally …it’s obvious when the bridge is working, when the vacuum is quiet, or if MQTT is failing. 5) Optional auto-entities in HA (Discovery) If enabled, HA automatically discovers and creates your sensors.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What the command does (the full flow)
When you run:
opendyson visnav
…it tries to load a saved “VisNav settings” config. If none exists, it prompts:
MQTT Server (IP/host or tcp://host:port)
MQTT Username (optional)
MQTT Password (optional)
Those are the details for your local broker (e.g. Mosquitto, EMQX, etc.) — this is the broker Home Assistant is usually subscribed to.
It then uses those settings to open a long-running connection to your MQTT broker. If the connection fails (wrong IP, wrong creds, broker down), it re-prompts so you can fix it.
Once connected, it stays connected and will auto-reconnect if the connection drops.
It calls:
funcs.Login() which uses the same OTP workflow as the normal opendyson login
If login fails, it still continues in case there’s a cached token that’s still valid (so you’re not blocked unnecessarily)
It calls:
funcs.GetDevices()
Then filters to:
GetModel() == "RB03" (Vis Nav)
So if you have multiple Dysons, it only bridges the robot vacs.
For each RB03 device it switches to IoT mode:
cd.SetMode(devices.ModeIoT)
Then it subscribes to cloud topics. Ideally it wildcard-subscribes to the whole device namespace:
277//# (command/status/fault etc)
If the wildcard subscribe isn’t supported (depends on library/device), it falls back to subscribing each topic explicitly:
277//status
277//fault
277//command
When a Dyson cloud message comes in:
it logs it as IOT IN
it remaps the topic to a clean local topic under Dyson//...
it publishes the same payload to your local broker
it logs it as MQTT OUT
So you can literally see bridging working message-by-message.
Topic remapping example
Cloud:
277//status
Local:
Dyson//status
Payload stays identical — the cloud JSON is not edited.
For status messages (the ones you care about most), it extracts a few key values:
newstate / oldstate / state (depending on message type)
batteryChargeLevel
Then it publishes three extra retained topics:
Dyson//state → retained JSON summary (displayState + old/new/current + battery)
Dyson//battery → retained battery percent as a plain string number
Dyson//state_text → retained plain state string (INACTIVE_CHARGED, etc.)
“Retained” matters because when HA restarts, it gets the latest known state immediately, not only after the next vacuum event.
If you keep the discovery code enabled, it also publishes retained config messages under:
homeassistant/sensor/.../config
That makes HA auto-create entities for battery/state without you writing YAML.
How the stored settings are protected (encryption explanation)
Your saved local MQTT broker settings contain credentials, so we don’t store them in plain text.
What gets stored
MQTT server
MQTT username
MQTT password
How it’s stored (high-level)
It uses symmetric encryption with AES-GCM:
AES = fast, industry-standard symmetric cipher
GCM = authenticated encryption mode (it both encrypts and detects tampering)
So it gives you:
confidentiality (can’t read it without the key)
integrity (if someone edits the encrypted file, decryption fails)
What files exist
There are two files:
Encrypted settings blob (ciphertext)
Example name: visnav.enc
Encryption key (32 bytes)
Example name: visnav.key
How the encryption works in practice
When saving:
Generate (or load) a 32-byte key
Create a random nonce (unique per save)
AES-GCM encrypts the JSON settings using key + nonce
It writes: [nonce][ciphertext] to visnav.enc
When loading:
Read the key from visnav.key
Read visnav.enc
Split out nonce and ciphertext
AES-GCM decrypts and verifies integrity
JSON decode back into settings
If either file is missing or tampered with, decryption fails and you’ll be prompted again (or you can use --reset).
Why the key is separate
This is standard for “lightweight local protection”:
It prevents casual reading of credentials from one file.
Someone would need both files to decrypt.
Important reality check: if an attacker has full access to the same machine/account where both files live, they can potentially grab both and decrypt. This is still useful protection for:
accidental leaks (copying config files)
someone opening a file and seeing passwords
basic at-rest security
For stronger protection you’d integrate OS keyrings (Windows Credential Manager, macOS Keychain, Linux Secret Service), but that’s heavier.
How a user uses it (real-world steps)
Setup once
Make sure you already can log in with opendyson:
opendyson login
(OTP happens here)
Run the bridge:
opendyson visnav
Enter your local broker details (first run only)
That’s it.
Everyday usage
Just run:
opendyson visnav
It:
connects to your broker using saved encrypted settings
logs into Dyson using cached token
starts forwarding messages continuously
Reset settings if you change broker / password
opendyson visnav --reset
Why it’s useful (the “why”)
Dyson’s IoT data lives in their cloud MQTT. Your automations and dashboards usually live locally (Home Assistant, Node-RED). This bridges that gap.
Everything is under:
Dyson//...
So your HA automations are stable and scalable (multiple robots don’t collide).
HA loves retained topics. Your sensors show the last known battery/state immediately after HA reboot.
Because it prints:
what came in from cloud
what got published locally
…it’s obvious when the bridge is working, when the vacuum is quiet, or if MQTT is failing.
If enabled, HA automatically discovers and creates your sensors.