Skip to content

Bridge for Vis Nav#4

Open
daknightuk wants to merge 1 commit into
libdyson-wg:mainfrom
daknightuk:daknightuk-visnavbridge
Open

Bridge for Vis Nav#4
daknightuk wants to merge 1 commit into
libdyson-wg:mainfrom
daknightuk:daknightuk-visnavbridge

Conversation

@daknightuk

Copy link
Copy Markdown

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.

  1. 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.

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

  1. 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.

  1. 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//# (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

  1. 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//...

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.

  1. 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//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.

  1. (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.

  1. Clean, predictable topic structure

Everything is under:

Dyson//...

So your HA automations are stable and scalable (multiple robots don’t collide).

  1. “Retained” state avoids “unknown” after restarts

HA loves retained topics. Your sensors show the last known battery/state immediately after HA reboot.

  1. 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.

  1. Optional auto-entities in HA (Discovery)

If enabled, HA automatically discovers and creates your sensors.

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant