Category: Guides

  • ESPHome 2026.9 Finally Encrypts OTA Updates. Here Is the Two-Step Migration That Can Strand a Device

    ESPHome 2026.9 Finally Encrypts OTA Updates. Here Is the Two-Step Migration That Can Strand a Device

    Most of my ESPHome devices have an api: block with an encryption key, and I have long felt good about that. Then I looked at the ota: block underneath it and saw a plain password, the same one copy-pasted into a dozen YAML files. ESPHome 2026.9, released on 16 September, finally closes that gap, and it comes with a migration trap that is worth understanding before you click “Update all”.

    What actually changed

    Until now, the OTA platform authenticated with a password, but the firmware transfer itself was not encrypted. As of 2026.9 the esphome OTA platform uses Noise (ChaCha20-Poly1305), the same scheme the native API already uses. According to the release notes, any device that has an api: encryption key now offers encrypted OTA automatically, and the ESPHome CLI uses it when it is offered. You do not have to touch your YAML for that part.

    If you want to make it mandatory, you add a bare encryption: under ota:. It reuses your API key, and it cannot be combined with password:.

    api:
      encryption:
        key: !secret api_key
    
    ota:
      - platform: esphome
        encryption:

    That is the whole end state: one key, no separate OTA password to rotate, leak or forget.

    The two-step migration (this is the part that bites)

    You cannot jump straight from a password to encryption: on a device that is still running older firmware, because that firmware does not know how to speak the encrypted protocol. The release notes spell out the order:

    • Update the device to 2026.9.0 with the password still in place. This gets the new OTA code onto the chip.
    • Only then replace password: with encryption: and flash again.

    If you do it in the wrong order you will find out the annoying way: a device that no longer accepts your update and a USB cable you have to go and find. For anything mounted inside a wall box or an enclosure, I would do the first pass for every device, confirm each one is online and reporting 2026.9, and only then start the second pass.

    The deadline is also real: the unencrypted OTA path is scheduled for removal in 2027.3.0. That is plenty of time, but it is a hard stop, and anything still on a password by then will need a cable to recover. The patch release 2026.9.1 (29 September) also includes a fix around OTA encryption key handling in safe mode, so if you are updating now, start from 2026.9.1 rather than 2026.9.0.

    Why I care about this on a local-first network

    “It is only my LAN” is the argument I have made for years, and it is not wrong, but it is lazy. My LAN also carries a pile of cheap IoT gear, guest devices and the occasional thing I have not audited. Firmware is the most privileged payload on any device I own, and sending it over a network in a form anyone on the segment can read or tamper with is exactly the kind of shortcut I would call out in a cloud product. Applying the same standard to my own gear is only fair.

    There is a related option from 2026.4 that is worth knowing about: signed OTA verification. It checks the firmware signature during an update without requiring hardware Secure Boot, so you do not burn any eFuses. In private key mode ESPHome signs at build time, and in public key mode only the public key lives on the device while you sign the binaries elsewhere, for example in CI. That is a different protection (is this firmware really mine?) from encrypted transfer (can anyone read it on the wire?), and the two complement each other. I will cover signing in its own post once I have lived with it for a while.

    Side effects worth knowing

    Encryption used to be expensive on small chips, and this release makes it cheaper. The notes quote the API encryption flash cost on an ESP8266 d1 mini dropping from 80.9 KB to 41.9 KB, with static RAM falling from 1376 bytes to 48 bytes, and the ESP8266 Noise handshake going from about 580 ms to about 300 ms. If you were leaving encryption off on tiny boards because of the footprint, that excuse just got a lot weaker.

    Builds are faster too. PlatformIO installs now run in parallel, and a no-op rebuild of a six-library config dropped from 2.9 s to 0.4 s. Those are the project’s own CI figures, so your mileage will vary, but the direction is right if you compile on a modest machine.

    Green printed circuit board with capacitors and microchips
    Photo: Pixabay via Pexels

    Before you update: my short checklist

    • Back up your ESPHome config folder first. It takes a minute and saves an evening.
    • Check which devices actually have an api: encryption key. Those without one will not get encrypted OTA until you add it.
    • Read the breaking changes. The Modbus Controller got a notable shake-up (custom_command became custom_pdu, and broadcast address 0 is now rejected), and a burned custom eFuse MAC now applies to Ethernet, Bluetooth and 802.15.4 addresses as well. That last one can change MAC addresses on upgrade, which matters if your router has DHCP reservations. The ignore_efuse_custom_mac: true option keeps the old behaviour.
    • Update one low-stakes device first, such as a desk sensor, not the one that controls your heating.
    • Do the password-to-encryption switch as a separate, second round.

    One more heads-up: the release notes say the native ESP8266 toolchain is expected in 2026.10.0, which has not been published as of today. I would wait for that and its own breaking changes before touching old ESP8266 boards a second time.

    My take

    This is the kind of release I like: no flashy feature, just a long-standing weak spot fixed properly, with a deadline and a clear migration path. The two-step dance is a little fiddly, but it is documented, and it is a one-time cost. If you run ESPHome devices you cannot easily reach, give yourself a quiet afternoon, go slowly, and let the first batch prove itself before you do the rest.

    Sources: ESPHome 2026.9.0 release notes; ESPHome 2026.4.0 announcement (signed OTA). Header photo from Pexels.

  • SolarEdge Gets a Local Home Assistant Integration in 2026.10. The Hard Part Is Opening the Inverter’s Menu

    SolarEdge Gets a Local Home Assistant Integration in 2026.10. The Hard Part Is Opening the Inverter’s Menu

    Solar inverters have a habit I find hard to forgive: they produce data all day, and the easiest way to read it is through the manufacturer’s cloud. For years the usual Home Assistant route for SolarEdge owners went through that cloud too, with an API key, a polling limit, and a dashboard that goes quiet whenever the vendor’s servers have a bad afternoon.

    Home Assistant 2026.10 adds a different route. According to the release notes, a new SolarEdge Modbus integration launched at platinum quality and talks to the inverter directly over your LAN. I do not run a SolarEdge inverter, so I have not connected one, and I will not pretend otherwise. What I did do is read the integration documentation closely, because the design is a good case study in what “local-first” should look like for energy gear.

    What actually changes

    The integration reads the inverter over Modbus TCP, a protocol already built into the device. The documentation states it needs no account, no API key, and no cloud service, so readings keep arriving when your internet connection is down. It is rated “Local Polling” and maintained by Frenck, one of the long-time core developers.

    The polling cadence is the part that made me raise an eyebrow, in a good way. The inverter and attached devices are polled every 10 seconds. Inverter settings refresh every five minutes, and attached meters and batteries are rechecked every 15 minutes, with an automatic reload if you add or remove one. Ten seconds is a very different world from the slow cloud polling many of us tolerated, and it makes things like “turn on the heat pump when export exceeds 2 kW” a reasonable automation instead of a lagging guess.

    Enabling Modbus is the awkward part

    The Home Assistant side is easy. The inverter side is where you will spend your time, and it depends on the model, according to the docs:

    • Inverters without a display: move the red ON/OFF/P switch to P for under five seconds to start the inverter’s own Wi-Fi access point, connect to it, open 172.16.0.1, go to Site Communication, and enable Modbus (TCP).
    • Inverters with a display: hold OK, enter the menu password, then go to Communication, LAN, and enable Modbus TCP.
    • Port and ID: the port is 1502 and the device ID defaults to 1. Give the inverter a fixed address in your router so it does not wander.

    The documentation also recommends a wired connection. Its stated reason is that SolarEdge may drop Modbus over Wi-Fi in future firmware. That is a polite way of saying the vendor can change the rules, which is exactly why I would run a cable and not rely on goodwill. For inverters without a network port there is an RS485 route, set to the SunSpec (Non-SE Logger) protocol, via a serial port or a Modbus TCP bridge.

    What you get, and what you do not

    The entity list is generous: inverter power, lifetime energy (usable in the Energy dashboard), AC and DC values, heatsink temperature and status, plus up to three meters and three batteries with state of energy, state of health, and charge and discharge figures. Voltage, frequency and power factor exist but are disabled by default, which is a sensible choice that keeps your entity list from ballooning.

    There are honest limits too. Per-panel and per-optimizer data is only available through the cloud-based SolarEdge integration, so if you love staring at individual panels, local does not give you that. SolarEdge EV chargers are not supported. Hardware changes can take up to 15 minutes to show up.

    The writable settings deserve a warning

    The integration can also write: backup reserve, export limit, active power limit, storage control mode. That is powerful, and the documentation is refreshingly blunt about the risks. The control registers are undocumented by SolarEdge and the inverter rejects values it does not accept. Writes wear flash memory, so you should avoid automations that write settings frequently. Changing export limits may also breach your grid operator’s agreement.

    My rule for anything like this is simple: read freely, write rarely. An automation that rewrites an export limit every time a cloud passes over is an excellent way to wear out a device you cannot easily replace and to annoy your utility. If I ran this, I would automate loads that react to the readings and leave the inverter’s own settings as a manual, deliberate action.

    One more catch: connection limits

    The docs note that one Modbus connection is shared per address, and that a separate Modbus YAML integration opens its own connection, which counts against the inverter’s client limit. If you previously hand-built a Modbus setup, do not run both against the same inverter. Migrate, then remove the old one.

    That ties into a broader theme. Home Assistant 2026.10 also adds a Modbus panel under Settings, Connectivity, listing every Modbus connection and the integrations using it. After years of Modbus being a pile of YAML you debugged by squinting, a screen that tells you what is connected to what feels overdue.

    Where I land

    For anyone with a SolarEdge system who has tolerated the cloud route, this looks like a clear upgrade: faster, independent of the vendor’s servers, and built to a quality bar that includes local-first behavior. The price is a one-time trip to the inverter’s menu and a network cable. If you try it, I would love to hear how the enabling step went on your model, because that is the part no documentation can fully predict.

    I will keep an eye on it. If I end up on a house with solar, this is the integration I would reach for first, and I would still wait for 2026.10.1 before touching a system that makes me money.

  • My Security Dashboard Used to Show Every Sensor, All the Time. Home Assistant 2026.9 Taught It to Shut Up Until Something’s Wrong.

    My Security Dashboard Used to Show Every Sensor, All the Time. Home Assistant 2026.9 Taught It to Shut Up Until Something’s Wrong.

    For as long as I’ve had a Security view in Home Assistant, it has worked like a spreadsheet: every contact sensor, every lock, every smoke detector, lined up in a grid whether anything was wrong or not. Front door sensor: closed. Office window: closed. Basement leak sensor: dry. Twenty-some rows of green ticks, scrolled past a hundred times a day, mostly on the way to the one row that actually mattered. Home Assistant 2026.9, released September 2nd and now four patch releases in at 2026.9.4, finally gave that dashboard a filter I didn’t know I wanted: it can just stay empty.

    A card that only shows up when it has something to say

    The new feature is called Active Alerts, and it lives inside the built-in Security dashboard strategy. You pick which entities are allowed to raise an alert, you assign each one a severity — Alert for the things that should interrupt you, like a smoke detector going off or a door unlocking itself at 3 a.m., and Warning for the things that are worth knowing but not panicking about, like a window left open — and then the card does something dashboards rarely do: it disappears. No alerts active, no card. The official 2026.9 release notes put it plainly: it “only shows up when something actually needs your attention.” A companion Favorites section sits next to it for the handful of entities you check manually and don’t want buried in the rest of the dashboard.

    It’s a small feature on paper, but it’s the kind of small feature that only happens because someone got annoyed enough to ask for it in public. Digging through the pull request that shipped it — frontend PR #53031, authored by core contributor timmo001 — the idea traces back to a community feature request thread on the Home Assistant forums asking for a proper severity attribute for entities. Reading that thread is a good reminder that a lot of what ships in Home Assistant started as someone’s mildly annoyed forum post, not a product roadmap meeting. One review comment on the PR is worth mentioning too: camera entities were deliberately pulled out of the available alert options before merge, because a live feed thumbnail sitting in an “alert” card next to a smoke-detector state just didn’t make sense. Small detail, but it tells you the feature was actually reviewed rather than rubber-stamped.

    Wiring up my own alerts

    My setup is the usual local-first pile: Zigbee2MQTT talking to a mix of Aqara and Sonoff sensors, all coordinated by Home Assistant OS running bare-metal on a decade-old HP EliteDesk 800 G4 mini PC that I bought used specifically so I wouldn’t feel bad about it drawing power 24/7. Nothing about that stack changes for this feature — Active Alerts doesn’t care whether your entities come from Zigbee, Z-Wave, Matter, or a cloud integration you’re slightly ashamed of. It just wants entity IDs.

    Setup took about ten minutes, and all of it happened in the Security dashboard’s own editor rather than in YAML, which was a pleasant surprise for something this configurable. I went through my binary sensors one at a time:

    The Aqara smoke detector in the utility room got Alert severity — the one entity in the house I actually want to interrupt whatever I’m doing. The two door contact sensors (front door, garage side door) also went in as Alert, because an unexpected unlock or open at night is exactly the “stop scrolling and look” situation this feature is built for. Window contact sensors in the living room and office went in as Warning — useful to know about, not worth a phone buzzing at 11 p.m. because I forgot to close one before a September evening got cold. The basement water leak sensor I set to Alert too; water damage is slow-motion but expensive, and I’d rather know in the first ten minutes than the first ten hours.

    Everything else — the fifteen-odd sensors that just describe the state of the house rather than flag a problem with it — I left out entirely. That’s really the point of the feature: it’s opt-in by entity, so the card can’t accidentally become the same wall of green ticks it was replacing.

    Illuminated security keypad mounted on a wall
    Photo: Pexels

    Where it’s genuinely useful — and where it isn’t

    After a couple of weeks living with it, the honest verdict is: it’s a good dashboard feature, not a security system. It doesn’t send push notifications on its own — you still need an automation with a notify action for that, same as before 2026.9. What it changes is the default view: when I do open the app because something buzzed, I land on a Security tab that either confirms “yes, that’s the thing, here it is” or is empty, which is its own useful signal that whatever triggered the notification has already cleared. Before this, I’d open the dashboard and have to visually hunt for the red row among twenty green ones. Now there’s nothing to hunt for.

    The two-tier severity system is also a real limitation once you start using it seriously. Alert and Warning cover most of a typical house, but I have exactly one situation that doesn’t fit either bucket cleanly — a freezer temperature sensor I’d like flagged as urgent but not visually screaming at me the same way as a smoke alarm. For now it lives in Alert, slightly overstated, because Warning felt too easy to ignore. A third severity tier would fix that, and given how this feature came to exist in the first place, I wouldn’t be surprised if someone’s already asked for it on the forums.

    What I keep coming back to, though, is that this is a genuinely local feature solving a genuinely local problem. There’s no cloud service deciding what counts as urgent in my house, no vendor dashboard that changes its layout every six months, no subscription gate between “you have a sensor” and “that sensor can tell you something’s wrong.” It’s a few lines of entity configuration sitting on the same box that’s been quietly running this house since I plugged it in. That’s the whole appeal of doing this locally in the first place — and it’s nice when a release actually delivers on it instead of just promising it in a changelog.

    If you’re running a recent Home Assistant instance, the Security dashboard and its Active Alerts card are already there waiting — go to Settings → Dashboards, add (or edit) the Security dashboard, and you’ll find the entity picker and severity dropdown right in the editor. Ten minutes, no YAML required, and one less wall of green ticks to scroll past every day.

  • For Years My ESPHome Updates Carried My Wi-Fi Password in the Clear. 2026.9 Closed That.

    For Years My ESPHome Updates Carried My Wi-Fi Password in the Clear. 2026.9 Closed That.

    I have been flashing ESPHome devices over the air since my first ESP8266 temperature sensor, and I never once thought about what was actually crossing the network while the progress bar filled. An OTA password was set. The dashboard said OTA successful. That was the end of my thinking on the subject.

    Then I read the ESPHome 2026.9.0 release notes, published on 16 September, and found the sentence that ruined my evening: until this release the firmware image travelled over the network in plaintext, so a passive listener on the LAN could capture the Wi-Fi credentials and the API encryption key baked into that image. The OTA password authenticated the uploader. It never made the transfer confidential.

    That is not a vulnerability in the dramatic sense. Nobody broke in. It is just a thing that was true about my house for years, that I had never bothered to work out for myself, and that I would have kept not working out if someone had not written it down in a changelog.

    What was actually on the wire

    An ESPHome binary is not an abstract blob. It contains the SSID and password of the Wi-Fi network it is supposed to join, and it contains the Noise pre-shared key that my Home Assistant box uses to talk to that device. Every time I ran esphome run against a sensor, both of those went across my LAN in the clear, addressed to the device but visible to anything sitting on the same segment.

    My honest threat model here is small. It is a flat home network, in an apartment, in Switzerland. The realistic attacker is a compromised smart TV or a guest device that should not have been on the main VLAN in the first place. But “my threat model is small” is exactly the sentence people say right before they explain how the thing got in. The local-first argument I keep making on this blog is that keeping data inside the house is worth something. It is worth rather less if the house itself is a broadcast medium.

    Ethernet patch cables on a dark surface
    The LAN is not a private channel just because it is yours. Photo: Pexels

    What 2026.9 actually changes

    The fix, contributed by bdraco in PR #18489, adds Noise encryption — ChaCha20-Poly1305 — to the esphome OTA platform, using the same protocol the native API already speaks. It sits on a new shared noise component that lifts the plumbing out of the API component so both can use it. There is no configuration for noise itself; you never touch it.

    Because it uses the Noise NNpsk0 pattern with a pre-shared key, the key does two jobs at once: it keeps the image confidential and it proves who is uploading. Which is why encryption and password: are mutually exclusive. The password had one job, and the key already does it better.

    On a device that already has an API encryption key, the new firmware simply offers OTA encryption using that key, and the CLI takes it whenever it is offered. No config change required. The config change is what turns “offered” into “required”.

    The config is one line. The migration is two installs.

    The end state is almost insultingly simple — a bare encryption: block under the OTA platform, which inherits the API key so there is exactly one secret per device:

    api:
      encryption:
        key: !secret device_name__encryption_key
    
    ota:
      - platform: esphome
        encryption:

    Here is the part that will bite people who paste that in and hit install. Once encryption: is in the config, the CLI refuses to send a plaintext image. Old firmware cannot offer encryption. So the install against your existing device stops with the device did not offer encryption; refusing to send the image in plaintext, and you sit there wondering what you broke.

    That is deliberate, and it is the right call: a silent fallback to plaintext would mean an attacker could strip encryption and force an unauthenticated upload. Both ends fail closed. But it does mean the order matters:

    1. Install 2026.9.0 first, with your config otherwise unchanged — keep the password:, make sure api: has an encryption: key:. This upload is still plaintext; that is unavoidable and it is the last one. Then check the device log for Encryption: offered, plaintext accepted.
    2. Now add encryption: under the OTA platform and remove password:, and install again. The log should read Encryption: required.

    If a device’s API key was provisioned by Home Assistant rather than written in your YAML, copy that provisioned key into api: encryption: key: before step 1. Do not generate a fresh one — that locks Home Assistant out of the device. The OTA platform docs also cover the MQTT-only case, where you temporarily add an api: block as a stepping stone and then move the key to the OTA block.

    The plaintext fallback is planned to disappear no earlier than 2027.3.0. After that, step 1 happens over a serial cable. So there is no rush, but there is a deadline, and I would rather do it at a desk than with a USB cable and a torch behind the washing machine.

    It costs less than it used to, not more

    My first instinct with anything involving the word “encryption” on an ESP8266 is to assume it eats flash I do not have. This release went the other way. The same PR series rewrote the noise-c and libsodium forks that had only ever been lightly adapted for embedded targets, and cut them close to in half.

    On a d1 mini, turning on API encryption cost 80.9 KB of flash and 1376 bytes of static RAM in 2026.8.0. In 2026.9.0 it costs 41.9 KB and 48 bytes. On an ESP32 AtomU it drops from 65.9 KB to 37.7 KB; on a Pico W, 65.5 KB to 34.7 KB. The handshake itself falls from roughly 580 ms to roughly 300 ms on the ESP8266, and 63 ms to 46 ms on the ESP32.

    So the ESP8266 devices I had quietly written off as “too small for this” mostly are not, and they got faster reconnects thrown in. That is the kind of change I like: not a feature, just someone going through old code properly.

    Two things to know before you commit

    First: there is no key rotation yet. The device only accepts the key it is running, and the CLI only presents the key in the config. Changing the key means a serial flash, or the web_server OTA platform if you have it configured. Losing the key means the same. A rotation mechanism is on the roadmap; it is not here. Put the key in your password manager now, not later.

    Second: if you keep the web_server OTA platform alongside encryption:, validation will warn you, because that endpoint accepts the same firmware image over plaintext HTTP. Which rather defeats the exercise. The captive portal’s update page is treated differently and does not warn, because it only exists while the fallback AP is up and it is the intended way to rescue a device that cannot join Wi-Fi.

    Worth noting that 2026.9.0 also has real breaking changes in modbus_controller — custom_command, register_count, force_new_range and skip_updates have all been renamed or replaced. If you have a solar inverter or an energy meter on Modbus, read the upgrade checklist before you touch anything. Between this and Home Assistant 2026.9 pulling Modbus into the UI, September has been a rough month for anyone with hand-written Modbus YAML.

    I am doing step 1 across my eleven ESPHome devices this weekend and step 2 the weekend after, because I want a week of normal operation between “this device can be updated encrypted” and “this device will accept nothing else”. I will write up whatever goes wrong. Something usually does.

  • For Two Years I Guessed Which Automation Turned On That Light. Home Assistant 2026.9 Just Tells Me.

    For Two Years I Guessed Which Automation Turned On That Light. Home Assistant 2026.9 Just Tells Me.

    The hallway light on my ground floor turns itself on sometimes. Not often, maybe twice a week, almost always late in the evening. For about two years my answer to “why did that happen?” has been a shrug and a vague theory involving the motion sensor near the front door.

    I had the tools to find out. Home Assistant has had automation traces for years, and I know how to read them. But the actual procedure went like this: notice the light is on, open Activity (the panel that was called Logbook until the UI rename), confirm that light.hallway turned on at 23:14, then guess which of my forty-odd automations did it, open that automation, click Traces, and hope the run I care about is still in the last five.

    That is not debugging. That is a scavenger hunt with a time limit. So I never did it, and the hallway light stayed a mystery.

    Home Assistant 2026.9, released on 2 September, quietly fixed the exact part I was stuck on.

    The feature that did not make the headline

    2026.9 shipped a Matter network map, an active-alerts section on the Security dashboard, and the beginning of the end of Modbus YAML. Those got the coverage, fairly enough. The change I actually care about is smaller: selecting any row in Activity now opens a details dialog that explains why that row exists.

    The dialog reads top to bottom as a chain. First, what started it: a person, a state change, a schedule, an integration, or a restart. Then the automations or scripts the event passed through. Then the entity’s own change at the end. Every step is clickable. Entities open their more-info dialog; automations open their trace directly, no hunting through the automation list. Timestamps go down to the millisecond, so two things that landed in the same second still keep their real order.

    That is it. No new protocol, no new hardware. It just closes the loop between “what changed” and “who did it,” which is the question I have been failing to answer for two years.

    What it told me about my hallway

    I upgraded, waited for the light to do its thing, and clicked the row. Ten seconds later I had the full chain: a state change on binary_sensor.front_door_contact, into an automation I wrote in 2023 and named “Evening arrival,” out to light.hallway.

    Which is correct behaviour. The automation is supposed to do that when the front door opens after sunset. The problem is that nobody opened the front door at 23:14. The contact sensor’s magnet has drifted a couple of millimetres out of alignment, and a firm gust against the door is now enough to report an open-and-close pair. Two years of “the house is haunted” turned out to be a piece of tired mounting tape.

    I want to be honest about this: I could have found that at any point in the last two years. I did not, because the old workflow made it a ten-minute job instead of a ten-second one, and ten-minute jobs for mildly annoying problems never get done. That gap is the entire value of the change.

    A dense bundle of tangled electrical wires
    Roughly what my automation graph felt like before I could click through it. Photo: Pexels.

    Three things I was not looking for

    Once the dialog exists, you start clicking rows for no reason, and that is where it earned its keep.

    A script I was sure I had deleted. I removed a guest-mode experiment last winter, or thought I did. Activity showed a chain passing straight through a script I no longer had a mental model for, called by an automation I had forgotten disables my bedroom presence timeout. It had been quietly running for nine months.

    A double fire. Two automations acting on the same kitchen light within the same second, one turning it on at full brightness and one immediately dimming it. I had assumed the visible flicker was a Zigbee transition artefact. It was me, twice. The millisecond timestamps made the ordering obvious in a way the old rows never could.

    Something that was never my automation at all. One of my plugs switches on by itself in the morning, and I had spent a while looking for the automation responsible. There is not one. The chain starts at the integration, because the device has its own on-board schedule that I set during setup and forgot about. Local control cuts both ways: the device does not need my automation engine to have opinions.

    The Sources pane is the other half of it

    History and Activity now share a single Sources panel down the left side. It merges target picking (floors, areas, devices, entities) with filtering by domain, device class, and integration, so you can take a whole floor and narrow it down to just the motion sensors on it. It docks open on wide screens, tucks away as a pull-up sheet on narrow ones, and it remembers what you filtered on last time.

    The two pages still behave differently on purpose. History waits for you to pick something before it draws a chart. Activity shows everything by default and lets you filter down without picking a target at all. That distinction sounds pedantic until you are trying to answer “what happened in the kitchen between eleven and midnight” at half past eleven at night. The date range picker also collapsed into a single pill with previous and next arrows, which is a small thing I now use constantly.

    Where the trail still goes cold

    Two limits worth knowing before you lean on this.

    Traces run out. The chain is clickable through to an automation’s trace, but Home Assistant stores only the last five runs per automation by default. For anything that fires often, the run you want is long gone by the time you look. You can raise it per automation:

    automation:
      - alias: "Evening arrival"
        trace:
          stored_traces: 25
        triggers:
          - trigger: state
            entity_id: binary_sensor.front_door_contact
            to: "on"

    I bumped it to 25 on the handful of automations I actually investigate and left everything else alone. Traces are not free, and there is still no global setting, which is a long-running annoyance in the community.

    Restarts eat traces. There is a long-standing open issue about traces not surviving restarts reliably. Activity itself is backed by the recorder database and holds up fine, but the trace behind a chain link may simply be gone after an update or a reboot. Practical rule: if something odd happened overnight and you want to know why, look before you restart, not after.

    Worth updating for?

    If you run more than a dozen automations, yes, and it is the least risky kind of upgrade because nothing in your config has to change to benefit from it. Read the breaking changes first, though: 2026.9 removes the VLC integration outright, drops the deprecated battery_level property from vacuum entities, and changes LLM tool names. None of those touched my setup, but the vacuum one will catch anyone with dashboard cards built on that attribute.

    What I keep coming back to is that this is not a feature that does anything. It does not add a device, speed anything up, or save a watt. It makes the house explainable, which for a system that has been accreting automations for six years is worth more to me than another integration. A local-first home is only genuinely yours if you can find out what it is doing without asking anyone.

    I remounted the front door sensor with fresh tape and a slightly closer magnet. The hallway light has not turned itself on since.

  • My Bathroom Humidity Sensor Died in April. I Found Out in August.

    My Bathroom Humidity Sensor Died in April. I Found Out in August.

    In April my bathroom humidity sensor read 58%. In August it still read 58%. Not 58% again — 58% still, the same reading, unchanged, for something like four months. I only found it because I finally got annoyed that the extractor fan never came on by itself anymore and went looking for the automation that was supposed to do it. The automation was fine. Its trigger was waiting on a number that had stopped moving.

    Here is the part that bothered me: nothing was broken, as far as Home Assistant was concerned. The entity was not unavailable. It was not unknown. It had a state, a unit, a device class, a battery level of 61%, and a friendly name. Every dashboard card showed a perfectly normal-looking humidity reading. The sensor had simply stopped talking, and Home Assistant kept faithfully displaying the last thing it said.

    A device can die two ways, and only one of them is loud

    The loud death is the one everybody designs for. An integration falls over, a hub goes offline, a Wi-Fi plug loses the network — and the entity flips to unavailable. Dashboards grey out. Templates that use has_value() return false. You notice within a day, because everything that depends on it visibly stops.

    The quiet death has no state of its own. The Home Assistant docs on working with states are clear that there are exactly two special values: unknown means the entity exists but its value is not known right now, and unavailable means it cannot be reached at all. There is no third value for “this number is real, it is just four months old.” The last good reading stays on the entity indefinitely, and every automation downstream keeps evaluating against a fossil.

    For a battery-powered Zigbee device this is the normal failure mode, not the exotic one. A coin cell that sags under load, an end device that drops off the mesh after a router is unplugged, a firmware hiccup — none of those necessarily produce an error anywhere. They produce silence, and silence looks identical to “nothing has changed.”

    Zigbee2MQTT can tell you, but it is a blunt instrument

    Zigbee2MQTT has an availability feature, and it is off by default, which is why mine was not helping. Once enabled it splits devices into two groups: active devices (mains-powered ones it can ping) get a ten-minute check-in window, and passive devices (battery ones it cannot ping without waking them up) get a default timeout of 1500 minutes — twenty-five hours. ZHA has broadly similar options.

    Twenty-five hours of silence from a battery device is the right default, because a door sensor on a door nobody opened has nothing to report. But it also means the safety net has a very wide mesh, and it only covers Zigbee. My house is Zigbee plus a handful of ESPHome nodes plus a few cloud-free integrations, and I did not want three different mechanisms each answering a slightly different question. I wanted one list: which of the things I actually care about have gone quiet?

    A motion sensor, a camera and a smartphone arranged on a dark background
    The watchdog only watches about thirty entities. The other nine hundred can fend for themselves. Photo: Pexels

    The whole watchdog is one template sensor

    Every entity in Home Assistant carries a last_reported timestamp — the moment it last said anything, whether or not the value changed. That is the signal. Everything else is bookkeeping.

    I did not want to monitor all nine hundred-odd entities, so I put a label called watchdog_6h on the roughly twenty that should never go more than a few hours without speaking: motion sensors, the temperature and humidity sensors that drive automations, the power monitoring plugs. Labels are the cleanest handle here because they cut across integrations, areas and device types.

    template:
      - triggers:
          - trigger: time_pattern
            minutes: "/15"
        sensor:
          - name: "Quiet entities 6h"
            unique_id: quiet_entities_6h
            state: >
              {% set cutoff = now() - timedelta(hours=6) %}
              {{ expand(label_entities('watchdog_6h'))
                 | selectattr('last_reported', 'lt', cutoff)
                 | list | count }}
            attributes:
              quiet_list: >
                {% set cutoff = now() - timedelta(hours=6) %}
                {{ expand(label_entities('watchdog_6h'))
                   | selectattr('last_reported', 'lt', cutoff)
                   | map(attribute='name') | list | join(', ') }}

    Two details in there cost me an evening each, so let me save you both.

    The first is triggers: with a time_pattern. My original version was an ordinary state-based template sensor, and it cheerfully reported zero quiet entities forever. Of course it did. A normal template sensor re-renders when one of the entities it references changes — and the entire point of this sensor is to detect entities that are not changing. Nothing ever triggered a re-render. Pinning it to a fifteen-minute clock is what makes it work at all.

    The second is why the list of names lives in an attribute rather than the state. Entity states in Home Assistant are capped at 255 characters. Five or six friendly names will blow straight through that and your sensor will start throwing errors at exactly the moment you most want it to work. Attributes have no such limit.

    A morning digest, not an alarm

    automation:
      - alias: "Watchdog - quiet device digest"
        triggers:
          - trigger: time
            at: "08:30:00"
        conditions:
          - condition: numeric_state
            entity_id: sensor.quiet_entities_6h
            above: 0
        actions:
          - action: notify.mobile_app_phone
            data:
              title: >
                {{ states('sensor.quiet_entities_6h') }} device(s) have gone quiet
              message: >
                {{ state_attr('sensor.quiet_entities_6h', 'quiet_list') }}

    My first instinct was to notify the instant an entity crossed the threshold. I tried it for a week. Every alert I fired at the moment of detection was an alert that arrived at some genuinely unhelpful hour, for a sensor that had been briefly out of range and would be back in twenty minutes. A device that has been silent for six hours will still be silent at 08:30. There is no emergency here — the emergency already happened, quietly, four months ago. What I needed was to be told at all, not to be told fast.

    Two versions of this were wrong before one was right

    Version one used six hours for everything. That is a sensible window for a motion sensor and a ridiculous one for a water leak sensor that reports once a day if it feels like it, or a button that only speaks when pressed. The fix was not cleverer logic, it was a second label: watchdog_36h, with an identical sensor and a longer cutoff. Two thresholds covers essentially my whole house. Anything needing a third threshold probably does not belong in a watchdog.

    Version two, before I moved to labels, swept up every entity in the sensor domain. That dragged in link quality and battery entities, which report on their own unrelated schedules, and the list came back so noisy it was useless. Being deliberate about what you monitor is not laziness — a watchdog you learn to ignore is worse than no watchdog, because you think you are covered.

    I should also credit the prior art: the community has been solving the adjacent problem for years, most visibly with jazzyisj’s unavailable entities sensor, which tracks entities in the unknown and unavailable states. It is a good package and I ran it for a while. It just answers the other half of the question — the loud deaths, not the quiet ones. The two together are the actual coverage.

    What 2026.9 helps with, and what it does not

    Home Assistant 2026.9, out on 2 September with 2026.9.1 following three days later, added an activity log that reconstructs why an entity changed — which person, schedule, sensor or automation was actually responsible. I have used it twice already and it is excellent. It is also, by construction, a tool for after you know something happened.

    That is the gap a staleness watchdog fills. The activity log answers “why did this change?” A watchdog answers the harder question, which is “why has nothing changed, and should it have?” Silence produces no log entry to go looking at.

    The honest limits

    This catches devices that stop reporting. It does not catch devices that keep reporting something wrong. My outdoor temperature sensor spent several weeks last winter reading a few degrees too warm because I had mounted it where the afternoon sun hit it, and no timestamp check on earth would have flagged that. It also will not catch a device that flaps — reports, drops, reports again — because it never crosses the cutoff. Those are separate problems needing separate tools.

    But the cost here is one template sensor, one automation and two labels, and it has already caught a door sensor with a dying cell and an ESPHome node that had wedged itself after a firmware update. Both would have been months of quiet nothing otherwise. That seems like a fair trade for twenty minutes of YAML and one evening of finding out why my first version reported zero forever.

    The bathroom sensor, incidentally, had a battery reading 61% right up until the moment I pulled it out and measured it at 2.4 volts. Reported battery percentage is a hint, not a fact. Timestamps do not lie.

  • My Home Assistant Database Was 5.8 GB. Most of It Was Signal Strength.

    My Home Assistant Database Was 5.8 GB. Most of It Was Signal Strength.

    When I wiped my Home Assistant box on purpose last week, the part that surprised me wasn’t the restore. That went fine. The part that surprised me was watching the progress bar sit there while a single file unpacked, and realising that one file — home-assistant_v2.db — was 5.8 GB.

    My entire configuration directory, minus the database, is under 400 MB. Everything I have ever written, every automation, every dashboard, every ESPHome YAML file, every secret: 400 MB. The database was fourteen times that, and I had never once looked at it.

    So I looked. This is what I found and what I changed. I’m running Home Assistant OS 2026.8.3 bare-metal on a used HP EliteDesk 800 G4 mini PC with a SATA SSD, which means none of this is an SD-card-wear panic. It’s just housekeeping I’d been putting off for two years.

    First, what the database is actually holding

    The Recorder integration writes a row every single time an entity changes state. Not every minute — every change. That’s the bit that gets people. A temperature sensor that reports every five minutes is nothing. A Zigbee link quality sensor that reports every time a packet lands is a firehose.

    By default Recorder keeps 10 days of that history (purge_keep_days), purges nightly at 04:12 local time, and repacks the database every second Sunday after the purge. All of that is on out of the box, and all of it was working correctly on my system. My database wasn’t 5.8 GB because purging was broken. It was 5.8 GB because ten days of my entity churn is genuinely that big.

    Worth knowing before you start cutting: long-term statistics are a separate thing. Those hourly and five-minute rollups that feed the Energy dashboard and your year-over-year graphs are not deleted by purge_keep_days. You can be aggressive with state history and still keep the long arc of your energy data. That distinction is the single most useful thing to understand here, and it’s the reason I stopped being precious about raw states.

    Measuring before cutting

    I didn’t want to guess. The SQL integration can point at the Recorder database directly, and the docs give you the query for size:

    SELECT ROUND(page_count * page_size / 1024 / 1024, 1) as size
    FROM pragma_page_count(), pragma_page_size();

    Add that through the UI, use size as the value column, set the unit to MiB and the device class to Data size. If you’re on Recorder’s default SQLite you don’t even need to supply a database URL — it finds it. Now you have a sensor, which means you have a graph, which means you can actually see whether a change helped instead of vibing it.

    To find the offenders themselves I sat with the History panel and the entity list for twenty minutes, sorted by “last changed”, and wrote down anything that had updated in the last sixty seconds and that I could not imagine ever wanting to look at historically. That list was depressingly easy to build.

    A stack of opened hard disk drives on a white surface
    Storage is cheap. Restore time, backup upload time, and repack headroom are not. Photo via Pexels.

    What I excluded

    Recorder filters live in configuration.yaml, not the UI. Here’s roughly what mine looks like now, trimmed of the site-specific bits:

    recorder:
      purge_keep_days: 7
      commit_interval: 30
      exclude:
        domains:
          - automation
          - update
          - device_tracker
        entity_globs:
          - sensor.*_linkquality
          - sensor.*_rssi
          - sensor.*_signal_strength
          - sensor.*_last_seen
          - sensor.*_uptime
          - sensor.sun*
        entities:
          - sensor.date
          - sensor.time
          - sun.sun

    The entity_globs block is doing almost all of the work. Every Zigbee device I own publishes a link quality value and a “last seen” timestamp through Zigbee2MQTT, and with a few dozen devices on the mesh those two patterns alone were the largest single category of rows in the database. I care about link quality when I’m debugging a mesh, and when I’m debugging a mesh I want live values, not last Tuesday’s. The update domain was another one — an entity per add-on and integration, all of them writing rows, none of them interesting after the fact.

    Two syntax notes from the docs that saved me a restart: globs support * for zero-or-more characters and ? for zero-or-one, and if you mix include and exclude the precedence rules are specific enough that it’s worth reading them rather than assuming. I stuck to a pure exclude list precisely so I wouldn’t have to think about precedence. Allowlists are tidier in theory; in practice an allowlist means every new device you add is silently not recorded until you remember to update the file, and I know myself.

    I also raised commit_interval from the default 5 seconds to 30. The docs suggest this mainly for SD-card installs, but there’s no reason a mini PC needs to fsync twenty times a minute either, and the history and activity panels don’t lag as a result — they get changes streamed to them immediately regardless of when the write hits disk.

    The repack nobody warns you about

    Excluding entities stops new rows. It does not remove old ones, and it does not shrink the file. SQLite hangs on to the pages. To actually reclaim space you call recorder.purge from Developer Tools with repack enabled, and then you wait.

    Here’s the part I’d underestimated: a repack needs room. The Recorder docs are blunt about it — you need at least as much free disk space as the database occupies, at all times, because a rebuild or repack can copy the data on disk. And in the disk-corruption case, where SQLite moves the broken database aside and starts a fresh one to keep you online, they want 2.5x free. On a 5.8 GB database on a small SSD, that stops being a rounding error. Check your free space before you press the button, not after.

    If you want to nuke history for specific entities without waiting for the retention window, recorder.purge_entities takes entity IDs, domains or glob patterns. I used it once on the link quality sensors and it did in a minute what dropping purge_keep_days would have taken a week to do.

    What I chose not to do

    I did not move to MariaDB. It’s supported — MariaDB 10.3+, MySQL 8.0+, PostgreSQL 12+ all work — and if you’re running a big installation with Grafana on the side there’s a real argument for it. But the Recorder documentation says plainly that SQLite is the most tested option and that recent Home Assistant versions are heavily optimised for it, and it warns that choosing something else makes you the database administrator, including for backups of that external database. My whole reason for running bare-metal HA OS is that there’s exactly one thing to back up. Adding a second database engine to save a few hundred milliseconds on history queries I run twice a month is a bad trade for me. It might not be for you.

    I also didn’t go below seven days of retention. I was tempted by five. But roughly once a quarter something misbehaves on a weekend and I don’t get to it until the following weekend, and a history window shorter than that turns “let me look at what happened” into “let me guess at what happened”. Seven days is the number where I stop losing the ability to debug my own house.

    Was it worth it?

    After the excludes, a purge and a repack, the database settled at just under 700 MB and has stayed roughly flat for a week. That’s an eight-fold reduction, and the honest answer to “did anything get faster?” is: the dashboard history graphs load noticeably quicker, and I have lost nothing I actually miss.

    The bigger win is less visible. My nightly backup is now small enough that uploading it offsite is a non-event, and the next time I do something reckless to this machine, the restore won’t be dominated by a file full of signal strength readings from a fortnight ago. That’s the whole point. The database exists to help me understand my house, not to archive the noise the house makes while it’s working.

    If you’ve never checked, go check. Settings → System → Storage will tell you the shape of it in about ten seconds, and the SQL sensor above will keep telling you afterwards. Mine had been quietly growing since 2024 and I only noticed because I broke my own system on purpose.

  • Home Assistant 2026.8 Ships a llama.cpp Brain. My CPU-Only Mini PC Volunteered.

    Home Assistant 2026.8 Ships a llama.cpp Brain. My CPU-Only Mini PC Volunteered.

    Home Assistant 2026.8 landed on August 5 with the tagline “Approachable by design,” and the headlines went to the friendly stuff: new installs answer on port 80 instead of :8123, “Developer Tools” is now just “Tools,” and around 43 “advanced” labels got quietly deleted from the interface. All fine. But scroll down to the new-integrations list and there it is, sitting between Lyngdorf amplifiers and pool controllers: a native llama.cpp integration. Home Assistant can now use a local language model as a conversation agent, out of the box, with no cloud account anywhere in the loop.

    For a house that runs everything on a used HP EliteDesk 800 G4 and treats cloud dependencies as a bug, this is the most interesting line in the entire release post. So I spent the weekend finding out what it actually means when your AI brain is a CPU-only mini PC that also has a day job.

    What actually shipped

    Two things, really. The llama.cpp integration (contributed by allenporter, who also maintains a good chunk of HA’s AI plumbing) lets you point Home Assistant at a local llama.cpp server — or any endpoint that speaks the OpenAI-compatible chat completions API — and use it as a conversation agent for Assist. You can set up several agents, each with its own model and its own instructions. Alongside it came a LiteLLM integration, which does the same trick through a proxy that can front almost any model provider, hosted or self-hosted.

    The important nuance: Home Assistant is not running the model itself. There’s no bundled inference engine, no add-on that magically eats your RAM. You bring your own llama.cpp server, and HA talks to it over HTTP. That’s the right call, in my opinion — inference is exactly the kind of workload you want to be able to place, throttle, and kill independently of the process that unlocks your front door.

    The setup, honestly documented

    My HAOS box is an EliteDesk 800 G4 mini with an i5-8500T and no GPU worth mentioning. Since HAOS is deliberately not a general-purpose Linux box, the llama.cpp server has to live somewhere else — in my case a second machine on the LAN, though a container on the same host works too if you run Supervised or Core. llama.cpp’s llama-server gives you the OpenAI-compatible endpoint with one command and a GGUF model file.

    On the Home Assistant side, the flow is what you’d expect from a 2026-era integration: add it from the UI, give it the server URL — and note that the integration wants the full path including protocol, port, and the /v1 suffix, which is the one place I fumbled — then pick your options. You can write the agent’s instructions using Home Assistant templating, choose whether the model can control entities or only answer questions about them, and either accept recommended sampling defaults or set max tokens, temperature, and top-p yourself. Crucially, the model only ever sees entities you’ve explicitly exposed to Assist. That boundary matters more with LLMs than it ever did with intent matching, because a language model will cheerfully improvise with whatever context you hand it.

    Close-up of a smart speaker on a wooden table
    The microphone is the easy part. The question is whose computer does the thinking. Photo: Pexels

    What a CPU-only box can honestly do

    Here’s the part the release notes won’t tell you, because it depends entirely on your hardware. On an 8th-gen desktop CPU with no accelerator, small quantized models — the 3B-parameter class at Q4 quantization — are the realistic ceiling. They fit in a few gigabytes of RAM and produce answers at a pace I’d describe as “thoughtful colleague,” not “instant assistant.” Single-digit tokens per second, in my experience. A 7B or 8B model runs, technically, the way a loaded moving van technically goes uphill.

    Whether that’s acceptable depends on what you ask of it. For the queries where an LLM genuinely earns its keep — “which windows are still open?”, “summarize what happened overnight” — waiting a few seconds for an answer that’s actually synthesized from my home’s state feels fair. For “turn off the kitchen lights,” it’s absurd, and you shouldn’t use it for that anyway: Assist’s built-in intent engine handles the direct commands instantly and deterministically. The pattern that works is local intents first, LLM as fallback — which Assist has supported for a while and which turns the slow model into a specialist you consult, not a bottleneck you live behind.

    The same release also ships GPT-5.6 support. That’s the point.

    I want to be fair here: 2026.8 also updated the OpenAI Conversation integration with GPT-5.6 support, and the Anthropic integration just reached platinum on the quality scale. The cloud models are better at this. Dramatically better. If you want an assistant that understands “make it cozy in here” with all its implied ambiguity, a frontier model through an API will outclass anything my EliteDesk’s neighbor can serve.

    But that’s always been the trade, and what 2026.8 changes is that the local end of it is now a first-party, UI-configured choice rather than a custom-integration adventure. The community has been wiring llama.cpp into HA for years through projects like Home LLM and various OpenAI-shim workarounds. Those deserve credit — they proved the demand. Having it in core, with entity exposure controls and proper config flow, is the difference between “possible for people who read forums” and “possible.”

    Should you bother?

    If you have any spare box with decent RAM — or better, anything with a GPU or a recent Mac — yes, try it. The marginal cost is an afternoon. If your entire smart home is one Raspberry Pi, no: the integration needs an external inference server, and that’s not your Pi’s fight.

    My own verdict after a weekend: it stays, as a fallback agent with a small model, answering the questions that intent matching can’t. It’s slow, occasionally confidently wrong about which sensor is which, and entirely mine. Every query, every response, every half-second of my house’s state stays on hardware I can unplug. In a release themed around approachability, the most on-brand feature isn’t the missing port number — it’s that running your own AI stopped requiring anyone’s permission.

    Primary sources: the Home Assistant 2026.8 release notes, the llama.cpp integration docs, and the llama.cpp project.

  • Zigbee2MQTT Stopped Trusting External Converters by Default. Mine Have Run Strangers’ Code for Years.

    Zigbee2MQTT Stopped Trusting External Converters by Default. Mine Have Run Strangers’ Code for Years.

    Buried in the release notes of Zigbee2MQTT 2.11.0, one line stopped me mid-scroll: “External extensions and external converters are now disabled by default for new installations.” My install predates the change, so nothing broke. Nothing even flickered. But I sat there with my coffee thinking about the JavaScript files I’d pasted into that folder over the years — code I’d copied from GitHub gists and forum posts, written by strangers, running with full access inside the process that controls every Zigbee device in my house.

    So this week’s post is half news, half confession, and half checklist. Yes, that’s three halves. It was that kind of audit.

    What actually changed

    Since version 2.11.0 — and still the case in the current 2.12.0 release from June — Zigbee2MQTT ships with external converters and external extensions off by default on new installations. If you need them, you now opt in explicitly with enable_external_js: true under the advanced section of your configuration. The change was contributed by Nerivec, who has been doing a lot of quiet hardening work on the project lately — the frontend also now refuses to save external JS files with invalid names.

    Existing installations are untouched. If your converters were working last month, they still work today. This is purely a change in what a fresh install trusts out of the box — which is exactly how breaking changes should be done, and the project deserves credit for it.

    Why this was overdue

    The official security documentation puts it plainly: external converters “execute arbitrary user-provided JavaScript code within the Zigbee2MQTT process,” and malicious or buggy code “can compromise the entire Zigbee2MQTT instance, and potentially the host system.”

    Think about what lives in that process. Your Zigbee network key. Your MQTT credentials. A direct line to every lock, relay, and sensor in the house. And the culture around external converters has always been wonderfully casual: you buy some obscure Tuya sensor for twelve francs, it pairs as “unsupported,” someone on a forum says “just drop this converter in,” and you do, because you want the thing working before dinner. I have done exactly this. More than once. The code was probably fine. “Probably fine” is not a security model.

    Smart home sensors and devices laid out on a table
    Every one of these needs a converter to speak MQTT. Where that converter comes from matters. Photo: Pexels

    The audit one changelog line triggered

    After last week’s USB humiliation, my faith in “it’s been fine for years” is at an all-time low. So I SSH’d into the EliteDesk and actually looked at my external_converters folder. Three files. Two of them were for devices that gained official support ages ago — the docs even recommend this lifecycle: submit your converter upstream as a pull request, then delete the local copy once it ships in a release. I’d done the first part and skipped the second. Both deleted. The third, for a genuinely unsupported presence sensor, I actually read line by line for the first time since installing it. It’s clean. Now I know that, instead of assuming it.

    While I was in there, I worked through the rest of the project’s securing guide, and I’d encourage you to do the same, because some of these defaults are sneakier than the converter one:

    The network key. If your network is old enough, it may still be using the historical default key — the docs specifically call out [1, 3, 5, 7, 9, 11, 13, 15, 0, 2, 4, 6, 8, 10, 12, 13] and strongly suggest changing it. A default encryption key is a published encryption key. The fix is setting network_key: GENERATE, and the price is re-pairing every single device, which is why so many of us keep putting it off. Mine was already randomized from a rebuild two years ago; if yours isn’t, this is the least fun and most important item on the list.

    The frontend. By default it listens on 0.0.0.0 — all interfaces — and anyone who can reach it has full control, same as MQTT. Mine is now bound to localhost, with an auth_token stored in secret.yaml rather than the main config. If you only ever open the frontend through Home Assistant on the same box, there is no reason for it to be reachable from your whole LAN.

    File permissions. configuration.yaml contains your MQTT credentials and network key in plain text. chmod 600 on it and on secret.yaml took ten seconds.

    Joining habits. Permit-join is temporary by default (254 seconds), but I’m the sort of person who opens it, gets distracted by a compile error, and leaves it. Closing the window manually after pairing is now a habit. For devices that ship with install codes — increasingly common on newer gear — use them; they encrypt the initial key handover to the joining device.

    OTA sources. Zigbee2MQTT pulls firmware from the curated Koenkk/zigbee-OTA repository by default. Leave it that way unless you have a very good reason. A custom OTA index URL is a supply chain you’re choosing to trust with your door locks.

    What I’d do this weekend, in order

    If you run Zigbee2MQTT, here’s the priority list as I see it. First, open your external_converters folder and delete anything for a device that’s since gained official support — check the supported devices list, you’ll probably be surprised. Second, read what remains; it’s usually under a hundred lines. Third, check whether you’re on the default network key, and if you are, schedule the re-pairing weekend and just eat the cost. Fourth, bind the frontend to localhost and add a token. None of this requires new hardware or breaks anything that works.

    And if you’re setting up a fresh instance: leave enable_external_js off until the day you actually need it. That’s the default now, and it’s the right one.

    There’s a comfortable myth in this hobby that “local-first” automatically means “secure.” It doesn’t. It means the vendor’s cloud isn’t your problem — and everything else is. The Zigbee2MQTT project just made the everything-else slightly smaller for new users, and gave the rest of us a well-timed nudge to look at what we’ve been running. Take the nudge.

  • ESPHome Retired the Dashboard I Knew. Here’s the Checklist I Ran Before Updating My Fleet.

    ESPHome Retired the Dashboard I Knew. Here’s the Checklist I Ran Before Updating My Fleet.

    The ESPHome dashboard I’ve been staring at for years is gone. With the 2026.6 release, the old text-editor-with-a-device-list has been retired and replaced by the new Device Builder, which just hit 1.0.0 and is now the dashboard bundled by default in the official Home Assistant ESPHome add-on. There’s no opt-in toggle anymore — the preview switch from 2026.5 is gone. If you update the add-on, this is simply what you get.

    I run a dozen-ish ESPHome nodes off my bare-metal Home Assistant box, so “the tool I use to manage all of them got replaced” is not something I update casually. Here’s what I found when I went through the release notes properly, the checklist I ran before touching anything, and my honest take after living with the new builder for a few days.

    What the Device Builder actually changes

    The headline framing in the announcement post is beginner-friendliness: a visual component catalog, guided configuration, fewer datasheet treasure hunts. My first reaction to “visual builder” was mild dread — these things usually mean the YAML gets hidden behind three clicks. That’s not what happened here. The CodeMirror YAML editor is still front and centre; the visual layer sits alongside it, and you can flip between “steps” and “code” views of the same config.

    For someone managing an existing fleet, the genuinely useful parts are less glamorous than the catalog. There’s now a firmware job queue: compile, install, and clean jobs run in the background with progress and history, instead of the old one-operation-at-a-time model where a long compile held your browser tab hostage. Multi-select bulk actions let you update or label an arbitrary set of devices in one go. There’s a per-board pin info viewer that shows which GPIO does what and which component is already claiming it — the kind of thing I used to keep in a text file. And every device now carries out-of-sync diagnostics: a version badge, a config-hash check, and an encryption-state mismatch warning. On my setup that immediately flagged two nodes I’d flashed from my laptop months ago and forgotten to sync back.

    There’s also a YAML diff view before install, cross-config search, and a command palette. None of this is revolutionary on its own, but together it turns fleet maintenance from a sequential chore into something closer to an actual workflow.

    The checklist I ran before updating

    The dashboard swap is the visible change, but the upgrade checklist is where the real risk lives. Two items matter for most people.

    First, and biggest: ESP8266 devices now default to WPA2 minimum (min_auth_mode: WPA2), matching what ESP32 has done for years. ESPHome says roughly 40% of installs still run on ESP8266 hardware, so this is the broadest change in the release. If your access point is anything from the last decade, nothing happens. But if you have an old device hanging off a legacy WPA-only (TKIP) network — a garage AP, an IoT VLAN on ancient hardware — that device will silently stop associating after the update unless you pin min_auth_mode: WPA under wifi:. ESPHome has been printing a deprecation warning about exactly this since January, which I will admit I had been scrolling past.

    Second: if you still load anything from a custom_components/ folder, it’s time to migrate to external_components: — the legacy path is done. Smaller items: the dsmr electricity_switch_position sensor moved from sensor: to text_sensor:, and if your time: component uses platform: homeassistant with an explicit timezone:, Home Assistant no longer overrides your configured zone. That last one is subtle and exactly the kind of thing that breaks a sunrise automation three weeks later when you’ve forgotten you updated anything.

    Soldering tools and a circuit board laid out on a workbench
    The fun part of ESPHome has always been the workbench. The new builder mostly stays out of its way. Photo: Pexels

    The quiet wins: memory you already paid for

    My favourite change in 2026.6 isn’t the dashboard at all. If you set enable_on_boot: false on WiFi — the standard trick for battery-powered sensors that wake, transmit, and sleep — the WiFi driver previously stayed resident in memory anyway. Only the start() call was skipped. Now the heavy allocation is lazy, so a dormant WiFi interface genuinely costs zero internal RAM: roughly 15–30 KB of DMA-capable SRAM reclaimed, with ethernet getting the same treatment for another 3–8 KB. On an ESP32-S3 running audio plus a Bluetooth proxy, the ESPHome team measured free internal SRAM under peak load going from about 14 KB to 32 KB. On constrained boards, that’s the difference between “works” and “reboots at 2 a.m.”

    Config validation also got dramatically faster — up to 4.5× on LVGL-heavy configs — which matters now that the builder revalidates on every save. And the audio stack was reworked with zero-copy ring buffers throughout the voice assistant and media player paths. I don’t run ESPHome voice satellites yet, but the direction is clear: the platform is being tuned for heavier workloads on the same cheap silicon, which is very much the local-first bargain I signed up for.

    My take after a few days

    I updated the add-on on my EliteDesk the evening the release landed in my update list, with one deliberate precaution: I ran a full backup first and validated every config before flashing anything, then pushed updates to devices one at a time — the same rule I follow for Zigbee OTA updates. Nothing broke. My single remaining ESP8266 node (a basement humidity sensor) kept associating because my AP has been WPA2 since forever.

    The Device Builder itself? It’s better than what it replaced, and I say that as someone who was perfectly happy with the old text editor. The job queue and the out-of-sync badges alone justify the switch for anyone with more than a handful of nodes. The visual catalog I’ll probably use twice a year, when I’m wiring up something unfamiliar and want the pin map without opening a datasheet PDF. The important thing is what they didn’t do: the YAML is still the source of truth, still diffable, still mine. As long as that holds, I’m happy to let the training wheels exist for people who need them.

    One thing worth watching: the announcement mentions an official ESPHome Starter Kit made with Apollo Automation, shipping soon. An officially blessed hardware on-ramp plus a beginner-friendly builder is a clear play to grow the community beyond the tinkerer crowd. Given the alternative is another generation of cloud-tethered gadgets, I’m for it.