Repository navigation
Releases: disruptivepatternmaterial/atmoph-win-2-remote
Release list
v0.6.0 — survives what a real window actually does
A defect-hunting release. Eight faults were found by reviewing every read and
write path against what a window does rather than what the fake peripheral
did, and each fix is held by a test that was checked by reverting the fix and
watching it fail.
Nothing here changes how the integration is set up or what entities it
creates, so upgrading is a HACS update and a restart.
Fixed
One odd value no longer costs you every entity. A refresh read six
characteristics with no tolerance for any of them, so a single empty string,
non-UTF-8 byte or truncated document raised out of the refresh and left the
entry with no entities at all — recoverable only by reloading, and then only
until it happened again. A value that will not decode now sets that one field
to unknown and the refresh carries on.
A slow display is no longer turned back off. Confirmation was allowed
three seconds and the retry waited two, so a display taking longer than three
seconds to report got a second toggle at around five — far outside the second
or so in which a window discards a duplicate. The result was the display
ending up in the state you did not ask for, with nothing raised. Each attempt
now re-reads before toggling, which is what tells a late confirmation apart
from a dropped write.
A renamed window keeps its entry and its history (#24). Renaming a
window in the Atmoph app changed the only thing discovery can match on, so it
read as a new window: a second entry appeared and the original was stranded on
a name nothing advertises, reachable only by deleting it and losing the
window's history.
A disconnect notice can no longer take down a healthy connection. BLE
addresses rotate every few tens of seconds, so a notice from a replaced link
arriving after its successor is up is ordinary. Acting on it marked a working
entry unavailable.
A truncated settings read no longer leaves stale values on screen. The
settings document was merged into what was already known, so a document
missing a key kept showing the old value for it indefinitely. Reads now
replace the document, since a read is the window's whole answer rather than a
delta.
Failures now say what to do about them. Every timeout, wrong-window and
connection error reached you as a stack trace in the log and an opaque failure
in the interface. They now name the likely cause — the display asleep, out of
range, or held by the Atmoph phone app. A failed setting write also no longer
reports that display power was not confirmed, which was the wrong operation
and the wrong advice.
A control the window cannot serve withdraws itself rather than accepting a
value and silently doing nothing. This covers the media player's volume as
well as any level the window reports no range for.
Smaller things: a window reporting an inverted or zero-width range is
handled rather than clamped the wrong way; notifications no longer push the
scheduled read out indefinitely; a window that has stopped advertising its
name is recovered without skipping the identity check; and a disconnect
arriving after Home Assistant has stopped no longer raises from a thread
nobody is watching.
Diagnostics
Settings → Devices & services → Atmoph Window → Download diagnostics now
carries the live GATT table and the window's own quick-settings document with
the bounds it reports, redacted of the advertised name, address and device
UUID. That is the one artifact nobody without the hardware can produce, and it
is safe to attach to an issue as-is.
Not verified
No release of this integration has run against an AW102. The suite is 266
tests and it found everything above, but every one of those defects was found
after the fake peripheral was taught to behave that way — so the fake encodes
what this project believes about the hardware and cannot contradict a belief
that is wrong. Hardware validation is #5; the view-ID sensor rests on the
weakest evidence of anything here and is #14.
Full Changelog: v0.5.0...v0.6.0
v0.5.0 — media player, and entities that admit when they don't know
Six issues closed. The headline is a media player entity, which gets you
the built-in Media Control card and voice-assistant support without a custom
frontend.
Added
A media player per window. The current view as cover art, its title, and
where it was filmed, with transport and volume. It presents the same state the
other entities do, so nothing you already built stops working.
Volume is scaled from the window's own reported range rather than treated as a
percentage — the range is per-device and never 0-100, so the ends have to land
exactly or the quietest and loudest settings become unreachable. Stepping moves
by one unit the window recognises, not a tenth of the range, because a range
can be as coarse as six values.
Play and pause are deliberately absent: the protocol has no such command, and
a card offering a button that does nothing cannot be told from a fault. Home
Assistant hides media attributes while a player is off, so the card looks
empty until the window is awake.
The live GATT table in the diagnostics download. Every service and
characteristic with its declared properties, which is the one thing about a
window that nobody without one can obtain. Properties are reported exactly as
declared, including on the two characteristics known to advertise write and
discard it — correcting them would hide the finding. Safe to attach to an
issue: identifying fields stay redacted, and a GATT table describes a model
rather than a unit.
Changed
Entities go unavailable when the Bluetooth link drops, instead of serving
the last value they were told for up to a minute. With a toggle-only display
that matters more than it sounds: an automation acting on a stale reading
inverts the command it then sends.
State is published once per poll. The client was publishing from refresh
and set_power as well as from a notification, and the coordinator publishes
what those return — so every entity was written twice per poll and the refresh
timer was being reset out of band.
set_setting re-reads before refusing. A key missing from the settings
document was treated as a key the firmware lacks, when it is equally a
notification that went astray. Only one of those is recoverable, and
re-reading tells them apart.
Not implementable, now recorded as such
View selection is closed
(#7).
No characteristic selects a view and none of the fifteen control tokens does
either; the one that looks like it should discards writes. FW and BW are
the whole navigation surface.
Testing
239 tests, up from 150. The larger part of that is not new coverage but
coverage that now runs: tools/selftest.py held 119 working assertions that
no CI job executed, leaving 1,932 lines of diagnostic tooling unverified
on every push. Those checks are now 67 pytest functions in the fast suite, and
the hand-rolled harness around them is deleted.
Every new behaviour in this release was verified by mutating the code it
targets and confirming the test fails.
Still unverified
Bluetooth is faked throughout, so timing-sensitive behaviour remains proven
against a model of the window rather than a window. Per-claim hardware
validation is
#5,
which this release makes materially easier: the GATT table it asks for now
arrives from Download diagnostics instead of a terminal.
v0.4.2
A Japanese view title or location split across two BLE packets no longer
disappears. Atmoph's titles and locations are Japanese, a notification can be
split at any byte including the middle of a character, and decoding each
packet on its own raised on both halves — so the update the window pushed was
dropped and the sensor sat on the previous view until the next poll, up to a
minute later. The decoder now holds the incomplete character until the rest of
it arrives.
Nothing to do to upgrade, and no configuration changes.
What else changed
Only tests, but they are the reason the fix above exists. A review found the
suite could be deleted through in places: stubbing out the notification
dispatcher — the whole of what makes this integration local_push — left all
80 tests passing.
The fakes now model the window docs/PROTOCOL.md actually describes rather
than a polite one, which turned up the split-character bug and several
untested requirements:
- Notifications are delivered through a bleak-shaped sender object, and every
notifying characteristic is followed to the entity state it lands in. - Quick-setting bounds come from the device and reach 25, not 10, and all nine
settings the app knows are reported, so three number entities and two
switches are no longer registered-but-unavailable throughout the suite. - A setting write is echoed back and clamped, so the optimistic update is
checked against what the device actually accepted. - The display's habit of silently dropping a toggle sent within about a second
of one that took effect is modelled, and the retry pause has to outlast it. - A nameless advertisement, and passive versus active scanning, are both
modelled: the name rides in the scan response, so the same window is seen
named and nameless seconds apart.
Statement coverage of the integration is 100%, up from 96.7%, and 34 deliberate
mutations of the production code are each caught by a named test.
v0.4.1 — Japanese view titles no longer get dropped
Install this if your windows show Japanese view titles or locations. They
were being silently discarded whenever the text happened to straddle a
Bluetooth packet boundary.
Fixed
A notification boundary falls wherever the window's MTU puts it, with no regard
for character boundaries, so a multi-byte character arrives in two pieces. The
JSON reader decoded each packet on its own, which raises on both halves,
and the client discards a payload it cannot decode. The update was lost, not
delayed.
Atmoph is a Japanese company and their view titles and locations are Japanese
text, so for the notification carrying them this is the ordinary case rather
than an edge one. Reproduced with a title split one byte into a three-byte
character.
Decoding is now incremental: the decoder holds a partial character until the
rest of it arrives. Genuinely malformed bytes still raise, and now also clear
the buffer, because a stream that has lost sync makes everything already in it
suspect.
Documentation
Records that this integration does not request an MTU where the app asks
for 128. A bleak client cannot portably, because BlueZ and CoreBluetooth
negotiate on the host's behalf — which is exactly why nothing may assume a
payload size, and why the incremental decode above is load-bearing rather than
defensive.
Testing
80 tests: 28 protocol, 52 Home Assistant layer. Two new protocol tests cover a
character split across packets and recovery after undecodable bytes.
An automated review of the test suite found that deleting the notification
dispatcher entirely leaves every test passing, so the push path is materially
untested even though this integration declares local_push. That is being
addressed next and is the reason this release is not described as verified.
v0.4.0 — verifies which window it is talking to
A security review of v0.3.1 found that nothing confirmed the connected window
was the one an entry was configured for. This release fixes that, and hardens
the workflows that publish.
Fixed: commands could reach the wrong window
Discovery matches on the BLE advertised name and resolves it to whichever
window is answering loudest. That is all an advertisement supports, but it is
not identity — two windows can carry the same name, and the loudest one is not
necessarily the one the entry was set up for.
Nothing checked. So a command sent to one window's entity could be delivered to
another, and that other window's view and power could be written into the first
one's entities.
The device UUID is now read and compared before the first write. On a
mismatch the connection is dropped and the entry reports as not ready, and the
state callback is attached only after the check passes, so a window that fails
it cannot publish anything on the way out.
What you may notice: if an entry now fails to load with a message about an
unexpected device UUID, it was previously talking to a window other than the
one it was configured for. Remove and re-add that entry.
Fixed: the test peripheral modelled an impossible window
The fake derived its device UUID from its BLE address. The address is precisely
what rotates, and the UUID is read over GATT precisely because it does not, so
the fake described a device that cannot exist — and a reconnection after an
address rotation would have looked like reaching a different window. Now keyed
on the window, with a test that a rotated address is still the same window.
Hardened
The release workflow interpolated a user-supplied ref directly into a shell and
checked it out unqualified. Git permits tag names containing shell
metacharacters, and an unqualified name resolves to a branch before a tag, so
a branch named after a release could have supplied the asset published under
that release — an asset HACS then distributes. The ref must now match vX.Y.Z,
is resolved to an immutable commit before checkout, and reaches the shell only
through the environment.
The wiki job holds contents: write and ran its publisher from a mutable
branch; it is now pinned to a commit. The validation workflow is explicitly
read-only.
Documentation
Two passages in docs/PROTOCOL.md followed the wording and structure of the
unlicensed third-party analysis too closely to be defensible as reporting
facts, and have been rewritten. The README's claim about that repository is now
precise about what was and was not taken.
Testing
78 tests: 26 protocol, 52 Home Assistant layer. The identity-enforcement and
rotation regressions were each confirmed to fail against v0.3.1.
Bluetooth is still faked throughout, so timing-sensitive behaviour remains
unproven against real hardware —
#5.
v0.3.1 — hardens the v0.3.0 identity migration
Install this rather than v0.3.0. A review of that release found three ways
its identity migration could leave an install worse than it started. Each is
now covered by a test that fails against v0.3.0.
If you are upgrading from v0.2.x, read the
v0.3.0 notes
for what the migration does. Entity ids and history are preserved.
Fixed
A unique-id conflict stranded the entry permanently. Home Assistant refuses
a unique id another entity of the same platform already holds, and that check
spans every config entry while the rewrite walked only one. A conflict part-way
through left some entities moved and some not, and raised a bare ValueError
out of setup — so the entry showed SETUP_ERROR with no message and every
retry failed identically. Targets are now checked before any move is applied,
and a blocked rekey leaves the entry on its advertised name, which is
recoverable.
A second entry could adopt a UUID another entry already held. Nothing
raised, because there were no entities yet to collide with, so it loaded with
the UUID stored and then had every entity rejected at creation. The result was
a loaded config entry with no entities and no device — invisible on the device
page, unreachable by a service call, and holding a BLE connection to a window
the first entry already owned. Adopting a claimed UUID now fails the entry with
a clear message instead.
A crash mid-adoption was unrepairable. Adoption writes to two stores that
flush independently: config entries after a second, the registries after ten
or, during startup, a hundred and eighty. A crash in between left the entry
claiming a UUID while the registries still held name-keyed rows, and because
the rekey only ran when the UUID was unset, nothing ever repaired it — the next
run built a second set of entities beside the orphaned ones. The registry side
is now reconciled against the stored key on every setup, and is a no-op when
the two already agree.
Also fixed
An emptied device registry row is now removed rather than left on the device
page as a second Atmoph device with nothing on it — Home Assistant does not
prune it, because it still references a live config entry. Device lookups are
scoped to the config entry, which resolves a deprecation that stops working in
Home Assistant 2027.8 and stops refusing renames that would have succeeded. The
config flow no longer raises if a window stops advertising between the form
being rendered and submitted. A disconnect that itself raises no longer
replaces the update error being reported.
Documentation
Corrected five places where the docs had drifted from the code: both documents
still described the diagnostic-port question as open when a full 1-65535 scan
had closed it, the second-service section contradicted itself on how much of it
the app knows about, the connection-initialization section claimed Home
Assistant follows the app's ordering when it deliberately subscribes before
reading, and send_command was documented against a table of tokens rather
than the argument it actually accepts.
Testing
77 tests pass: 26 protocol, 51 Home Assistant layer. The four new identity
regressions were each confirmed to fail against v0.3.0 rather than assumed to.
All Bluetooth remains faked, so timing-sensitive behaviour is still unproven
against real hardware —
#5.
v0.3.0 — entity identity moves to the device UUID
Superseded by v0.3.1. Install that instead — a review of this release found three ways the migration below could leave an install worse than it started.
These notes were missing when this release was cut, and are backfilled here for the record.
Entity identity changed, and migration is automatic
Entity and device identity moved from the BLE advertised name to the GATT
device UUID. On the first connection after upgrading, entity unique ids and the
device registry row are rewritten in place, so entity ids, history, names,
and area assignments are preserved — dashboards and automations keep working.
Why
Identity previously came from the advertised name, while device_info
preferred the device UUID and fell back to the name. A window that had not yet
reported its UUID when entities were created got a name-keyed device row that
silently became UUID-keyed on a later run. Home Assistant treats that as a
different device: duplicate devices, and history stranded on the old one.
The two roles are now split by what each identifier can actually do. Discovery
and the config entry's unique id stay on the advertised name, because
rediscovery has only an advertisement to match against and reading the UUID
needs a connection. Entities and the device registry key on the UUID, adopted
once and never replaced — a different UUID means a different window.
Known problems, fixed in v0.3.1
- A unique-id conflict part-way through the rewrite left the entry stranded
across two identity namespaces, failing setup with no message and no recovery. - A second entry could adopt a UUID another already held, then load with no
entities at all. - The entry and the registries are written to stores that flush seconds apart;
a crash in between left them disagreeing with nothing to repair it.
v0.2.1 — works on Home Assistant 2026.9
Install this if the integration stopped working after updating Home
Assistant. Nothing was wrong with the integration's code — it never got the
chance to run.
Fixed
The manifest required bleak-retry-connector==4.6.3. Home Assistant 2026.9.0
ships 4.7.0. Home Assistant resolves an integration's requirements before
importing it, so setup failed at that stage and no message from this
integration appeared in the log to explain why.
The constraint is now a floor, >=4.6.3. That is the correct shape for this
dependency: bleak-retry-connector is a Home Assistant core dependency, so
core decides which version is installed, and an integration only needs to
declare the oldest release it works against.
Verified against Home Assistant 2026.9.0, which ships bleak-retry-connector
4.7.0, bleak 3.0.2 and habluetooth 6.26.11: all 24 protocol tests and all
39 Home Assistant layer tests pass unchanged.
Fixes #11,
which was filed a week ago predicting exactly this.
No functional changes
Same entities, same services, same protocol as
v0.2.0.
v0.2.0 — services and view ID
Reaches the parts of the recovered BLE protocol that no entity could express.
What is new
Two services. The entity platforms could not carry everything the protocol
supports, so rather than invent an entity per token there are now two services,
both targeting a specific window so a household with several is unambiguous.
atmoph_window.send_commandsends any one of the 15 implemented control
tokens. This is howdouble_tapandsearchare reached — they have no
button, because a double tap and a jump to the search screen depend on what
the window is already showing, and search leads to a text field this
integration deliberately cannot fill.atmoph_window.set_settingwrites any one of the nine quick-menu settings,
includingSoundscapeLayerandCurrentDecoration, which have no entity
because they are catalogue indexes rather than magnitudes and a slider would
misrepresent them.
Both validate against the protocol module rather than trusting the service
schema, so a YAML automation cannot push an unverified token or key at the
window; either raises a readable error instead. set_setting checks a level
against the bounds the window itself reported, never a hardcoded range.
A view-ID sensor. A diagnostic sensor carrying the Atmoph catalogue id of
the view on screen, with the render revision as an attribute. View titles repeat
across the catalogue and change with the app's language, so they are unreliable
to match on in an automation; the id is not.
What is verified and what is not
This project labels every protocol claim by evidence tier, and these two
features sit in different tiers.
| Capability | Evidence |
|---|---|
send_command tokens |
App-verified. Every token comes from the ControlCommand enum in our own decompilation of Atmoph app 2.3.4. S, FW, BW, and C are additionally confirmed from live BLE traffic on Atmoph hardware. |
set_setting keys and the bare-value write format |
App-verified. Both come from QuickMenuSetting and the app's own write path. |
| Per-device bounds on a level | Reported. That real bounds run wider than 0–10 is a third-party hardware observation, which is exactly why the integration reads them from the device instead of assuming. |
| The view-ID sensor | Unverified third-party observation. It may not work on your window. |
On that last row, plainly: the characteristic 03cffbfe-… is declared in the
app but the app never binds it, so our decompilation says nothing about how a
window responds to a read. The claims that a window answers at all, and that
the value is <view-id>/<revision>, come from a third party working on a
Window Yo — not a Window 2, and not confirmed by us.
So it is built to cost nothing when wrong. The subscription may be refused, the
read may fail, and the first failure stops the client asking again for that
connection. A window that does not implement it simply gets no view-ID sensor
rather than one stuck unavailable, and setup, polling, and the existing
current_view sensor are unaffected either way. If the entity does not appear
after upgrading, your window does not expose the characteristic.
Nothing in this release has been checked against an Atmoph Window 2 (AW102) by
this project. See #5 for the hardware validation checklist and #14 for what
specifically needs measuring on the view-ID characteristic.
Also worth knowing
set_setting refuses a key the window has not reported yet, because the write
format gives no way to tell a boolean from a level and writing the wrong type
to this device is worse than declining. In practice the window reports all nine
on connect. Tracked in #15.
Not implemented and deliberately so: text entry, which needs an AES key
hardcoded in the Atmoph APK that this repository will not publish; and zoom,
which maps poorly onto any Home Assistant entity.
Install: HACS installs from the atmoph_window.zip asset below.
Full protocol notes: docs/PROTOCOL.md
v0.1.1 — fixes a permanently failed first install
Upgrade if you installed v0.1.0. That version could leave the integration
permanently broken after a failed first connection, and a window that is asleep
or out of Bluetooth range is the ordinary first-install case.
Fixed
A failed first connection never retried. Setup registered its Bluetooth
callback for automatic unload and unregistered it by hand on failure. Home
Assistant already runs unload callbacks when setup fails, so the second
unregister raised out of a finally and replaced ConfigEntryNotReady. The
entry landed in SETUP_ERROR and stayed there instead of retrying.
Unloading did not stop the coordinator. async_shutdown never called its
parent, so the coordinator was not marked shut down and its debouncer kept
running. An in-flight refresh could reopen the BLE connection that unload had
just closed.
Moving a brightness or volume slider made it unavailable. Levels are
reported as an object carrying min/max/value but written as a bare
number. The optimistic update stored the bare number, erasing the bounds, so
the entity went unavailable until the next poll 60 seconds later.
Also in this release
An out-of-range number now raises a translated ServiceValidationError rather
than a bare ValueError, so the UI shows something meaningful. Entity icons
moved to icons.json. ConfigEntry is parameterised across the platforms. The
reconnect task is scoped to the config entry so it cannot outlive it.
Testing
A Home Assistant layer test suite was added — 20 tests covering both config
flow paths, setup and unload, the retry path when the window is out of range,
reconnection across a BLE address rotation, the display switch's
read-before-toggle path, and diagnostics redaction. Three of them were
confirmed to fail against the pre-fix code.
It runs as its own CI job, because the fast protocol suite asserts Home
Assistant is absent from sys.modules and these tests need the opposite.
Still unverified
All Bluetooth in the tests is faked, so toggle timing and address rotation are
proven against a model of the window rather than a window. Per-claim hardware
validation is #5.