Dyson Air for Omarchy
Control Dyson purifiers, fans, heaters and humidifiers from the Omarchy bar, through Home Assistant.
<p align="center"> <img src="preview.png" alt="The Dyson Air panel showing mode, speed, a row of mode icons, airflow, a sleep timer, a PM2.5 graph, air quality readings and a collapsed Extras section" width="420"> </p>Not affiliated with, sponsored by, or endorsed by Dyson, the Home Assistant project, or Omarchy.
Requirements
- Omarchy 4 (
schemaVersion: 1plugin API) - Home Assistant with the hass-dyson integration (install through HACS, search "Dyson")
secret-tool(libsecret) with a running keyring daemon. Omarchy ships gnome-keyring.
No Python, no vendored libraries, no helper process. One REST poll is shared across every widget on the bar.
Install
omarchy plugin add https://github.com/AllStars101-sudo/omarchy-dyson.git --enable
omarchy bar put io.github.allstars101-sudo.dyson-air
Click the fan icon and enter your Home Assistant address and a long-lived access token (Profile → Security → Long-lived access tokens → Create token). The token goes to your system keyring, never to a config file.
Include the scheme in the address. An address typed without one is assumed to be
https, and a default Home Assistant serves plain http on port 8123.
Remove
omarchy bar remove io.github.allstars101-sudo.dyson-air
omarchy plugin remove io.github.allstars101-sudo.dyson-air
secret-tool clear service io.github.allstars101-sudo.dyson-air origin <your-ha-url>
rm -rf ~/.config/omarchy/io.github.allstars101-sudo.dyson-air
Removing the plugin does not remove your token from the keyring. The
secret-tool clear line does.
Controls
| Where | Action |
|---|---|
| Bar, left click | Open the panel (or settings, if not yet connected) |
| Bar, middle click | Toggle power |
| Bar, scroll | Speed up / down |
| Panel | Off / Fan / Heat, target temperature, humidity, speed |
| Panel | Focused / diffused airflow, front / back direction |
| Panel | Oscillation, sweep width, tilt |
| Panel | Night mode, auto mode, sleep timer |
| Panel | Heating mode, water hardness |
| Panel | PM2.5 graph, air quality readings, filter life |
| Panel | Extras: faults, filters, connection, schedule, continuous monitoring, firmware auto-update, filter reset |
Every one of those is drawn only when the device has it. Nothing is greyed out; a control that cannot act is not there. For the same reason, everything that shapes moving air collapses while the fan is off — power, the readings and the set-once settings in Extras are what remain.
The panel does not scroll. A bar dropdown gives no hint that it could, so the controls a device supports have to fit: the frequent ones are icons with tooltips, each labelled control shares its line with its label, and the read-only diagnostics fold into Extras, which opens itself when a fault is active or a filter is due.
Omarchy's bar hotkey (Super+Ctrl+N for the Nth panel in a section) opens the panel, not the settings overlay.
Over IPC, for keybindings:
omarchy-shell dyson-air toggle # open/close the panel
omarchy-shell dyson-air power # toggle power
omarchy-shell dyson-air settings
omarchy-shell dyson-air devices
Settings
| Key | Default | Meaning |
|---|---|---|
barMetric |
Fan speed |
Bar number: Fan speed, PM2.5, or None |
historyHours |
24 |
Graph window |
pollSeconds |
10 |
Poll interval while every panel is closed |
staleSeconds |
300 |
Silence before the device counts as stale |
autoReconnect |
true |
Press the reconnect button when stale |
An open panel polls every 2s regardless. PM2.5 past WHO's 2021 24-hour guideline of 15 µg/m³ is drawn in the theme's urgent colour; nothing else is coloured by state.
That threshold is deliberately not the 35 µg/m³ the US EPA uses for 24 hours — WHO lists 35 as Interim Target 1, a staging post for heavily polluted regions. A home rarely reaches it, so a warning set there would never fire. 15 is reachable indoors from cooking, a candle or a vacuum pass, which is when the readout is worth looking at.
Several devices
Add the widget once per device and assign each one in Settings → Devices. Widgets left on Automatic follow the first Dyson found.
Assignment lives in Settings because two widgets of one plugin share a module name, so only their position on the bar tells them apart, and the settings overlay is the only surface that can see positions.
How it decides what to show
Nothing is hardcoded per model. hass-dyson creates entities conditionally from
each device's capability list, so entity presence is the capability map and the
panel renders only what it finds.
Two matching rules:
- Companion switches match on an exact suffix, never a substring. A substring
match resolves "auto mode" to
switch.<device>_firmware_auto_update. - Air quality sensors match on
device_class, not name. Older models call PM2.5particulates, newer onespm25. Formaldehyde is the exception, since Home Assistant has no HCHO device class. - Fault sensors match on the
fault_prefix, because which subsystems a model reports varies and a fixed list would miss the ones nobody here has seen.
Option lists are read off the entity too. The sweep-width chips show 45/90/180/ 350 or 15/40/70 depending on the model's capability, and Breeze only where the device offers it — none of that is written down in this plugin.
Staleness
Home Assistant keeps serving the last state it saw after a device's MQTT session dies. Nothing errors. Suspending the machine Home Assistant runs on does this.
The newest timestamp across all of a device's entities is used as a heartbeat.
The fan entity alone is not enough: its last_updated only moves when the fan
changes, so an untouched fan looks identical to a dead session. After
staleSeconds the bar icon dims and drops its number, and with autoReconnect
on, the integration's reconnect button is pressed at most once every two
minutes.
Model support
Verified against real hardware
| Model | Type | What was exercised |
|---|---|---|
| Pure Hot+Cool Link (HP02) | 455 | Heat, speed, focused/diffused airflow, oscillation, night mode, continuous monitoring, air quality, graph, faults, staleness, settings |
Untested, should work
Each has a mock device in tests/fixtures/synthetic.js built from hass-dyson's
entity definitions. No physical unit has been connected.
| Model | Type | What the mock covers |
|---|---|---|
| Purifier Cool Formaldehyde (TP09) | 438E | Cool-only, auto as a switch, formaldehyde, PM10/NO₂, oscillation angle, both filters |
| Purifier Hot+Cool (HP07) | 527K | Heat via a climate entity, heating-mode select |
| Pure Humidify+Cool (PH01) | 358 | Humidifier entity, humidity target, water hardness, no heat |
| Purifier Big+Quiet (BP) | 664 | A non-ten-speed dial (12.5% steps, so 8 speeds), and a vertical tilt axis alongside the sweep |
| A minimal Link-era device | n/a | Near-empty: every optional control stays hidden rather than rendering dead |
Recognised but not modelled
These codes resolve to a product name but have no fixture: 358E, 358K, 438, 438K, 469, 475, 520, 527, 527E.
A code missing from every table still works. It shows as Dyson <code>, and
controls come from the entities Home Assistant exposes.
If your device misbehaves, open an issue with:
curl -s -H "Authorization: Bearer $TOKEN" "$HA_URL/api/states" \
| jq '[.[] | select(.entity_id|test("dyson"))]'
Air treatment only. Dyson robot vacuums and lights are out of scope.
Contributing
A states dump from a model nobody here owns is the most useful contribution.
npm run checkruns what CI runs. It should pass before you open a pull request.- Logic goes in
Dyson.js,View.js,Config.jsorOrigin.js, not in the QML. Those four are the only files testable outside a running Omarchy shell, and the coverage gate holds them at 100%. - New device support means a fixture, not a special case. A branch on a product code usually means the capability should be discovered from an entity instead.
- Write tests that would fail against a wrong implementation. Fixtures like
nearMissandprefixSiblingsexist to stress the matchers rather than agree with them. - Run
npm run lint:strictif you have Omarchy. It catches QML problems CI cannot see, since a CI runner has no Quickshell to resolve types against.
Bug reports are easiest to act on with what you expected, what happened, and the tooltip text from the bar icon, which names the failure.
Development
npm test
npm run coverage # tests + the 100% gate
npm run lint # manifest, cross-file symbols, QML syntax
npm run lint:strict # the above plus full qmllint semantics (needs Omarchy)
npm run check # all three, as CI runs them
No dependencies. package.json exists only for the scripts.
What is and is not covered
The logic and view layers are held at 100% line, branch and function coverage,
enforced per file by scripts/check-coverage.js:
| File | What it holds |
|---|---|
Dyson.js |
Entity discovery, capability mapping, speed, air quality, model names, history, liveness, reconnect policy |
View.js |
Every view decision: bar label, status text, which rows exist, reading list, control options, error messages |
Config.js |
Config parsing, clamping, serialization |
Origin.js |
URL normalisation, the identity the keyring scopes tokens by |
The QML is not covered. Panel.qml, Settings.qml, Service.qml and
CredentialManager.qml hold polling, HTTP, the keyring, IPC and rendering, none
of which run outside a live Omarchy shell. They get scripts/check-qml.sh
instead: the manifest and its entry points, cross-file symbol references, and
QML syntax. --strict adds full qmllint semantics but needs the Omarchy shell
to resolve qs.Ui, so CI runs syntax mode. Beyond that the QML is verified by
hand against real hardware.
Fixtures
tests/fixtures/hp02-455.json is a real /api/states dump from a Dyson Pure
Hot+Cool Link. tests/fixtures/synthetic.js holds hand-built dumps for models
not available here. They constrain the code but are not evidence that a real
TP09 or PH01 behaves this way.
nearMiss carries names that extend a wanted suffix, such as
_night_mode_schedule. prefixSiblings holds two devices whose slugs overlap.
unavailable holds the non-numeric states a reconnecting device emits.
bigQuiet uses a non-ten-speed dial.
After editing, restart the shell rather than trusting hot reload:
omarchy restart shell
Licence
MIT. Portions derived from konradk/hass. See
THIRD_PARTY_NOTICES.md.