michaelharley

joined 5 months ago
 

Welp. My Forgejo instance got popped with an RCE two days ago by CVE-2026-60004. Luckily, I noticed the following morning and had the day free to figure out what happened. Let's dive in!

As a homelab enthusiast, I found this a very interesting post. Here are my take aways from the post that I'm implementing myself:

  • Miner detection. I've updated monitoring rules to now watch the CPU on my hosts. If the same thing happened to me I would not have been alerted at all as I'm doing simple up / down monitoring. Fixed.
  • Access logging. I turned on access logging for my homelab Caddy instances.
  • Log retention. I have increased the amount and retention of my logging. The hope is this will help me reconstruct what happened after a breach.
  • Logs offsite. The VPS access logs now ride along with the normal backup process, which runs hourly. The homelab side still only gets caught by the weekly VM backup, so that's next.
  • Closed an open signup. My webtrees instance (genealogy) had self registration enabled, which is the same door this guy got hit through. Oops. Fixed.
  • Built a tool. log-inventory.sh, so "could I actually reconstruct what happened" is a command I run instead of a thing I assume.
[–] michaelharley@infosec.pub 6 points 2 weeks ago

It's a system76 Launch Heavy. It's expensive but it's the best keyboard I've ever owned.

 

Polly is helping out at work today. She usually likes to spend her day outside but it's supposed to be hot today!

[–] michaelharley@infosec.pub 1 points 3 weeks ago

I have one of the nabu casa voice devices. When I first got it, I experimented with it and I found it wasn't as nice as an Apple HomePod Mini. Which is fine but I was mostly irritated that I couldn't set my own wake word. It has like 3 you can choose from but I want a custom one. I want to be able to say 'hey robot' to wake it up.

But to your point, it acts just like the other voice assistant devices. The speaker isn't for music but it's fine. I'd probably want to plug it into some sort of powered speaker if I was using it full time. I need to come back to it and run try to use it.

Really all we do with our HomePods is set timers and to control basic things liek turning the lights off and on or closing/opening the curtains.

The moment I can say 'hey robot, set a timer for 5 minutes' or 'hey robot, open the curtains' is the moment i buy enough to replace all the Apple HomePods.

[–] michaelharley@infosec.pub 1 points 3 weeks ago

Is there some cheap and simple way to watch water values with home-assistant? Specially nitrate, nitrite pH and so on. Not that I've been able to find. I use the API master test kit instead of strips, but I still struggle with the colors sometimes. Especially the nitrates 40 to 80 range.

 

I feel the aquarium hobby is a retro hobby. Sure, the lights and filters have got much more expensive, and now rimless tanks are popular online, but the standard size tanks like the 40 breeder and the 75 gallon are the same footprint from decades past.

What can I automate? Can I improve monitoring and alerting? There are some off-the-shelf options like Neptune Systems Apex, but what could a person do with Home Assistant? That's what I'm trying to figure out.

Hardware

My home automation platform of choice is Home Assistant. This runs bare metal on a little mini PC NUC with the following specs:

  • System76 Meerkat (Intel NUC10i3FNB board)
  • Intel Core i3-10110U @ 2.10GHz, 4 threads
  • 16 GB RAM
  • 228 GB disk
  • Home Assistant OS
  • Zigbee: ZBT-2 and ZHA
  • Z-Wave: ZWA-2 and Z-Wave JS

To enable all the automations I'm writing about here, each aquarium gets these smart devices. When relevant, I will call out the specific bit of aquarium gear I picked for how I need it to work with Home Assistant.

  • Power Strip: Zooz Zen20 Power Strip. This has 5 regular plugs and 2 USB plugs, all of which can be individually controlled. This runs on my Z-Wave network. Each aquarium gets one.
  • Temperature Sensor: Apollo TEMP-1 Temperature Sensor. This allows me to get the temperature into Home Assistant; it runs on ESPHome and connects via Wi-Fi. If aquariums are close enough together, a single sensor can have multiple probes.
  • Water Leak Sensor: Zooz Water Leak Sensor. At least one goes into each plant saucer/catch tray that I use with my canister filters.
  • Smart Button: Aqara Mini Switch. I can use these buttons at the tanks to trigger automations without having to fish out my phone. Single press, double press and long press are all available to do things like toggle the lights or activate maintenance mode.
  • Automatic Feeder: Avast Labs Plank MINI. These feeders have no built-in schedules or batteries and that's what I like. When there's power to the unit, the corkscrew turns, dispensing food. To stop dispensing, turn off the power.
  • Heater: Bulk Reef Supply Titanium Heater Element. These do not have built-in thermostats and plug directly into the Zen20. When the plug is powered on, it heats. To stop the heat, turn off the power. Easy peasy. The smallest these go are 100W and it's what I use in my 20 gallon long aquariums. In the 75 gallon display tank, I use two 200W heaters.
  • Filters: Eheim Classic Canister Filter & Aquarium Co-Op Coarse Sponge Filter. I like having both a canister and sponge filter in my tanks. If my canister is clogged and I don't notice, the sponge can carry things until I do. In the event of a power outage, I can run the air pump for the sponge filters off a battery pack for a good long time. Finally, having a seeded sponge filter at all times is nice for quickly spinning up a new tank.
  • Air Pump: Eheim Air. This pump has been the quietest in my experience.
  • Lights: Hygger HG999. This is just a budget light but it's a good price and my plants seem happy. Works fine powering it off and on from the power plug.

A well-decorated freshwater aquarium with green plants, driftwood, and a skull decoration on sandy substrate. Several small fish swim among the vegetation and ornaments under blue lighting.

Aquarium 2! This is my "shrimp tank", which I put white cloud mountain minnows with. I'm rehoming the shrimpies and the minnows into a hillstream tank which is cycling now! (Richmond, VA · August 2026)

Lighting

Probably one of the easiest and most forgiving things to automate is the lighting schedule. I use Hygger HG999 lights with my tanks and they're fine for the price. If I plug the light into a smart plug, then it's easy to set a simple schedule to turn the lights on and off. The HG999 does have its own little controller, but it restores its previous state on power restore. So I just turn it on for the first time, set the brightness I want, and control it with Home Assistant via the smart plug.

Helpers

  • Lights On Time: Date and/or time. Time only. Example: 7:30 a.m.
  • Lights Off Time: Date and/or time. Time only. Example: 4:30 p.m.
  • Lights On: Template binary sensor. It converts Lights On Time/Lights Off Time into an on/off state that then drives the automations. Example: On

Automations

Each tank gets two automations for lighting.

  • Lights On: When the Lights On template binary sensor turns On, this fires and turns the lights on.
  • Lights Off: This is the off version! When the Lights On template binary sensor turns Off, this fires and turns the lights off.

Dashboard

The dashboard gives me exactly one control for the lighting.

Dashboard for Aquarium #1 showing status alerts including a dry leak and overdue water change, with controls for main power, filter, heater, light, and feeder, along with settings for temperature setpoint (75.0°F), feed schedule (8:45 AM), and other aquarium parameters.

I have controls on each tank dashboard to do a manual feed, to adjust when the feed happens, a feeder master switch and the feed seconds.

Improvements

I plan to add a WLED light strip to each tank. The Hygger lights have morning, evening and night modes, which basically set the colors at a dim level. It's really more aesthetic, but I would like to be able to replicate that with my setup because I think it's cool. Plus, it seems the sudden on of the main light startles the fish, so I'd like to ramp up in the morning and ramp down in the evenings.

A vibrant blue and red betta fish swims among green aquatic plants and brown leaf litter on the dark substrate of an aquarium.

Tina! (Richmond, VA · August 2026)

Feeding

Feeding the fish is one of my favorite things about fish keeping. So why then would I want to automate it? Well, mostly to see if I can. But secondarily, it means I don't have to feed the fish every day if I don't want. I let the autofeeders dispense the dry pellets and I will supplement with frozen treats when I'm in the mood. Plus, if I automate the feeding like this, then I don't need to recruit anyone to come feed the fish while I'm away.

I knew I wanted to use automatic feeders with my fish. In my research, most hobbyists seemed to dismiss the idea as pointless and maybe even dangerous. Most of the anecdotes I saw were people using cheap barrel feeders like the Eheim Autofeeder while they were away, coming back to dead fish because the feeder dumped too much food into the aquarium.

Naively undeterred, I started experimenting with what was actually possible. I did try the cheap barrel feeders and yeah, they're not great. The dispensing bit is clunky and not very precise, plus there's no way to have a fasting day schedule with the barrel feeders I tried (Eheim and Aquarium Co-Op). That's where Home Assistant and the Avast Labs Plank MINI come in. This is a dumb feeder. If I plug it up, it runs the motor that spins the auger to dispense food. I then power off the plug to stop it. Hell yeah.

It actually took me a long time to find this feeder because I didn't know what to type into the search bar. Avast makes a regular-sized Plank, which is expensive. The mini is also expensive compared to the battery powered barrel feeders but I just squint my eyes and hit the buy button. Buy once, cry once, I always say.

This automation could be dangerous for my fishy friends so I spent a fair amount of time thinking through the edge cases and how this could fail. Accidentally dumping the full canister of food into the tank because a run got stuck would be a critical incident. Let's try not to do that.

The other failure mode is less critical. That's the feeder not dispensing food when I think it is, so I've also built some automations and monitors to try to catch this failure.

Helpers

  • Feeding Enabled: Toggle. This allows me to arm and disarm the automatic schedule, should I for some reason want to turn off the automatic feeder without having to go around disabling automations. Example: On
  • Feed Time: Date and/or time. This is when the food drops. Example: 7:45 a.m.
  • Feeder Seconds: Number. This is the number of seconds the motor should run. I ran a few test runs of the feeder to see how much it dispensed and I continue to tweak this as needed. Example: 35
  • Feeder Active: Toggle. The automations and scripts set this helper during legitimate feeding paths. Example: Off
  • Last Verified Feed: Date and/or time. I use this helper to work out whether a scheduled feed actually happened. Example: August 24, 2026 at 7:45 a.m.

Automations

  • Feed: The Feed Time helper triggers this automation. When the time arrives, it toggles the Feeder Active helper on, powers the feeder on, and the auger starts to dispense food. It waits the number of seconds I set in Feeder Seconds, then it powers the feeder off and toggles the Feeder Active helper off.
  • Cutoff: This automation monitors for every edge case that I could think of that would cause an outlet to be on when it shouldn't be, then turns off the feeder and sends out all the alerts to all the places. There are three triggers:
    • Overrun: If the outlet has been on past the max feed window, then this trigger executes. The max feed window is a hard-coded value of 120 seconds.
    • Unexpected on: This trigger fires if the outlet came on with no feed running. A ghost Z-Wave command, a hand tapping the strip, a stale state restore. This branch only works because the automation switches the Feeder Active toggle helper on during a real feeding session. So the condition boils down to this: the plug on the feeder is powered on and the Feeder Active toggle helper is off.
    • Startup recovery: This trigger fires if the outlet was already on when Home Assistant booted, which probably means HA rebooted mid-feed.
  • Feed Verify: This runs a few seconds into a feed and checks the outlet is actually drawing current. If it is, it stamps Last Verified Feed. A relay can report a perfectly clean feed into a dead outlet and this is the automation that catches it.
  • Feed Watchdog: This runs 15 minutes after the feed was due and alerts me if nothing stamped Last Verified Feed today. This one is for a feed that never ran at all.

Dashboard

Dashboard for Aquarium #1 showing status alerts including a dry leak and overdue water change, with controls for main power, filter, heater, light, and feeder, along with settings for temperature setpoint (75.0°F), feed schedule (8:45 AM), and other aquarium parameters.

I have controls on each tank dashboard to do a manual feed, to adjust when the feed happens, a feeder master switch and the feed seconds.

Heaters

Aquarium heaters are notoriously unreliable. The general advice on them is that they're so unreliable that fish keepers should replace theirs proactively, every 2 or 3 years. The main thing is the cheaply made thermostats on the heaters can fail in the on position, cooking all the fish in the tank. Or the glass can break, causing an electrocution risk.

Sources:

Hobbyists recommend using a separate controller, like an INKBIRD. Then I'd just turn the thermostat up on the aquarium heater and let the INKBIRD control the temp. The INKBIRD is higher quality, one presumes, so is less likely to catastrophically fail. Instead of an INKBIRD, I decided to use Home Assistant as my controller.

To run the heaters, I use the Zooz Zen20 Power Strip, Apollo TEMP-1 Temperature Sensor, and the Bulk Reef Supply Titanium Heater Element. I'm choosing the BRS heater because it does not have a built-in thermostat. I power on its plug to run the heater and I turn off its plug to turn off the heater.

I've seen people say to get a standard aquarium heater and to set the temp a few degrees above the target so the external controller can drive the heater, but if the controller fails in some way, then the built-in thermostat can be the backstop. I reject that because if I don't trust the thermostat enough to run the heater, why would I trust it to be the backstop?

This is the most dangerous automation. A heater stuck in the on position can kill the fish in hours, so I've really spent a lot of time and energy to ensure I've accounted for the failure modes and edge cases that could cause the automations to fail. So far so good.

A colorful fish with a reddish-orange chest and spotted gray-blue body swims in an aquarium among green aquatic plants, driftwood, and fallen leaves. The tank features dark water with hanging root systems visible at the top.

A male pearl gourami in the 75 gallon tank downstairs. (Richmond, VA · August 2026)

Helpers

If I were building this today, I'd just use the Generic Thermostat integration but I built this before I knew about the integration. There's little value that I can see to migrate to the integration so I'm just going to keep using my hand rolled solution.

  • Set Point: Number. This is the target temperature of the aquarium. Example: 78
  • Hysteresis: Number. I didn't know what this meant before working on this project but hysteresis is the gap you deliberately leave between the point where something switches on and the point where it switches off, so it doesn't flap back and forth every time the measurement wobbles across a single number. If I set the hysteresis at 1, then at 77.5, the heater turns on. At 78.5 it turns off. Hysteresis is basically the band of the swing. Example: 1
  • Call For Heat: Template binary sensor. It reads the temperature probe, then compares against the Set Point and half the Hysteresis, then returns true or false depending on the target temperature and the actual temperature. It also reads its own current state, which is what creates the band: once it's on it stays on until the top of the band, and once it's off it stays off until the bottom. If the sensor is not returning a value, say the probe got unplugged, then it falls back to false so a heater can't get stuck on.
  • Maintenance Mode: Toggle. Every tank gets a helper that allows me to put the tank into maintenance mode. This stops the filters and disables the heater so it's not running during a water change. It also holds off the temperature alerts, the sensor offline alert and the filter watchdog, because none of those are news while I've got my hands in the tank. Example: Off
  • Temp Offset Alert: Number. This is the maximum amount of temperature swing, compared to Set Point, allowed before the system starts alerting. Example: 3

Automations

I use several classes of automations to run the heaters and verify everything is working correctly.

  • Heater On: There are three triggers for this automation. When Call For Heat flips to true, when Home Assistant restarts, and when Maintenance Mode turns off. This automation is gated on Call For Heat, so when Home Assistant restarts, if Call For Heat is off then nothing happens. It's also gated on Maintenance Mode. If it's on, then do nothing because I'm doing a dang water change.
  • Heater Off: There are also three triggers for this automation. When Call For Heat flips to false, when Maintenance Mode turns on and when Home Assistant restarts. Both Heater On and Heater Off fire on a reboot, so this one also carries the appropriate conditions to let the correct mode win.
  • Low Temperature Alert: This triggers when a tank has drifted below setpoint by more than the alert offset.
  • High Temperature Alert: This triggers when a tank has drifted above setpoint by more than the alert offset.
  • Heater Stuck On: This triggers if the relay reads off while the heater outlet is still drawing over 20W. This is a critical automation and I've experienced this exact condition.
  • Temperature Sensor Offline: This triggers if the temperature sensor is offline for more than five minutes.

Dashboard

Dashboard for Aquarium #1 showing status indicators including temperature at 74.6°F, energy at 12W, and various system controls like main power, filter, heater, and light switches, along with settings for temperature setpoint and hysteresis levels.

The dashboard for aquarium 1. At the bottom are the controls and dials for the temperature automations and helpers.

Leak detection

I have no idea if canister filters are more prone to leaking than hang-on-back filters, but if one of mine springs a leak, I'd like to know it. So at a minimum, each of my tanks gets a Zooz Water Leak Sensor that goes into the leak tray of each filter. I have a holding/QT tank that uses an undergravel filter and a hang-on-back. For that one, I put the leak sensor right under the hang-on-back in case it overflows.

I treat leak detection alerts as critical, so they play no matter the time of day or night.

Helpers

  • Leak Detected: Toggle. When a leak sensor detects water, the automation toggles this On.
  • Leak Detected Time: Date and/or time. This records the date and time of an active leak.

Automations

  • Leak Alert: This automation fires when a leak sensor detects water. It repeats every 5 minutes while the probe reads wet, capped at 12 passes, so an unanswered leak nags for an hour and then stops. Each pass plays a prerecorded clip at full volume and sends a high-priority push alert to my phone.
  • Leak Latch: This automation fires when a leak sensor detects water. It turns on the Leak Detected helper and sets the Leak Detected Time helper. I do this so a leak detection must be manually cleared.
  • Leak Reset: This is actually a script, but it's what resets the Leak Detected and Leak Detected Time helpers. I clear the leak from the dashboard.

Dashboard

Dashboard displaying status of six aquariums with temperature and energy readings, operational modes, leak status, and a shared light schedule showing lights on from 8:00 AM to 4:00 PM.

I show the leak status in several places in the UI. Here on the fish dashboard, plus the alert dashboard and each individual tank dashboard.

Alerts

Since Home Assistant is running important life support for my fishy companions, I've put a fair amount of thought into making my alerting system as robust as possible.

Everything related to life support gets both a voice announcement played loudly over our house speakers and a high-priority alert sent to my phone, with no consideration for hour of the day. If I have a water leak, then yes I want to be woken up so I can deal with it.

I prefer the Nabu Casa voices over the Piper voices, so I do use a cloud-based service for my TTS. What happens if there's a critical alert during an internet outage? The alert fails is what! To mitigate that, I made prerecorded announcements with the Nabu Casa voices, then I just cast the announcement over my local network to my media devices, no internet required.

An orange and brown pleco catfish forages along the gravelly bottom of an aquarium, with aquatic plants, driftwood, and scattered pebbles visible in the tank.

Super Red Bristlenose in the 75 gallon display tank downstairs. (Richmond, VA · August 2026)

Conclusion

So what can I automate and monitor? A fair amount, but nothing really groundbreaking, it turns out. I'm happiest with my solution for the automatic feeders. All the rest of it is mostly just nice-to-haves.

When I first started, I thought I might be able to monitor my water parameters like pH, ammonia, nitrites and nitrates, but that still doesn't seem possible. I was hoping for some probes, connected via ESPHome and reporting to Home Assistant. Alas, I use the API Master Test Kit like everyone else.

I think dosing would be trivial if I had the right type of doser, like this BRS model or even an Apollo PUMP-1 Fluid Pump for close to half the price because both work from just being powered on or off from the switch. I just manually dose my tanks weekly, plus I run low-tech tanks, so it's not strictly necessary.

I run my aquariums like a fleet and I use blueprints because if I need to adjust something, I don't want to make the same adjustment six different times. I've tried to create a system that I can replicate and stamp down when I add a new tank. Get the tank and hardware, run the onboarding script and boom, I have a tank!

Are you in the fish keeping hobby? If yes, I'd be curious how you do it. Are you strictly old school or do you have some automations? I'd be interested in hearing.

[–] michaelharley@infosec.pub 1 points 1 month ago

She was labeled as a female koi alien betta.

[–] michaelharley@infosec.pub 4 points 1 month ago (1 children)

I'm not exactly sure. I'm pretty new to scuds and got this bag of bugs thing from Phillip Fish Works. I'm fine with Tina picking them off but I hope the colony survives and Tina has some enrichment from hunting the little critters.

 

I moved Tina from the big community tank downstairs up to her very own 10 gallon tank. She was being harassed by the big male pearl gourami. So far, she's been exploring her tank and picking off scuds.

[–] michaelharley@infosec.pub 3 points 1 month ago

No. I do not use Google devices to call HA scripts/scenes. I play TTS announcements over my local network to the nest minis. The devices have physical switches on them to disable the mics and I have turned them all off.

[–] michaelharley@infosec.pub 1 points 1 month ago (2 children)

Google nest minis.

[–] michaelharley@infosec.pub 1 points 1 month ago (1 children)

Happy to! I'll do Lily, since it's the same block copy pasted five times, once per goblin.

Fair warning: I'm not a developer and there's almost certainly a tidier way to do all of this. It works though, and good enough is good enough.

The helpers

All of this is built on helpers, the ones under Settings > Devices & services > Helpers. Birthday and gotcha date are each a date/time helper set to date only, no time. I keep mine in input_datetime.yaml, but making them in the UI gets you the exact same thing:

lily_birthday:
  name: Lily Birthday
  icon: mdi:cake-variant
  has_date: true
  has_time: false
lily_gotcha_date:
  name: Lily Gotcha Date
  icon: mdi:home-heart
  has_date: true
  has_time: false

The collar needs three more: one date/time helper for the expiration date, plus two toggle helpers that my alert automation flips on and off. I made these ones in the UI so they live in .storage instead of a file, but here's the shape:

# input_datetime
lily_flea_collar_expiration:
  name: Lily Flea Collar Expiration
  has_date: true
  has_time: false

# input_boolean
lily_flea_collar_expired:
  name: Lily Flea Collar Expired
dismiss_lily_flea_collar_alert:
  name: Dismiss Lily Flea Collar Alert

I left initial: off all of them on purpose. If you set it, the next restart resets the helper back to that default and throws away whatever date you actually put in. Took me a restart or two to work that one out.

Age and With Us

These two are Mushroom template cards on the dashboard (Mushroom is a HACS install if you don't already have it). Add a manual card, hit "Show code editor" and paste. It's the same card twice, just pointed at the other helper:

type: custom:mushroom-template-card
primary: Age
entity: input_datetime.lily_birthday
icon: mdi:cake-variant
icon_color: blue
secondary: >-
  {% set b = state_attr('input_datetime.lily_birthday','timestamp') %}
  {% set by = b | timestamp_custom('%Y', true) | int %}
  {% set bm = b | timestamp_custom('%m', true) | int %}
  {% set bd = b | timestamp_custom('%d', true) | int %}
  {% set months = (now().year - by) * 12 + (now().month - bm)
     - (1 if now().day < bd else 0) %}
  {% set y = (months // 12) | int %}
  {% set mo = (months % 12) | int %}
  {%- if y > 0 %}{{ y }} yr{{ 's' if y != 1 }}{% endif %}
  {%- if mo > 0 %}{{ ' ' if y > 0 }}{{ mo }} mo{% endif %}
  {%- if y == 0 and mo == 0 %}new{% endif %}
  · {{ b | timestamp_custom('%b %-d, %Y', true) }}
tap_action:
  action: more-info

Mine reads 7 yrs 3 mo · Apr 2, 2023.

One thing worth calling out: counting whole months first and then dividing by 12 avoids the leap year drift you get from dividing days by 365.

The collar card

Another template card, this one reading the expiration helper. It's what goes green, orange, red as the days run down:

type: custom:mushroom-template-card
primary: Flea Collar
entity: input_datetime.lily_flea_collar_expiration
icon: >-
  {% set ts = state_attr(entity,'timestamp') %}
  {% set days = ((ts - as_timestamp(today_at('00:00'))) / 86400) | round(0) | int %}
  {% if days < 0 %}mdi:alert{% elif days <= 30 %}mdi:alert-outline
  {% else %}mdi:shield-check{% endif %}
icon_color: >-
  {% set ts = state_attr(entity,'timestamp') %}
  {% set days = ((ts - as_timestamp(today_at('00:00'))) / 86400) | round(0) | int %}
  {% if days < 0 %}red{% elif days <= 30 %}orange{% else %}green{% endif %}
secondary: >-
  {% set ts = state_attr(entity,'timestamp') %}
  {% set days = ((ts - as_timestamp(today_at('00:00'))) / 86400) | round(0) | int %}
  {% if days < 0 %}Expired {{ days * -1 }}d ago{% else %}{{ days }}d left{% endif %}
  · {{ ts | timestamp_custom('%b %-d, %Y', true) }}
tap_action:
  action: more-info

I compare against today_at('00:00') instead of now(). With now() the count flips over at whatever time of day I happened to put the collar on, so it'd read a day short all afternoon. Little thing, but it bugged me.

The reset script

This part isn't a card, it's a script. Settings > Automations & scenes > Scripts, create one, then use the three dot menu to edit it in YAML. Mine lives in scripts.yaml:

reset_lily_flea_collar:
  alias: Reset Lily Flea Collar
  description: Sets Lily's flea collar expiration to today + 7 months and clears alerts
  icon: mdi:cat
  sequence:
    - action: input_datetime.set_datetime
      target:
        entity_id: input_datetime.lily_flea_collar_expiration
      data:
        date: "{{ (now() + timedelta(days=213)).strftime('%Y-%m-%d') }}"
    - action: input_boolean.turn_off
      target:
        entity_id: input_boolean.lily_flea_collar_expired
    - action: input_boolean.turn_off
      target:
        entity_id: input_boolean.dismiss_lily_flea_collar_alert

And the button that fires it, which is one more template card back on the dashboard:

type: custom:mushroom-template-card
primary: Reset Collar
secondary: Expiry 7 months out
entity: script.reset_lily_flea_collar
icon: mdi:restart
tap_action:
  action: perform-action
  perform_action: script.reset_lily_flea_collar
  confirmation:
    text: Reset Lily's flea collar expiration to 7 months from today?

The confirmation is in there because I tap this on my phone and I have fat fingers.

Those two input_booleans are for my alert automation. _expired flips on when the date passes so it gets read out in our good morning announcement, and dismiss_ shuts it up once I've actually heard it (otherwise it just keeps announcing it, every single morning, forever). If you're not doing alerts you can drop those steps and the helpers along with them.

Hope that helps! Let me know if something doesn't work, I probably typed something wrong. Are you tracking vaccines too? That's the one I still need to build.

[–] michaelharley@infosec.pub 1 points 1 month ago (3 children)

Thanks! Both are the same trick: an input_datetime per cat, and a Mushroom template card doing the math inline in Jinja. No custom integration.

The collar card colors off days remaining, and the reset button runs a script that pushes the date out and clears the alert flags.

Want the YAML?

 

I'm a big Home Assistant fan and power user. It's the home automation platform of choice in our house and I've added many devices and automations, sometimes to the chagrin of Rachelle.

We also have five cats so it only makes sense to use some automations to help manage all the cat chores.

A four-panel collage of cats in various settings: top left shows a black cat with yellow eyes wearing a red collar sitting indoors by a window; top right shows a cream-colored cat with a pink heart-shaped tag; bottom left shows a tabby and white cat lying on a colorful rug; bottom right shows two long-haired cats sitting together outdoors.

Top row: Lily and Mac. Second row: Linus, Lennie and Polly.

The dashboard

To help me stay organized and visualize all the cat automations and functionality, I've created a cats dashboard. Let's walk through the dashboard, and I'll explain as we go.

A smart home dashboard for managing cats and their accessories. The interface displays the presence of four cats (Lily, Linus, Mac, and Polly), cat door status showing one cat outside, feeder information, and litter box status for two automated litter boxes with battery and waste levels.

Cat door

Our cats are indoor/outdoor cats and we have a SureFlap Microchip Cat Flap. This thing talks to a little Sure Petcare hub in our living room, and that hub is what connects to Home Assistant. It's Cloud Polling, which isn't the best but when I was shopping, I couldn't find one with better local connectivity.

It allows us to use Home Assistant to lock the door when needed so the cats can't go out. Home Assistant does this on a schedule. It's open during the day but prevents the goblins from leaving after sunset.

Security camera footage from a cat door showing a laundry room or utility space with a white cat door flap in the center of a dark door, surrounded by household appliances, storage items, and a window with bright light streaming through it.

We have a little Unifi G3 Instant camera pointed at the cat door so we can see who's coming and going and fiddling around at the door.

All of our cats have a microchip and it uses this chip ID to only allow our cats in. That means no neighborhood cats or raccoons can use the door, thank you please. We also get presence reporting so we can generally see who's in, who's out and when they transitioned.

Automatic feeders

We have Aqara Smart Pet Feeders that we use to dispense breakfast and snacks. They're zigbee devices so they connect directly to Home Assistant and are fully local. No Aqara app needed for any part of it. These are pretty basic devices and I wish it could alert us on food level but alas, they simply dispense. The automatic feeders hit at 6 a.m., a time I set from a tile on the dashboard rather than hardcoding it. Cats go scattering every which way to get their spot at a feeder.

Litter boxes

We have two Litter Robot 4s we purchased used from Facebook Marketplace. They're Cloud Push, which means the internet has to be up for these to work correctly with Home Assistant (or I presume the Whisker app). This thing allows us to unlock a couple of fun automations.

Voice announcements when a Litter Robot is full

When a litter box is full, Home Assistant announces it over the speakers: "The black litter robot is full and needs to be emptied." Then when we're up there, we pull the bag of dirty litter and reset it. If it needs a top off, we do that too. With two litter boxes and five cats, we can go a few days before one of them needs to be emptied! Not bad.

Send the robot vacuum after a cat has used the robot litter box

This is probably one of my favorite automations. Home Assistant can detect when a cat uses a litter box, which allows us to trigger automations to send the Roomba to spot clean the litter box area. We gate when the vacuum can run as we don't want the thing running during the middle of the night if a cat goes to the bathroom.

A security camera view of the upstairs hall: a white litter robot beside a low shelf holding a planted aquarium, framed artwork on the wall behind it, and a Roomba is on the wood floor mid-hall. Timestamped August 5, 2026 at 9:02 a.m.

Lennie is an especially messy litter box user so this has been a great automation.

Helpers and trackers

So there are several helpers and trackers we use to keep up with everyone's different dates and things and such.

  • Flea collar: We use Seresto flea collars and they need to be replaced about every 8 months. I set up a helper to track this so it announces it in the good morning announcement message when it's due.
  • Age: Based on their birthday!
  • With Us: Based on their gotcha date!
  • Yearly Vaccines: I've not built this yet but I plan to also track when each cat received their vaccinations so I can remember when it's time to reschedule.

Pet tracking dashboard showing details for a cat named Lily and her flea collar. Left panel displays Lily's information including age (7 years 3 months), weight (8.8 lb), and activity metrics. Right panel shows the Flea Collar status and a location history timeline with timestamps indicating Lily's movements between home and away locations on August 4-5, 2026.

Improvements

Cat door

I purchased the wrong cat door. When we first purchased it, we had Oliver and he was a bigger boy and I was skeptical he could fit through the smaller door. The problem is the larger door is all or nothing: locked or unlocked, for everybody. The smaller flap they sell can be set per cat. That would have been helpful in the past when someone was sick or wasn't confident using the cat door yet and we were going to be away. We could restrict the kitten or the sick kitty while letting the rest come and go as usual.

Automatic feeders

I wish the Aqara feeders reported food levels instead of just dispensing. I also wish the automatic feeders were the chip variety and allowed us to restrict certain feeders to only certain cats. Linus likes to go around and finish off everyone else's bowls.

Water fountains

We purchased an AquaPurr fountain but have never fully installed it. We had it in one of the bathrooms but disconnected it. My plan is to move it into the guest bathroom then hopefully we can stop fiddling with these little plastic filter ones. This thing doesn't connect to anything but it would be super nice to eliminate the water fountain clean chore.

Conclusion

So that's it! Thanks for reading this. Do you have any smart home automations that you especially like?

 

Look at this sweet boy. Say hi to Lennie!

 

I think white cloud mountain minnows are underrated. I really like this little things y'all.

 

A smart thermostat was one of the first connected devices I purchased when I started building our smart home. I wanted automatic presence detection so the thermostat could set an away mode when we leave and "we're home" mode without us having to do anything. Never mind the fact that Rachelle and I both work from home and are both homebodies. On the rare occasion that we're both away from home, the house knows it, automatically! ;)

Throughout the year, I'd find myself fiddling with the set points though. 68°F (20°C) in February feels different than 68°F in August. So twice a year, as the seasons started to tip over into warmer or colder weather, I'd change the upper and lower bounds.

It feels weird to run the AC in winter or the heat in summer. We sleep with the thermostat set at 65°F (18°C). Our preference for day time temp is 68°F to 72°F (20°C to 22°C). So when we wake up, and toggle sleep mode off in Home Assistant, the thermostat sets itself to the day time values and turns on the heat to raise the temp from 65 to 68; even in summer. That bothers me. I'd rather the thermostat just set the lowest temp to 68 and allow the house temp to naturally rise.

Plus, I have all these damn sensors in the house, outside the house, and a weather forecast available. I feel like I have all the inputs needed to make some data-driven decisions.

A climate control dashboard showing a T6 Pro Thermostat set to cooling at 72°F on the left, with environment metrics in the center including indoor temperature of 73°F and humidity at 46%, and miscellaneous data on the right showing UV index and rain forecast, along with detailed air quality measurements for indoor and outdoor CO2, VOC, and particulate matter levels.

My climate dashboard.

Goal

My goal was to stop running heat on summer mornings and AC on winter evenings. The set points stay fixed. What changes is the HVAC mode: heat-only, cool-only, or both.

Data

For Home Assistant to pick the right mode, it needs to know what's going on outside, so I made a couple of template sensors.

Outdoor Blend

It all comes down to one template sensor: sensor.outdoor_temp_blend. It's the average of three numbers: the current outdoor temperature, today's forecast high, and today's forecast low. The result tells me what kind of day this is rather than what the temperature is right this minute. That blend feeds sensor.thermostat_season, which labels the day heating, cooling or shoulder-season. The season is what picks the HVAC mode.

Why average instead of just reading the current outdoor temp? A 50°F dawn before a 75°F afternoon shouldn't kick the heater on for an hour at sunrise. The blend flattens out the time-of-day swings so the thermostat isn't reacting to a number that's about to change anyway.

{% raw %}

- sensor:
    - name: "Outdoor Temp Blend"
      unique_id: outdoor_temp_blend
      device_class: temperature
      unit_of_measurement: "°F"
      state_class: measurement
      availability: >-
        {{ state_attr('weather.forecast_12_grimmauld_place', 'temperature') is not none
           and states('sensor.today_forecast_high') | float(none) is not none
           and states('sensor.today_forecast_low') | float(none) is not none }}
      state: >-
        {% set current = state_attr('weather.forecast_12_grimmauld_place', 'temperature') | float %}
        {% set high = states('sensor.today_forecast_high') | float %}
        {% set low = states('sensor.today_forecast_low') | float %}
        {{ ((current + high + low) / 3) | round(1) }}

{% endraw %}

Thermostat Season

sensor.thermostat_season reads the outdoor blend and labels the day as one of three states:

  • cool once blend reaches 68°F, holds until blend drops below 65°F
  • heat once blend falls to 55°F, holds until blend rises above 58°F
  • heat_cool for everything in between

The mode scripts (thermostat_active_mode, thermostat_sleep_mode, thermostat_away_mode) read this sensor to decide whether the thermostat should run cooling only, heating only, or both with a deadband.

That 3°F gap on each side is on purpose. Without it, a blend hovering right at 68°F would flip the HVAC mode every time the value bounced from 67.9 to 68.1 across forecast updates. The hysteresis means the season has to meaningfully change before the mode does. Once it decides it's summer, it doesn't second-guess itself over one cool morning.

{% raw %}

- sensor:
    - name: "Thermostat Season"
      unique_id: thermostat_season
      icon: >-
        {% set s = this.state %}
        {% if s == 'cool' %}mdi:snowflake
        {% elif s == 'heat' %}mdi:fire
        {% else %}mdi:swap-vertical{% endif %}
      availability: >-
        {{ states('sensor.outdoor_temp_blend') | float(none) is not none }}
      state: >-
        {% set blend = states('sensor.outdoor_temp_blend') | float %}
        {% set prev = this.state if this.state in ['heat','cool','heat_cool'] else 'heat_cool' %}
        {% if prev == 'cool' %}
          {# stay in cool until blend drops below 65 #}
          {{ 'heat_cool' if blend < 65 else 'cool' }}
        {% elif prev == 'heat' %}
          {# stay in heat until blend rises above 58 #}
          {{ 'heat_cool' if blend > 58 else 'heat' }}
        {% else %}
          {# in heat_cool, transition based on outer thresholds #}
          {% if blend >= 68 %}cool
          {% elif blend <= 55 %}heat
          {% else %}heat_cool
          {% endif %}
        {% endif %}

{% endraw %}

Helpers

I built some helpers that allow me to easily set the temperature range. They're just input_number helpers, one pair per mode: day, night and away. The scripts below read them by name, so when I nudge the day time high from 72 to 71 on my dashboard, that's the number the thermostat gets the next time anything runs.

Thermostat Settings screen showing temperature modes, schedule, HVAC maintenance information, and battery status. Left panel displays Day Mode (68-72°F), Night Mode (60-65°F), and Away Mode (60-75°F) settings. Center shows daily schedule from 6:00 AM to 7:30 PM, HVAC and humidifier filter expiration dates of 2026-09-19 with 60-day replacement intervals. Right panel indicates outdoor blend temperature of 82.0°F, cool season logic, and thermostat battery level at full capacity.

Putting it all together now

Three scripts do the actual work: thermostat_active_mode, thermostat_sleep_mode and thermostat_away_mode. They're deliberately dumb. Each one reads sensor.thermostat_season, picks a branch, and writes to the thermostat. That's the whole job. None of them know a thing about the weather.

Here's the day time one. I trimmed a couple of housekeeping steps off the end (it also flips some booleans and sets the fan mode) to keep the important part visible:

{% raw %}

thermostat_active_mode:
  alias: Thermostat active mode
  sequence:
    - choose:
        - conditions:
            - condition: state
              entity_id: sensor.thermostat_season
              state: cool
          sequence:
            - action: climate.set_temperature
              target:
                entity_id: climate.t6_pro_z_wave_programmable_thermostat
              data:
                hvac_mode: cool
                temperature: "{{ states('input_number.thermostat_day_temp_high') | float }}"
        - conditions:
            - condition: state
              entity_id: sensor.thermostat_season
              state: heat
          sequence:
            - action: climate.set_temperature
              target:
                entity_id: climate.t6_pro_z_wave_programmable_thermostat
              data:
                hvac_mode: heat
                temperature: "{{ states('input_number.thermostat_day_temp_low') | float }}"
      default:
        - action: climate.set_temperature
          target:
            entity_id: climate.t6_pro_z_wave_programmable_thermostat
          data:
            hvac_mode: heat_cool
            target_temp_high: "{{ states('input_number.thermostat_day_temp_high') | float }}"
            target_temp_low: "{{ states('input_number.thermostat_day_temp_low') | float }}"
  mode: single

{% endraw %}

In cool season the script sets hvac_mode: cool and one number, the day time high. That's it. No lower bound anywhere in that call, so nothing can ask for heat. Winter is the same trick backwards, hvac_mode: heat and just the day time low. Only the shoulder season sends both numbers as a deadband, which is what I had before.

Sleep and away are the same shape. They just read their own pair of helpers.

What calls the scripts

Mostly, nothing new. My wake, sleep and away automations already called these three scripts, because that's how the house has worked for a while now. They picked up the season awareness for free. I didn't have to touch a single one of them. So that's cool.

The one thing I did have to add is an automation for when the season itself changes:

{% raw %}

- id: thermostat_reevaluate_on_season_change
  alias: Thermostat, re-evaluate on season change
  triggers:
    - trigger: state
      entity_id: sensor.thermostat_season
      to:
        - heat
        - cool
        - heat_cool
  conditions:
    - condition: template
      value_template: >-
        {{ trigger.from_state is not none
           and trigger.from_state.state in ['heat','cool','heat_cool'] }}
  actions:
    - choose:
        - conditions:
            - condition: state
              entity_id: input_boolean.thermostat_away_mode
              state: "on"
          sequence:
            - action: script.thermostat_away_mode
        - conditions:
            - condition: state
              entity_id: input_boolean.thermostat_night_mode
              state: "on"
          sequence:
            - action: script.thermostat_sleep_mode
        - conditions:
            - condition: state
              entity_id: input_boolean.thermostat_day_mode
              state: "on"
          sequence:
            - action: script.thermostat_active_mode
  mode: single

{% endraw %}

Without it, a season flip in the middle of a Tuesday afternoon would just sit there. Nothing would be calling a script until bedtime. This one re-runs whichever mode is already active so the thermostat catches up.

The condition looks fussier than it needs to be. It's there because the season sensor drops to unavailable for a split second now and then, usually when the weather integration hiccups or Home Assistant restarts. Without the guard, every one of those blips would re-fire the thermostat. I went back through the last couple of weeks of history and it happened 16 times. So, worth having.

Conclusion

So, back to the thing that bugged me. Summer morning, house is sitting at 65, I toggle sleep mode off. The season is cool, so the script sends cool-only at 72 and there's no lower bound for the furnace to chase. The house drifts up to 68 on its own, for free, the way it would have anyway if I'd just left it alone.

I pulled the history to make sure I wasn't fooling myself. Over the last twelve days there were 13 mornings where the house was sitting at exactly 65°F when we got up, and the furnace ran for zero minutes. Under the old setup that's 13 mornings of running heat in the middle of summer.

I should say though, it's August. The heat branch has never actually fired for real. I've read it a dozen times and I think it's right. Ask me again in December.

I'm not an HVAC guy and there's almost certainly a smarter way to do this. What's your approach? Does your thermostat know what time of year it is, or do you fiddle with it twice a year like I used to?

 

A few weeks ago, the Canadian wildfire haze reached Central Virginia, where I live, and we noticed it as a haziness in the air. Well, I have an AirGradient Open Air (model O-1PST) outdoor sensor and two AirGradient ONE units (model I-9PSL) indoor sensors. I'm a data-driven person, and I was instantly curious how this was showing up to the sensors in Home Assistant.

Here's what it looked like in the sensor logs:

Line graph showing air quality measurements from July 17-18. The orange line represents outside air quality (back porch) peaking at 164, while the cyan line shows inside air quality (dining room and upstairs hallway) staying below 40. A dashed line marks the EPA sensitive-groups threshold at 35.4.

Hourly mean PM2.5, 17-18 July 2026, central Virginia. The shaded band spans the two indoor sensors; the dashed line is the EPA 24-hour "unhealthy for sensitive groups" breakpoint (35.4 µg/m³).

The signal was unmistakable. Outside, PM2.5 peaked at 164 µg/m³, against a normal range of 7 to 15 for that sensor. Inside, both sensors traced the same curve at about a quarter the height, topping out near 38.

What I didn't expect was the timing. Outside peaked at 9 a.m., but inside didn't level off until noon. And the house was slower to clear than the air outside was. At the worst of it, inside was running about a fifth of outside. The next morning, as the outdoor numbers dropped away, inside was still sitting at a third of them.

Here are some of my automation ideas based on this event:

  • I have a morning automation that fires at 7 a.m. It basically just gives the current weather conditions, the daily forecast, if any litter robots need to be emptied, etc. I can add an air quality alert to this announcement if it's outside of the 'good' range.
  • I can also use the same sensor data to trigger ad hoc voice announcements and alerts.
  • I built a Corsi-Rosenthal Box a couple years ago, but Rachelle really didn't like it because it's ugly. I think I will resurrect one anyway. I still have the fan, and I've already purchased new MERV filters for it. Wildfires are the exact use case for these box fan filters, so I intend to use one! When the indoor sensors cross my clean-air threshold, I can build automations to run the filter.
  • Of course, I'll need automations and sensors to track how long the fan filter has run so we can have some idea on when to change the filters out.

Did you detect the wildfires where you live? Do you have any smart home automations about air quality?

 

Y'all! I have a dang flower in one of my 20 longs. I'm shocked. This tank is running fine. Animals seems healthy but I'm not really doing anything special. This makes me feel like this tank is exactly dialed in though if this plant is so happy it wants to flower! I think it's some sort of Buce but I've bought many randoms from /r/AquaSwap.

Originally posted on my site: https://michaelharley.net/photos/2026/07/27/

[–] michaelharley@infosec.pub 2 points 1 month ago

I'm not sure. When I was searching around I never discovered it but that doesn't mean much.

[–] michaelharley@infosec.pub 5 points 1 month ago (2 children)

My favorite thing to self host is my RSS feed reader. I'm a huge fan of feedbin but I wanted to self host. I tried the usual self hosted RSS feed readers but I just prefer the way feedbin is laid out and how it works. So, I used Claude to create a self hosted clone of feedbin but with my personal modification to fit my workflow.

  • Customized keyboard shortcuts for next and previous.
  • I added webmention support which allows me:
    • Determine if a site supports webmentions
    • Allows me to send a webmention like
    • Allows me to send a webmention comment
  • I can share and sydicate a link from my feed reader to my website
  • I can save a link as a blog idea, like if I want to respond or write about a topic for example.
  • Custom filters so I can basically mute certain keywords from me feed.
 

Linus did so well at the vet yesterday! They said he needs to lose some weight but who doesn't?! Not pictured: Polly who was huddled in the corner who also got a clean bill of health but does not like new people, places or things.

Originally posted on my site: https://michaelharley.net/photos/2026/07/25/

[–] michaelharley@infosec.pub 2 points 2 months ago

Short version, and Deebster's got the shape right: use a DNS-01 challenge. You prove domain control with a TXT record instead of serving a file over HTTP, so the hostname never has to be reachable from the internet. That's what gets you a real Let's Encrypt cert for a LAN-only name.

Mine's acme.sh issuing a wildcard for *.lan.michaelharley.net via my registrar's DNS API (Porkbun), with a deploy hook dropping it where Caddy reads it. Caddy's own Cloudflare plugin is simpler, I just already had acme.sh. Local DNS is a wildcard rewrite on my router.

 

This is the companion to I own my books, where I made the case for stripping DRM and actually owning what I buy. This post is the machinery overview: how I store, serve, and read all of it from my own homelab.

My books workflow

When I find a book I want to buy, I go to eBooks.com and search for it. They have most titles, but occasionally I will have to go to Kobo to find it; regardless, I purchase the book.

Note: This is just an overview of how this works and is not a technical, step-by-step walk-thru.

Calibre

If the book has DRM, the distributor gives me a file with the .acsm extension. If the book is DRM free, then they hand me the ePUB file directly. Either way, the next step is to import the downloaded file into Calibre. With the right plug-ins, Calibre will automatically download the book, remove the DRM (if any) and add the ePUB file to the Calibre books library.

Syncthing

Once the book is in Calibre as a DRM-free ePUB, Syncthing syncs it to one of my homelab instances that serves it with Calibre-Web.

Calibre-Web

I use Calibre-Web to then serve my Calibre books on my local network. Calibre-Web implements the same endpoints the Kobo store exposes: sync, library metadata, cover images, reading state. This is the first piece that allows me to connect my books library to my Kobo device.

DNS and certificates

Two things have to be in place to allow my Kobo to connect to Calibre-Web. My local DNS has to point an internal hostname at the server running Calibre-Web. And that server needs a real, publicly trusted TLS certificate, because the Kobo thinks it's talking to a commercial store and won't accept a self-signed one. Getting a legitimate certificate for a hostname that only exists inside my homelab is the fiddly part. I may write that up separately.

Kobo

Now I update the api_endpoint setting in the .kobo/Kobo/Kobo eReader.conf file on the Kobo itself so it points to the internal hostname from the DNS and certificates section above. I plug the Kobo into my computer over USB and it mounts as a drive, which is how I get at the file. There's no way to do this from the device's settings menus.

All together now

So my workflow looks like this in practice:

  1. Buy and download the book.
  2. Import the book into Calibre.
  3. Open my Kobo device and download the new book to the reader.
  4. Read.

The default software on the Kobo is a first-class reading experience. Connecting it to my Calibre-Web server keeps that experience intact while my whole library syncs down over my local network.

KOReader

I see many people talking about KOReader, so when I first started on this path, I had every intention of installing and using it. In my own tests, the software just felt a lot rougher and harder to make into a nice reading experience, and I still had to connect my device directly to my laptop to load books. No shade towards KOReader and all the people who seem to love it, but the Kobo reading software works perfectly for me, and I can't think of a feature that KOReader provides that isn't already in the Kobo software.

Audiobooks

I listen to a fair amount of audiobooks, and my preference is to purchase them from Libro.fm. Their audiobooks are already DRM-free, so I just download the files from the Libro.fm website and boom, I have my backup.

Audible books do have DRM, but I can remove it. In order to make my backups, I use ffmpeg with credentials that audible-cli legitimately pulls from my authenticated account. I'm decrypting content I bought, using a key I'm entitled to, into a universally playable format (.m4b). So I can back up even Audible exclusives like Dungeon Crawler #8.

No matter how I purchased and backed up the audiobooks, I prefer to listen to them in Audiobookshelf, which is a self-hosted solution in my homelab.

Conclusion

And that's my books workflow and an overview of how I self-host all this. I'm really interested in hearing about others' book workflows too. Drop me a line if you've written something up and want to share.

Originally posted on my site: https://michaelharley.net/posts/2026/07/21/how-i-self-host-my-ebooks-and-audiobooks/

view more: next ›