v0.10.0 (Remotes Have Been Buffed) - #103
Merged
Merged
Conversation
…ngs rework, pin storage, trigger dialog aims at any Remote Signpost 3 replaces the old ADOPT flow with a USE fork across all four surfaces (Sniffer, Clipper, Plucker, Mirror): picking a signal now offers USE (bind it straight to an existing device or trigger) beside the prior create-new path, with ghost tiles standing in for the old Add buttons and supporting file-drop creation on both Add dialogs. Per-kind source tabs (Manual/Closet/Sniffer/Clipper/Plucker/Remotes) are wired live end to end on both the Add Device and Add Trigger Remote dialogs, so every source surfaces its own picker instead of a generic list. The device settings dialog was reworked (header collapse-to-active chips, Emitters:/Receivers: header chips, duplicate dialog's footer receiver-chip picker, the mirror-door mint buttons wired live with a backend-issued pin prompt on first mint) alongside eager HA device registration for named remotes as soon as they're created, USE-as- Remote creation from every source, and combined/enabled/disabled trigger counts on remote cards. Pin storage lands gated behind PINNING_UI_ENABLED -- built and tested dark, no user-facing surface turned on yet. The trigger create dialog now aims at any Remote: a REMOTE picker (HAIR Triggers drawer or any named TriggerRemote, "+ New Remote" last) replaces the old drawer-only assumption, edit mode shows the owning remote as a fixed label, and the receiver picker only renders when the effective target is the drawer -- a named remote's own receiver_scope covers it instead. The trigger popover now names each row's owning remote. Also folds in the signpost's punch-list pass: dropped noise-filtering of non-sniffed unknown devices, fixed two stray "adopt" locale strings, remote-card reactivity and the redundant trigger-count line (overview cards and the HAIR Triggers drawer's own), the panel header wordmark removal, a locale first-paint race on language switch, the Add-dialog form-clarity rework (SOURCE/DEVICE/EMITTERS sections, live Name-required treatment, static Create button, top-anchored geometry), and the double-scrollbar fix in the SOURCE picker that followed it.
Derivation, retransmit, echo defense (anchored single-use ticket, post-send guard, loop breaker, coalescing), pin surfaces, header pin layout, punch items 9-13.
A matrix Remote is the hear side of the lattice a matrix Device already sends from. It carries its own copy of the wig's climate matrix, watches the air for those states, and reports what it heard: a bloom on the cell as the frame lands, a gold rest ring that stays on it, a slim readout on the card, and a LAST HEARD row underneath. All four read from one stored fact on the remote, so a reload comes back already filled instead of blank until the next press. Hearing and browsing stay separate on purpose. Browsing is what the user clicked and fills blue; hearing is what came off the handset and rings gold. Neither disturbs the other, so the card can show what just happened while the user reads somewhere else in the lattice. Mode, fan and swing rings are dimension level; the temperature ring is scoped to its branch, because the same tile is a different command under a different combination. Any cell can become a trigger, through three doors that share one dialog: the LAST HEARD row, the action bar on a cell that was heard, and the action bar on a cell that never has been. The third is only possible because the card renders live from the first paint, so a brand new Remote can be given its first trigger straight off the lattice. The minted row wears the cold blue STATE chip, and fires, pins, renames and deletes like any other trigger. Pinning meets the matrix at the coordinates. When a matrix Remote hears a cell and is pinned to a matrix Device, the same coordinates resolve in that device's own lattice and go out through the existing retransmit dispatcher, with its breaker, its coalescing and its echo ticket unchanged. Flat triggers on the same remote keep the path they always had. ONE IDENTITY FORM, and it is the wire form. Everything that matches a frame now hashes the canonical Pronto: the listener's cell index, the triggers minted off a cell, and the commands adopted from a wig. A state heard off the air and the same state read out of a file resolve to the same identity, which is what lets a wig adopted device recognise its own undecoded commands. State heard is its own event and its own device trigger row, one per matrix Remote, filtered on the remote id so renaming never strands an automation. Automations template on the decoded dimensions directly. The card itself was extracted rather than copied: one component, two moods, consumed by the device page in send mode and the remote page in hear mode. The trigger row's anatomy moved to shared styles for the same reason, since the LAST HEARD row wears it too. Punch list items 14 through 23 ride along. The ghost tile becomes legible and then steps back into a faded grey with the full format list restored. The Mirror's filter header becomes four fixed pills whatever the size of the house, with the emitter list behind a dropdown. The settings dialogs get a seam at every section boundary on every device kind and one footer shape, Delete apart from the constructive pair. The trigger dialog's Create button joins every other Create in green. A mirror door pin now shows on both objects without a refresh. The two detail headers align their action glyphs by giving both buttons the same square box. And a seeded branch no longer pretends to be a selection. Ten locales for every new string. Green in both venvs, ruff, tsc, build, and locale parity.
A code HAIR knows only from a file does not match its own capture over a
real air path. Twenty presses of one Mitsubishi cell through a
microsecond-accurate ESP32 transmitter produced twenty distinct byte
hashes and not one of them was the code that was sent; short flat codes
hash stably but on a transmitter-specific wrong value, and ESPHome and
Broadlink each land on their own. The bench_rx injector reproduces the
file identity exactly, which is the control: the miss is the air path,
not the identity code. Marks come back short and spaces come back long
(mean mark ratio 0.83 to 0.95), the photodiode AGC signature, with
per-edge excursions from 0.71 to 1.27 -- more than enough to flip an
individual short/long decision.
THE TIER RULE. Every capture and every file-sourced record gets a
NORMALIZED FINGERPRINT: canonical-strip the trailing gap, reduce to the
first frame, divide marks by the code's own median mark and spaces by
its own median space, split each into two levels at their widest
relative gap (1.20x or more, so a run with no real separation stays one
level), and hash the level sequence with the edge count. It is the
LOWEST of four tiers, consulted only after decoded, (fingerprint, byte
hash) and byte hash have all missed, and only when the capture decoded
as nothing. A value claimed by two genuinely different waveforms is
dropped rather than won by whichever record was indexed last (13 of
2,354 across the bench closet), because naming the wrong state is worse
than naming none.
The levels are FOUND, not thresholded: applying the existing short/long
threshold to the normalized values collapses all sixteen ACER RC-17DE0
codes onto one value, where splitting at each code's own widest gap
gives 16 of 16 and 34 of 34 on a 64-cell Mitsubishi lattice.
ONE STRIP, SHARED. canonical_edges drops trailing zeros then a trailing
space, leaving every edge list ending on a mark. The same code was
measured presenting as 67 edges from the file path and 68 from the
capture path, which is enough on its own to break any identity that
counts edges. canonical_pronto routes through it too.
WHOSE BYTES NEVER CAME THROUGH A RECEIVER. Receiver-learned records
never gain the tier: their existing tiers already work, and handing them
this one would re-collapse the sibling buttons the byte hash exists to
separate (measured: 53 buttons of one Sony wig share a single normalized
value, and every one of them decodes).
Triggers, by the origin each door actually writes:
closet ws_wig_make_remote file-sourced
matrix the panel, for Track M's "+ Trigger" file-sourced
device ws_device_make_remote ask the source command
remote catalog promotes, sniffed or pasted no
manual the drawer's own dialog no
Plus: any undecoded row on a Remote that is a lattice, came from the
closet, or carries a wig id -- which is what covers a trigger saved
from a matrix card, since that door stamps the dialog's origin rather
than the lattice's.
Commands: source in {database, imported, matrix}, a plucked command
name, or a porthole row; OR, as the upgrade path for data written
before the doors stamped anything, the owning device carries
source_wig_id / source_file / origin "closet" AND the command has no
decoded identity. That last guard matters: a decoded row is already
answered by tier 1 and has no use for this one.
Doors fixed to stamp CommandSource.IMPORTED: Adopt Device and the
supersession top-up it shares, the Remote-to-Device mint when the
source trigger is itself file-sourced, and Clipper/Plucker assigns.
IMPORTED rather than a new enum member: it already means "came from a
file rather than off the air" and nothing renders it. One trigger door
was also lying -- duplicating a Remote stamped every copied trigger
"manual" -- and now copies the source trigger's origin.
Known gap: a Clipper paste or a Plucker pull promoted to a Remote
arrives as origin "remote", indistinguishable from a Sniffer promote.
Widening that vocabulary is frontend work and was declined for this
track, so those two miss the tier.
WHERE THE TIER IS CONSULTED. The matrix listener's CellIndex (fourth
map; the stored index goes to hair-cell-index/2 so every lattice
rebuilds once), storage.get_triggers_for_signal, storage.match_command,
and pin_bindings.DeviceCommandIndex. Captures compute the value once, in
signal_monitor.normalize, beside the byte hash. One DEBUG line names the
tier that answered.
THE ECHO TICKET. The send expectation claimed on decoded identity and
the short/long fingerprint, both computed from the file, so a clean echo
of a file-sourced code matched neither: on the bench, setting a pinned
matrix Device's dial put three frames back through the receiver, the
fuzzy garble guard swallowed two, and the third was heard as a handset
press and fired the state trigger, twice reproducibly. The ticket now
carries norm_fp as one more fallback, armed and spent like the others
and offered only to a capture nothing could decode. This supersedes the
plan's section 4.5.
ONE PRESS IS ONE EVENT. An AC state code is two complete frames, which a
real receiver delivers 103 to 148 ms apart, so the 100 ms window fired
hair_state_heard twice per press and saved the store twice; the state
trigger fired twice for the same reason. MATRIX_STATE_DEDUP_WINDOW_S is
400 ms, keyed on (remote, cell) so a deliberate change of state inside
the window is still two events, and the trigger dedup takes the same
number only for rows that are file-sourced AND multi-frame -- a Sony
keypad's 45 ms repeat frames and single-frame wig rows keep the 100 ms
window they were sized for.
FIXTURES, in tests/fixtures/air-path/, gzipped because they ship to
every install: 51 real captures of four codes from two transmitters with
the identity HAIR stored for each, the four code lists themselves, the
34 distinct codes of the Mitsubishi lattice, and the 16 signals of the
ACER wig. The tests assert the run's own numbers -- 34 of 35 air
captures land on the value computed from the file, the one miss being a
Broadlink send of C2, named rather than hidden.
Bench, on the deployed build: 50 presses through the ESPHome transmitter
gave 10 of 10 for C2 and OFF, 11 events for 10 C1 presses (one press
split its frames by 330 ms, which is what took the window to 400), 20
trigger fires for 10 ACER Power presses across the two Remotes that
carry that code, and nothing at all for a code held out of the lattice.
Thirty pinned retransmits, thirty echo suppressions, zero leaks. Every
match read "on the normalized tier"; nothing matched on any tier above.
No frontend.
One tap is one fire, including on a handset that repeats the whole frame while the button is down. The regression bench against 0164e53 measured a Samsung32 handset repeating its full frame every 102 to 119 ms on an ordinary tap. The trigger path was borrowing MULTI_RECEIVER_DEDUP_WINDOW_S, which is 100 ms and was sized against Sony SIRC repeats at 45 ms. A full-frame repeater therefore landed just outside the window and a single tap fired two to four times; which it was came down to about ten milliseconds of jitter, so the fault was intermittent. Volume Up settled it inside one press: three captures at 65 ms and 106 ms spacing, the 65 ms gap suppressed and the 106 ms gap not. This is not a regression from any recent track. The constant is unchanged since v0.5.8 and was identical in all four builds of the bisect, which is why all four doubled. What changed - New TRIGGER_FIRE_DEDUP_WINDOW_S in const.py, 250 ms, with the bench measurements and the sizing argument in the comment. Above the widest full-frame repeat measured (119 ms) with room for receiver jitter, below a deliberate release-and-re-press. - The window is ANCHORED at the last capture allowed through, not slid forward by suppressed ones. Sliding made a held button invisible: while the frames kept arriving the window never expired, so a two-second hold was one fire and nothing more. Anchored, it expires on schedule and the next repeat re-fires -- against a 108 ms repeat that is a fire on roughly every third frame, about three a second, the cadence the appliance itself repeats at. - MULTI_RECEIVER_DEDUP_WINDOW_S is left alone at 100 ms and keeps its remaining job, grouping observations of one press for the diagnostic log line. The trigger path stops borrowing it. - _maybe_prune_recent_fires now sizes its cutoff on the widest window a row can be given rather than the narrowest, so an entry cannot be pruned while its window is still live. How the two windows compose One mechanism, the window chosen per row, the wider one winning where it applies. A row that is both file-sourced and multi-frame keeps MATRIX_STATE_DEDUP_WINDOW_S at 400 ms: the two frames of an AC state code arrive 103 to 148 ms apart through a real receiver and such a code has no repeat behaviour to preserve. Every other row gets the new 250 ms. The matrix listener already stamped on emit only, so anchoring the trigger path also brings the two into line. The dedup key is unchanged: trigger id alone, as it has been since v0.5.8. The module docstring still described it as (trigger, fingerprint), which went stale when the key changed; corrected here. Behaviour change worth naming: two taps closer together than 250 ms now count as one press. That is the deliberate cost of covering full-frame repeaters, and the affected test moved from 150 ms to 300 ms to say so. Tests - one tap of a full-frame repeater (0 / 108 / 216 ms) is one fire - a held button re-fires on the anchored ramp (0 / 108 / 216 / 324 / 432 gives two) - two taps 300 ms apart are two fires - Sony's five frames 45 ms apart are still one fire - the multi-receiver fold still holds, here and in the harder differing-byte-hash case in test_bytehash_identity - the wider file-sourced multi-frame window still wins where it applies - the tolerant track's two-frame press test is untouched and passes Both CI-parity venvs green: 3.12 2976 passed / 34 skipped, 3.14 3039 passed / 1 skipped. ruff clean. No frontend.
The regression bench proved this one over air. Three ESPHome sends of an Arris Power Pronto were heard cleanly by the Athom and fired nothing: trigger Power | origin remote file_sourced_trigger -> False air : fp 49b80b5e33c2cec8 bh 188018037eea2072 norm_fp cec7ced5e9fb2fba file: fp 49b80b5e33c2cec8 bh 4457e79c2bdf1eca norm_fp cec7ced5e9fb2fba air, no norm_fp -> [] air, with norm_fp -> [] file identity -> [Power] Three things line up there. The S/L fingerprint matched exactly, but the bare-fingerprint tier is withheld from hash-bearing rows (the deliberate v0.5.8 anti-false-match rule). The byte hash moved, so both hash tiers missed. The normalized fingerprint was identical, so the tolerant tier would have matched -- and was never offered, because the row said origin=remote. It said that because the promote door stamped every catalog row the same way, whichever of the three USE-as-a-Remote tabs made it. The Arris row came from a Clipper paste (its Sniffer group carries source=manual), and a pasted code is a file by definition: it never came off a receiver. What changed - Two new IRTrigger.origin values, clip and plucked, documented in the models.py field comment alongside the existing five. - identity.FILE_SOURCED_TRIGGER_ORIGINS, one frozenset that is now the single home for the vocabulary. file_sourced_trigger reads it. - signal_monitor.CATALOG_SOURCE_TRIGGER_ORIGIN maps UnknownDevice.source to a trigger origin: sniffed -> remote, manual -> clip, plucked -> plucked. echo never reaches the door and falls through to the default. - A load-time backfill, HAIRStore.backfill_catalog_trigger_origins, run from async_setup_entry rather than async_load because it is the only backfill needing both stores: the trigger names its Remote, and only the signal store knows which catalog row that Remote was promoted from (UnknownDevice.promoted_to_remote). One save, folded like the others. Deliberately narrow -- it rewrites origin=remote and nothing else, and only when the linked catalog row is manual or plucked. The mint doors, and what each writes - signal_monitor.promote_to_remote -- the USE-as-a-Remote fork for all three catalog tabs. The one door this commit changes. - websocket_api.ws_wig_make_remote -- closet. Unchanged. - websocket_api.ws_device_make_remote -- device, the source command decides. Unchanged. - websocket_api.ws_duplicate_trigger_remote -- copies the source trigger origin, so clip and plucked propagate for free. Unchanged. - websocket_api.ws_create_trigger -- writes the origin the panel sends (matrix for the lattice doors). Unchanged: a trigger created through this door on a Clipper-promoted Remote lands as remote and the backfill repairs it on the next load. Noted rather than papered over. No frontend. The panel indexes ORIGIN_COLORS by dialog TAB, not by a stored trigger origin, and the only consumer of trigger.origin is ir-trigger-row.ts testing equality against matrix -- so a clip row simply does not paint a STATE chip, which is correct. The origin colour question stays declined. Tests New test_clip_plucked_origins.py, 18 cases: - each source maps to the right origin, and an unknown source falls back to remote - the predicate is true for clip and plucked, false for remote, and the vocabulary is the ruled one - the backfill restamps a Clipper and a Plucker promote, leaves a real sniffed promote alone, leaves other origins alone, is idempotent, and does nothing for an unpromoted row - the bench fixture, tests/fixtures/air-path/arris-power-clip.json, carrying the pasted Pronto and the code the Athom actually handed back: the air form moves the byte hash and holds the normalized fingerprint; as shipped (origin=remote) it matches nothing; stamped clip it matches on the normalized tier; and stripped of norm_fp it still matches nothing, so the origin decides whether the tier is offered and the tier is what carries it. test_init.py stubs the new backfill on its mocked store; those three cases are about setup wiring. Both CI-parity venvs green: 3.12 2994 passed / 34 skipped, 3.14 3057 passed / 1 skipped. ruff clean.
Owner ruling, second bench pass, measured rather than argued. At 250 ms the bench ran three deliberate Volume Up taps whose frame trains measured 219, 213 and 215 ms end to end. All three folded to a single fire, but with only about 30 ms of margin. A Volume Down tap in the same session emitted three frames spanning 254 ms and fired twice -- six milliseconds outside the window. 300 ms clears the whole measured spread of a deliberate tap and still sits far below a release and a re-press. The second reason is consistency. 300 ms is SIGNAL_REPEAT_SUPPRESS_MS, the Sniffer window. The fault this constant exists to fix was the two paths disagreeing about what one press was -- the Sniffer counted a tap once while the trigger fired twice on the same frames. They now agree. Three gates remain (Sniffer 300, trigger 300, matrix 400) and a unified capture stage is expected to supersede all three; until then two of the three are the same number for a stated reason. Cost to a held button: the cadence is whenever the first frame past the window arrives, so it follows the handset repeat rate. At the measured 100-110 ms that is a fire every 324 to 400 ms, between 2.5 and 3 a second. The bench measured seven fires across a real two-second hold at 250 ms, 318 ms apart. A held volume button steps a little slower. Tests - the ramp test now spells the cadence out: six frames at 108 ms fire on frames 1 and 4, and the docstring records that the interval depends on the handset rate rather than on the window alone - the bench worst case is pinned directly: three frames at 0 / 163 / 91 (the 254 ms tap that failed at 250) is one fire - the boundary is probed a millisecond either side rather than exactly on the window; an exact-boundary assertion tests IEEE rounding on a monotonic clock, not the rule - the window assertion also pins it equal to SIGNAL_REPEAT_SUPPRESS_MS, so the two cannot drift apart silently - every spacing sized against 250 moved: deliberate-double-tap to 400 ms in test_bytehash_identity, the location-aware distinct-press gap to 400 ms, the pin-retransmit min_hits sleep to 400 ms, and the file-sourced composition probe to 350 ms (through 300, inside 400) Both CI-parity venvs green: 3.12 2996 passed / 34 skipped, 3.14 3059 passed / 1 skipped. ruff clean.
The bench saw two fires 3 ms apart on one Volume Up tap (2026-08-18) and the logs could not say why, because the suppression path was silent: you could not tell a capture gated out here from one that never reached this point at all. Investigation first, then the change. What was found, from the stored evidence 1. Both captures of that pair were on ONE receiver. Every receiver in that remote scope was queried for the window 20:20:45.0 to 45.5: only infrared.athom_rx recorded anything (45.485, 45.800, 45.803); the other six were static, their last states hours old or unknown. So the pair is not the multi-receiver fold failing. 2. The stamp is recorded at the moment of the check, before the fire path, with no await between: on_signal_captured is synchronous, and the check and the record are adjacent statements. Check-and-record cannot be interleaved on the event loop, so the race the pair would imply does not exist in this method. There IS an await upstream -- _process_parsed_signal awaits _match_echo before calling in -- which can reorder captures relative to their arrival, but it cannot let two of them through this gate. A comment now says so at the record, because the property is load bearing and invisible: making this method async, or moving the record below _fire_trigger, would open exactly the window the pair implies. 4. The 45.485 capture left no trace anywhere: no fire, no Sniffer row, no Mirror row, no log line at DEBUG. Its Sniffer group (the Samsung S/L fingerprint df9d5b3f3fab077d) is on the dismissed list as of earlier the same evening, and the dismiss check runs AFTER the trigger check, so dismissal explains the missing catalog row but not a missing fire. The consistent reading of the surviving evidence is a Samsung frame that decoded to something other than Volume Up: no trigger matched, and the dismissed group swallowed it. Stated as the reading it is; the new log line settles it next time either way. The 3 ms pair itself is NOT explained. What is established is that it is not the fold, not a check-and-record race, and not the window logic -- the window was verified directly on the deployed build with the injector: two injections 50 ms apart gave one fire, 320 ms apart gave two, and five at 100 ms gave two. What changed - A DEBUG line at the suppression continue, naming the trigger, the receiver, the measured gap in ms and the window in ms. This is the whole point: the next question of this shape is a log read. - The record site carries the comment about why it must stay where it is. - 300 ms stands (owner ruling, third pass). Recorded in the constant comment along with why: a later tap whose frames spanned 318 ms fired twice, which is the rule working. The boundary moves under the thumb wherever it is put, and 400 would collide with MATRIX_STATE_DEDUP_WINDOW_S. Tests - two receivers, same identity, 3 ms apart, as two asyncio tasks: one fire. The guard for the property in finding 2. - the same receiver twice, 3 ms apart, as two tasks: one fire. This is the bench actual shape per finding 1, and the one worth guarding. Both CI-parity venvs green: 3.12 2998 passed / 34 skipped, 3.14 3061 passed / 1 skipped. ruff clean.
The suppression line added in 0e45b88 could not settle the bench question it was added for. A capture that PASSED the dedup gate still left no trace, so "allowed" and "never reached the gate" read identically in the log, and the two Volume Up fires 3 ms apart at 2026-08-17 20:20:45 stayed ambiguous: either the second capture saw no prior stamp, or it saw a gap wide enough to clear the window. The allow line closes that. It names the receiver, reports the gap, and says whether a stamp existed at all. A missing stamp is reported as missing rather than printed as a number, because with no prior fire the stored value is 0.0 and the gap is time since the process started, which would read as a plausible but meaningless interval. The fire line now names the capturing receiver too, as the suppression and allow lines already do. Without it the receiver behind either half of a double fire had to be reconstructed from recorder history. Instrumentation only. No behaviour change and no test change: the dedup key, the window, the anchoring and the fire path are untouched.
Turn signals into a device: Devices vs Remotes, USE, one add dialog, ghost tile, reciprocal Make a Remote/Make a Device. Capture a remote: USE replaces old assign-only flow, count dot on repeat use. Quick start: step 3 updated to USE / Make a Device. Set up an air conditioner: STATE MATRIX card language, preset star, new Listen to the wall remote passage for AC Remotes. Import codes: USE button, one line on file-sourced Remotes recognizing real handsets. Set up triggers: trigger belongs to a Remote, Pin a Remote to a Device. The Devices tab: DEVICES and REMOTES sections. Use the Mirror: Mirror filter row (Search, Not heard, Emitter dropdown).
…-tile wig filing Capture a remote: a new remote needs at least three presses before it shows as a card. Set up an air conditioner: reworded the import-limits paragraph, added the comb-flagged-cell command-row behavior. Turn signals into a device: dropping onto the ghost tile also files a wig in the Closet. Import codes: cross-referenced the ghost-tile shortcut.
Restore the two screenshots the 0.10.0 draft dropped (devices overview, add-a-device dialog) alongside the placeholders for the new shots, reword the Import codes good-to-know line, and carry forward the MQTT capture and send notes and the MQTT adopters row for HA 2026.8.
CHANGELOG gains the 0.10.0 entry: the Devices and Remotes split with the dashed add tile, the one add dialog and USE; pinning a Remote to a Device; air-conditioner handsets as Remotes with LAST HEARD and the State heard trigger; thermostat presets by star; codes that came from a file recognizing the real handset; the Mirror filter row and the settings, header and finish pass; the tap-counts-once fix; ten languages. The entries since 0.9.8 are both unrelated hotfixes, so nothing here repeats them. info.md gains two lines on the HACS card: Devices and Remotes with pinning, and air-conditioner handsets as Remotes. llms.txt gains the architecture: the Devices/Remotes split with each Remote a real HA device and its device_trigger entries, pinning and the loop breaker, matrix Remotes in listen mode with hair_state_heard and the State heard device trigger, climate presets from starred commands, and the tolerant signal-shape match used only when a stored code is file-sourced and every exact tier misses. The nine translated READMEs get the v0.10.0 stamp; the repo carries no glossary section, so there was none to extend.
manifest.json and the panel's HAIR_VERSION footer constant both move to 0.10.0, and the dist bundle is rebuilt from that source so the footer a user reads matches the version HACS installed. Source and bundle land together, which is what test_locales.py's panel-stamp test checks.
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.
Release 0.10.0, Remotes Have Been Buffed. Branch trigger-styling onto main, squash merge.
What is in it: Devices and Remotes as two sections; USE on every set of codes to make a Device or a Remote; Make a Remote from a Device and Make a Device from a Remote; each Remote is its own Home Assistant device with device triggers; pinning a Remote to Devices with echo defense, loop breaker and coalescing; air-conditioner Remotes with the STATE MATRIX card in listen mode, LAST HEARD, state triggers and the "State heard" device trigger; the climate preset star; the add tile with file drop; the Mirror emitter filter; settings and header alignment; ten languages.
Behind it: one identity form for wig cells (wire form, backfilled); a persisted cell index; receiver-tolerant matching for file-sourced codes (normalized fingerprint as a lowest tier for cells, wig, Clipper and Plucker triggers, and wig-adopted commands; echo ticket carries it too); trigger dedup anchored at 300 ms keyed on trigger id with allow and suppress log lines; Clipper and Plucker triggers stamped clip and plucked and treated as file-sourced, with a load-time backfill.
Docs: README, CHANGELOG 0.10.0 entry, info.md, llms.txt, nine translated README stamps, manifest and panel footer 0.10.0 with the bundle rebuilt.
Verification: full suites green (3.12: 2998 passed, 34 skipped; 3.14: 3061 passed, 1 skipped), ruff clean; a real-air regression bench on the release candidate with an ESPHome transmitter and an Athom receiver, thirty presses through a pinned pair with zero echo leaks, three restarts, two receivers one press, zero HAIR warnings; eight-hour soak running.
No community issue closed by this release. PR #80 (Spanish README) is unrelated and stays open.