Tracking Cat Litter Cleaning with ESPHome and Home Assistant

One of our daily tasks is to clean out the cat litter boxes and we (on maybe I just wanted an excuse to automate part of the process!) run in to a couple of small problems:

  1. We usually don’t know if the other has already cleaned them.
  2. We obviously don’t want to forget.
  3. Since I primarily took over this job, I’ve been using a daily reminder, but it’s on my iPhone nagging me all day, even though it’s set for noon. I only need a reminder when it doesn’t get done.

I thought of a solution with an ESPHome based device and Home Assistant. I used a WEMOS D1 Mini Lite, WEMOS OLED Shield, button, red LED, green LED, and 220 Ohm resistors. I connected everything on a breadboard for testing and then made it more permanent.

Key functionality:

  • During the day, the green LED will be lit if the litter boxes have been cleaned and the red LED if they need to be cleaned. If we’re walking by, green means keep going and red means stop here.
  • At night (8pm to 8am), the LEDs are off, unless the device is woken up.
  • Press the button to turn on the display and status LED if at night. The display shows the last time the litter was cleaned in one of three formats, depending on how long it’s been.
    • Today
      1:23 PM
    • Yesterday
      2:48 PM
    • 2 days ago
      11:00 AM
  • Press the button when the display is on to update the litter box last cleaned date and time. The LEDs flash to signify something is happening. The new date and time gets shown on the display.
  • After 30 seconds the display turns off.
  • At 4pm send a reminder to our phones if the litter boxes need to be cleaned.
  • In Home Assistant a toggle can disable the reminders. Useful when we’re on vacation.

Here’s the ESPHome device YAML:

substitutions:
  device_name: cat-litter
  api_key: !secret catlitter_api
  ota_password: !secret catlitter_ota
  ip_address: !secret catlitter_ip

packages:
  base: !include z_package_base.yaml

esphome:
  name: ${device_name}
  friendly_name: Cat Litter
  on_boot:
    priority: 800 
    then:
      - lambda: |-
          id(cat_litter_display).turn_on();
          id(screen_is_active) = true;
      - component.update: cat_litter_display
      - script.execute: display_timer

esp8266:
  board: d1_mini_lite

globals:
  - id: screen_is_active
    type: bool
    restore_value: no
    initial_value: 'true'

# WEMOS OLED Shield
i2c:
  sda: GPIO4 
  scl: GPIO5 
  scan: true
  id: bus_a
  frequency: 100kHz

time:
  - platform: homeassistant
    id: esptime
    on_time:
      - seconds: 1
        minutes: 0
        hours: 8
        then:
          - script.execute: update_led_logic
      - seconds: 1
        minutes: 0
        hours: 20
        then:
          - script.execute: update_led_logic

font:
  - file: "fonts/Roboto-Regular.ttf"
    id: roboto_font
    size: 12

text_sensor:
  - platform: homeassistant
    id: litter_text
    entity_id: sensor.cat_litter_status_formatted
    on_value:
      then:
        - component.update: cat_litter_display
        - script.execute: update_led_logic

binary_sensor:
  - platform: homeassistant
    id: remote_litter_status
    entity_id: binary_sensor.litter_needs_cleaning
    on_state:
      then:
        - script.execute: update_led_logic

  # Physical button
  - platform: gpio
    pin: 
      number: GPIO0 
      inverted: true
      mode: INPUT_PULLUP
    name: "Cleaned Button"
    filters:
      - delayed_on: 50ms
    on_press:
      then:
        - if:
            condition:
              lambda: 'return !id(screen_is_active);'
            then:
              # WAKE UP
              - lambda: |-
                  id(cat_litter_display).turn_on();
                  id(screen_is_active) = true;
              - component.update: cat_litter_display
              - script.execute: update_led_logic
              - script.execute: display_timer
            else:
              # TRIGGER CLEANED
              - delay: 50ms 
              - homeassistant.service:
                  service: script.cat_litter_cleaned
              - script.execute: led_flash_animation
              - script.execute: display_timer

output:
  - platform: esp8266_pwm
    pin: GPIO14 
    id: led_green
  - platform: esp8266_pwm
    pin: GPIO12 
    id: led_red

script:
  - id: update_led_logic
    then:
      - lambda: |-
          if (!id(remote_litter_status).has_state()) return;

          auto time = id(esptime).now();
          bool is_night = false;
          if (time.is_valid()) {
            is_night = (time.hour >= 20 || time.hour < 8);
          }

          if (id(cat_litter_display).is_on()) { is_night = false; }

          if (is_night) {
            id(led_red).turn_off();
            id(led_green).turn_off();
          } else {
            if (id(remote_litter_status).state) {
              id(led_red).set_level(0.2); 
              id(led_green).turn_off();
            } else {
              id(led_red).turn_off();
              id(led_green).set_level(0.2);
            }
          }

  - id: display_timer
    mode: restart
    then:
      - delay: 30s
      - lambda: |-
          id(cat_litter_display).turn_off();
          id(screen_is_active) = false;
      - script.execute: update_led_logic

  - id: led_flash_animation
    mode: restart
    then:
      - repeat:
          count: 5
          then:
            - output.set_level: { id: led_green, level: 0.3 }
            - output.turn_off: led_red
            - delay: 150ms
            - output.turn_off: led_green
            - output.set_level: { id: led_red, level: 0.3 }
            - delay: 150ms
      - script.execute: update_led_logic

display:
  - platform: ssd1306_i2c
    id: cat_litter_display
    model: "SSD1306 64x48"
    address: 0x3C
    rotation: 180° 
    lambda: |-
      if (id(litter_text).has_state()) {
        std::string full_text = id(litter_text).state;
        size_t pos = full_text.find("@");
        if (pos != std::string::npos) {
          std::string day = full_text.substr(0, pos);
          std::string time_str = full_text.substr(pos + 1);
          it.printf(32, 8, id(font_main), TextAlign::TOP_CENTER, "%s", day.c_str());
          it.printf(32, 26, id(font_main), TextAlign::TOP_CENTER, "%s", time_str.c_str());
        } else {
          it.printf(32, 24, id(font_main), TextAlign::CENTER, "%s", full_text.c_str());
        }
      } else {
        it.printf(32, 24, id(font_main), TextAlign::CENTER, "Syncing...");
      }

Some helpers in configuration.yaml:

template:
- binary_sensor:
  - name: "Litter Needs Cleaning"
    unique_id: litter_needs_cleaning
    # This turns ON (Red LED) if the date is not today
    state: >
      {% set last = states('input_datetime.cat_litter_last_cleaned') | as_datetime %}
      {% if last is none %} true {% else %}
        {{ last.date() < now().date() }}
      {% endif %}
- sensor:
  - name: "Cat Litter Status Formatted"
    unique_id: cat_litter_status_formatted
    state: >
        {% set last = states('input_datetime.cat_litter_last_cleaned') | as_datetime %}
        {% if last is none %}
            No Data @ --:--
        {% else %}
            {% set diff = (now().date() - last.date()).days %}
            {% set time = last.strftime('%-I:%M %p').lower() %}
            {% if diff == 0 %}
                Today @ {{ time }}
            {% elif diff == 1 %}
                Yesterday @ {{ time }}
            {% else %}
                {{ diff }} days ago @ {{ time }}
            {% endif %}
        {% endif %}

notify:
  - name: "momrik_phones"
    platform: group
    services:
      - service: mobile_app_nick
      - service: mobile_app_brandi

Automations in automations.yaml:

- id: '1768843528106'
  alias: Notify - Cat Litter @ 4PM
  description: ''
  triggers:
  - trigger: time
    at: '16:00:00'
  conditions:
  - condition: state
    entity_id: binary_sensor.litter_needs_cleaning
    state:
    - 'on'
  - condition: state
    entity_id: input_boolean.cat_litter_reminders
    state:
    - 'on'
  actions:
  - action: notify.momrik_phones
    metadata: {}
    data:
      title: "Cat Litter \U0001F408 \U0001F4A9"
      message: The litter hasn't been cleaned yet today!
      data:
        tag: cat-litter-alert
        actions:
        - action: MARK_LITTER_CLEANED
          title: I did it
  mode: single
- id: '1768843827814'
  alias: Notify Clear - Cat litter
  description: ''
  triggers:
  - trigger: event
    event_type: mobile_app_notification_action
    event_data:
      action: MARK_LITTER_CLEANED
  conditions: []
  actions:
  - action: script.cat_litter_cleaned
    data: {}
  - action: notify.momrik_phones
    data:
      message: clear_notification
      data:
        tag: cat-litter-alert
  mode: single

One script in scripts.yaml:

cat_litter_cleaned:
  alias: "Cat Litter Cleaned"
  sequence:
    - service: input_datetime.set_datetime
      target:
        entity_id: input_datetime.cat_litter_last_cleaned
      data:
        datetime: "{{ now().strftime('%Y-%m-%d %H:%M:%S') }}"
    - service: notify.momrik_phones
      data:
        message: "clear_notification"
        data:
          tag: "cat-litter-alert"

I also added a couple of simpler helpers via Settings -> Devices & services -> Helpers:

  • Cat Litter Last Cleaned – Date and time
  • Cat Litter Reminders – Toggle

Finally, I added cards to a Home Assistant dashboard. See screenshots of the different states of each card below.

Here’s the dashboard YAML if you want it.

type: horizontal-stack
title: Cat Litter
cards:
  - type: custom:mushroom-template-card
    primary: >-
      {% set last = states('input_datetime.cat_litter_last_cleaned') |
      as_datetime %}

      {% if last is none %}
        Unknown State
      {% else %}
        {% set diff = (now().date() - last.date()).days %}
        {% if diff == 0 %}
        Cleaned Today
        {% else %}
        Needs Cleaning
        {% endif %}
      {% endif %}
    icon: mdi:emoticon-poop
    features_position: bottom
    secondary: >-
      {% set last = states('input_datetime.cat_litter_last_cleaned') |
      as_datetime %}

      {% if last is none %}

      ???

      {% else %}
        {% set diff = (now().date() - last.date()).days %}
        {% if diff > 0 %}
          {{ last.strftime('%b %-d') }} -
        {% endif %}
        {{ last.strftime('%-I:%M %p') }}
      {% endif %}
    color: |-
      {% if is_state('binary_sensor.litter_needs_cleaning', 'on') %}
        red
      {% else %}
        green
      {% endif %}
    tap_action:
      action: more-info
    icon_tap_action:
      action: perform-action
      perform_action: script.cat_litter_cleaned
      target: {}
      confirmation:
        text: Are you sure you want to update the last cleaned date/time to now?
    entity: input_datetime.cat_litter_last_cleaned
  - type: custom:mushroom-entity-card
    entity: input_boolean.cat_litter_reminders
    name: Reminders
    tap_action:
      action: toggle
    hold_action:
      action: more-info
    icon_color: amber

If we get a reminder, it looks like this. We can click I did it to update the date/time to now if we forgot to press the button on the physical device. With the automation set to run at 4pm, we should rarely see this though.

The final device lives right next to the waste bag dispenser I made. This way it’s easy to see the state and hit the button when cleaning out the litter. Here are photos in place with a few different states.

This project was so fun. Using Google Gemini for stuff like this makes it so much faster. I’ve had some of these microcontrollers and other parts for almost a decade so it’s nice to finally put them to use.

What other features or automations would you add to this? Have you built anything similar?

Updates to my PyPortal Home Assistant Display Project

I picked up a 5m strip of adressable LEDs for my PyPortal device. The strip has 60 LEDs/m, which is double the density of the Neopixel strip I fried. To prevent future accidents, I tested improvements. Adding a 1,000 µF electrolytic capacitor protects the LEDs from a power spike and a 470Ω resister protects the first LED from data ringing.

Everything worked great, so I cut off a section of 62 LEDs. Then I worked on a little board and better wiring.

I didn’t trim the protoboard, so I could use the two holes to screw it to the undersize of my desk. This LED strip had an adhesive backing, which worked much better than the clips. I did a bit of cable management and also mounted the air quality monitor under the desk.

The code, which can be found on GitHub, got a bunch of small improvements:

  • Updated the LED count
  • Limited brightness to 50%, which is plenty and helps with power draw
  • Adjusted the pulse functionality to account for the brightness limit
  • Added a button to the interface which can be used to clear the LEDs
  • When filling a color, reset the LED params global, so an animation won’t play on the next loop
  • Tweaked the chase functionality to work better with the new LED density
  • Changed the button success animation so the loop continues sooner

I had Google Gemini help me some stuff. First was to update the PyPortal firmware and CircuitPython version. Then update the code to make it compatible with CircuitPython 10.03. I also made an interface for my dashboard where I can select the different commands and options to send to the device manually instead of typing them in through Developer Tools -> Actions in Home Assistant.

Here’s a new demo video, so you can see how it works and what the lighting looks like.

Of course, the real power still comes from automations, where something happening around the house triggers Home Assistant to send commands to the device. Time to work on more of those. Almost a year later and this project is finally where I hoped it would end up.

Using PyPortal to Display Air Quality Data and Interact with Home Assistant

I got AdaBox 011 in March of 2019 and wrote this about the PyPortal (buy at Adafruit):

I’ll likely turn this into something that interfaces with my Home Assistant server to control different devices around my house.

The PyPortal has been sitting on a shelf ever since. Way back in February, it caught my eye, and I picked it up, not remembering what it’s capabilities were. Then I started upgrading IKEA air quality monitors and even made my own. Since I’m at the desk in my office a large portion of the week I thought I would make that 2019 prediction come true.

I could show a bunch of data on the screen and the PyPortal has a touchscreen, so I could display buttons for triggering things around the house. The device also has connectors for doing GPIO, so I got the idea of adding an LED strip, which I could use for notifications. I even had a meter long strip of Adafruit Mini Skinny NeoPixels I had bought in 2017 and never touched that would be perfect. I needed to buy a 2.0mm JST PH Connector kit in order to make a wire that would connect to the pack of the PyPortal. I ended up using a piece of Cat6 cable, even though I only needed 3 of the 8 wires inside.

I used light strip clips to mount the LEDs to the back of my desk.

I also mounted a power strip under the desk and cleaned up all of the cables.

The code I’m running on the PyPortal was heavily inspired by Adafruit’s PyPortal MQTT Sensor Node/Control Pad for Home Assistant and PyPortal NeoPixel Color Picker. It’s done with CircuitPython and I’ve added my code to pyportal-home-assistant-display on GitHub.

All of this was done back in March. I quickly began having issues with the ethernet cable and the small JST connectors, so I put this post on pause. Figured it was time to finally fix this before the end of the year. While testing, I determined the LED strip got fried up at some point. It was probably some kind of short from the janky wire.

Here’s what my display looks like.

My favorite aspect of the project and code is being able to publish MQTT messages from Home Assistant, which the PyPortal listens for and reacts to. I can send various commands, such as fill:blue, which turns all of the LEDs blue, or whatever color I set. I have commands to chase a color from one side to the other, bounce a color from left to right and back to the left, pulse the entire strip, animate a rainbow, or set the brightness. Since I don’t have another strip of Neopixels, in order to create a demo video, I wired up a 24 LED circle. You’ll have to imagine the effects on the back of my desk, lighting up the wall.

I can manually send these MQTT messages as shown in the demo, but the real power comes from automations. For example, the LEDs automatically pulse blue when the washing machine is done and pink when the dryer is done.

With the different effects and color combinations, the possibilities are endless. What kind of automations would you run?

Update: I got a new LED strip, made a new demo video, and improved a bunch of stuff. See Updates to my PyPortal Home Assistant Display Project.

Building a Shop Air Filter with Box Fans

Over the years, I’ve seen many versions of a shop air filter, made from box fans and 20×20 inch furnace filters. A few years ago I picked up some old box fans on Facebook Marketplace and bought a pack of filters from Sam’s Club. They’ve been stacked in the corner.

It was finally time to build my air filter. I removed the back covers, feet, handles, and knobs from the fans. I got my first look at the switches inside, which are nearly identical.

I’d easily be able to wire the fans together, so I removed the switches and power cords.

I put together a frame from OSB, cut slots to feed the wires through, and screwed the box fans in.

Then I grabbed wood that had been salvaged from a pallet to construct a door.

On the back side, I used glue and brad nails to attach plywood rails. I also made tabs to hold the filters secure.

I attached the door with a couple hinges and made some notched tabs to hold the door shut.

Then it was time to work on the wiring and electronics. I had recently watched a YouTube video showing how to make an old fan smart and his code with ESPHome and Home Assistant gave my a great start. I bought a 4 channel relay board to use with a an ESP8266 development board, a button, and some LEDs. I tore open an old USB power plug and was originally going to tie in the 120 volt line, but decided against it. First, I tested out the circuit and code on a breadboard and then soldering things up more permanently.

A plastic screw container was a good side, so I used hot glue to secure the boards and then wired up all of the fan connections.

I’m not sure if I’ll ever use the button, but it allows me to cycle between the three speeds and turn it off. The three LEDs show which speed is currently running. The only thing I got wrong was reversing the low and high speeds, which was a quick fix in the ESPHome code. Speaking of the code, here’s mine.

esphome:
  name: shop-air-filter
  friendly_name: Shop Air Filter

esp8266:
  board: d1_mini

logger:
  level: WARN

api:
  encryption:
    key: "input_yours"

ota:
  - platform: esphome
    password: "input_yours"

wifi:
  min_auth_mode: WPA2
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  manual_ip:
    static_ip: 192.168.1.2
    gateway: 192.168.1.1
    subnet: 255.255.255.0

time:
  - platform: homeassistant
    id: home_time

binary_sensor:
  - platform: gpio
    pin: {number: D5, mode: INPUT_PULLUP, inverted: true}
    name: "Speed Button"
    on_press:
      then:
        - script.execute: cycle_fan_speed

  - platform: template
    id: active
    lambda: 'return id( fan_speed ).state > 0;'

switch:
  # Relays
  - platform: gpio
    pin: D3
    id: speed_1
    inverted: true
    interlock: &fan_interlock [speed_1, speed_2, speed_3]
    internal: true
  - platform: gpio
    pin: D2
    id: speed_2
    inverted: true
    interlock: *fan_interlock
    internal: true
  - platform: gpio
    pin: D1
    id: speed_3
    inverted: true
    interlock: *fan_interlock
    internal: true

  # LEDs
  - platform: gpio
    pin: D6
    id: led_1
    internal: true
  - platform: gpio
    pin: D7
    id: led_2
    internal: true
  - platform: gpio
    pin: D8
    id: led_3
    internal: true

number:
  - platform: template
    name: "Fan Speed"
    id: fan_speed
    min_value: 0
    max_value: 3
    step: 1
    optimistic: true
    restore_value: true
    on_value:
      then:
        - script.execute: set_shop_filter_speed

text_sensor:
  - platform: template
    name: "Current State"
    id: current_state

sensor:
  - platform: duty_time
    name: "Filter Runtime"
    id: shop_filter_usage
    sensor: active
    restore: true
    unit_of_measurement: h
    accuracy_decimals: 1
    filters:
      - multiply: 0.000277778 # Convert seconds to hours

button:
  - platform: template
    name: "Reset Filter Timer"
    icon: "mdi:timer-off"
    on_press:
      then:
        - sensor.duty_time.reset: shop_filter_usage

script:
  - id: set_shop_filter_speed
    mode: restart
    then:
      - switch.turn_off: speed_1
      - switch.turn_off: speed_2
      - switch.turn_off: speed_3
      - switch.turn_off: led_1 
      - switch.turn_off: led_2
      - switch.turn_off: led_3
      - delay: 300ms 
      - lambda: |-
          if ( id( fan_speed ).state == 0 ) {
            id( current_state ).publish_state( "Off" );
          } else if ( id( fan_speed ).state == 1 ) {
            id( speed_1 ).turn_on();
            id( led_1 ).turn_on();
            id( current_state ).publish_state( "Low" );
          } else if ( id( fan_speed ).state == 2 ) {
            id( speed_2 ).turn_on();
            id( led_1 ).turn_on(); id( led_2 ).turn_on();
            id( current_state ).publish_state( "Medium" );
          } else if ( id( fan_speed ).state == 3 ) {
            id( speed_3 ).turn_on();
            id( led_1 ).turn_on(); id( led_2 ).turn_on(); id( led_3 ).turn_on();
            id( current_state ).publish_state( "High" );
          }
  - id: cycle_fan_speed
    then:
      - lambda: |-
          int next_speed = id( fan_speed ).state + 1;
          if ( next_speed > 3 ) next_speed = 0;
          id( fan_speed ).publish_state( next_speed );

I used Google Gemini to help and it had a great suggestion to track the run time and add a maintenance reminder when it was time to replace the filters.

In Home Assistant I created some automations. My dust collector uses a smart plug, so when it draws electricity, the air filter automatically turns on at high speed. When the dust collector turns off, the air filter continues to run for 15 minutes before turning off. If I had to remember to turn on the air filter all the time, it would rarely happen, so this is amazing.

I’m still on lifting restrictions for several weeks so Brandi helped me install the air filter on the ceiling.

I wish I hadn’t waited so long to build this!

DIY Gate Sensor for Home Assistant

In our basement we have a baby gate, which surprisingly keeps our cat out of the gym and golf sim areas.

Sometimes we forget to close the gate, so I needed a sensor to monitor its state. I still had the breadboard from the air quality monitor project, so it was quick to add a magnetic door switch and test things out with the D1 Mini clone.

I have extra sensors, so those were kept in the project and allowed me to get rid of the shitty DHT22 I added to the golf remote. Everything worked, but I want to save my last two D1 minis and use them for something with the screens I have for them. So I swapped in an Adafruit Feather HUZZAH ESP8266, which I got with AdaBox 3 or 4 in 2017 and made minor changes to the code.

Parts:

References:

ESPHome YAML code:

substitutions:
  slug: gate
  friendly: Gate

esphome:
  name: ${slug}
  friendly_name: ${friendly}

esp8266:
  board: huzzah

logger:
  level: WARN

api:
  encryption:
    key: 'xxx'

ota:
  - platform: esphome
    password: "xxx"

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  manual_ip:
    static_ip: x.x.x.x
    gateway: x.x.x.x
    subnet: 255.255.255.0

i2c:

binary_sensor:
  - platform: gpio
    pin:
      number: GPIO14
      mode:
        input: true
        pullup: true
    name: ${friendly}
    device_class: door

sensor:
  - platform: htu21d
    model: SI7021
    temperature:
      name: Temperature
      id: ${slug}_temp
    humidity:
      name: Humidity
      id: ${slug}_humid

  - platform: aht10
    variant: AHT20
    temperature:
      name: AHT21 Temperature
      id: ${slug}_aht21_temp
    humidity:
      name: AHT21 Humidity
      id: ${slug}_aht21_humid

  - platform: ens160_i2c
    address: 0x53
    eco2:
      name: CO²
    tvoc:
      name: VOC
    aqi:
      id: demo_aqi
      name: AQI
    compensation:
      temperature: ${slug}_aht21_temp
      humidity: ${slug}_aht21_humid

text_sensor:
  - platform: template
    name: AQI Rating
    lambda: |-
      switch ( (int) ( id( ${slug}_aqi ).state ) ) {
        case 1: return {"Excellent"};
        case 2: return {"Good"};
        case 3: return {"Moderate"};
        case 4: return {"Poor"};
        case 5: return {"Unhealthy"};
        default: return {"N/A"};
      }

I also added this to my configuration.yaml because I wanted a gate icon instead of the door, due to the device class of the binary sensor:

template:
 - binary_sensor:
    - name: Gate
      unique_id: gate_template
      device_class: door
      state: "{{ is_state( 'binary_sensor.basementgate_gate', 'on' ) }}"
      icon: |
        {% if is_state( 'binary_sensor.basementgate_gate', 'on' ) %}
        mdi:gate-open
        {% else %}
        mdi:gate
        {% endif %}

I figured I might as well use one of the fancy Adafruit Perma-Proto boards I had, which makes soldering all of the connections much easier. As a bonus it was nearly a perfect fit for the case.

The magnetic switch and Si7021 will live outside the box, so those couldn’t get soldered yet. After connecting power I checked the ESPHome logs to make sure everything was working.

I cut holes in a project box, finished soldering, and used hot glue to secure the board..

I reversed the swing of the gate, placed my device, and attached the two sides of the magnetic switch to the gate.

In Home Assistant an automation runs whenever the stairs light is turned off to check the state of the gate. If it’s open, a notification is sent to our phones.

I’m enjoying these little electronics projects, and it feels good to finally put various parts to use.

Replacing a Rigid Shop Vacuum Switch

I’ve had this Rigid shop vacuum, from Home Depot, for about 20 years.

At some point in the last year, the switch started having issues. The vacuum would only turn on if the switch was actually pressed in, instead of toggled. I’ve never seen that happen, but I’m guessing it was from the accumulation of dirt and dust getting inside the switch body. Then the switch wouldn’t even push in, so the vacuum wouldn’t run.

I figured it would be an easy switch replacement, so I removed a bunch of screws to take off the cover. Sure enough, the switch had two wires clipped on to it, and was held in place by the case.

I had a perfect replacement, salvaged from some device I don’t remember, in my collection of electronics parts.

It fit like a glove and the vacuum turned on as if it was brand new. I screwed the case back together and called it done.

DIY Air Quality Monitors for Home Assistant

The upgraded IKEA air quality monitors I did work great, but the LED indication isn’t great for a bedroom and the fan noise was annoying in my office. So I wanted to create a couple of my own devices for those locations. I used:

The SEN50 is a big upgrade over the PM sensors used in the IKEA devices and I used the Si7021 in place of the BME280 I had used because I think they’re a bit better. I soldered 47µF electrolytic capacitors from a big kit I’ve had (similar on Amazon) to the ENS150 modules to improve their power.

Then I attached 5 of the crimped wires to a 6P JST connector, which is what the SEN50 modules require. I’m note sure why buying the actual cable for these SEN50s are so expensive, but I got the entire JST kit for cheaper than a couple of the special cables.

All three sensors communicate with the microcontroller over I²C, so a breadboard test was easy to wire up. The SEN50 does require 5 volts instead of 3.3, so I’m glad I checked.

The ESPHome YAML code is very similar to the code used for the modified IKEA air quality monitors.

substitutions:
  slug: demo
  friendly: Demo

esphome:
  name: ${slug}-wemos-d1
  friendly_name: ${friendly} Wemos D1

esp8266:
  board: d1_mini

logger:
  #level: WARN

api:
  encryption:
    key: 'xxx'

ota:
  - platform: esphome
    password: "xxx"

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  manual_ip:
    static_ip: xxx
    gateway: xxx
    subnet: 255.255.255.0

i2c:
  frequency: 50kHz

sensor:
  - platform: sen5x
    pm_1_0:
      name: PM 1µm
      accuracy_decimals: 0
    pm_2_5:
      name: PM 2.5µm
      accuracy_decimals: 0
    pm_4_0:
      name: PM 4µm
      accuracy_decimals: 0
    pm_10_0:
      name: PM 10µm
      accuracy_decimals: 0

  - platform: htu21d
    model: SI7021
    temperature:
      name: Temperature
      id: ${slug}_temp
    humidity:
      name: Humidity
      id: ${slug}_humid

  - platform: aht10
    variant: AHT20
    temperature:
      name: AHT21 Temperature
      id: ${slug}_aht21_temp
    humidity:
      name: AHT21 Humidity
      id: ${slug}_aht21_humid

  - platform: ens160_i2c
    address: 0x53
    eco2:
      name: CO²
    tvoc:
      name: VOC
    aqi:
      id: demo_aqi
      name: AQI
    compensation:
      temperature: ${slug}_aht21_temp
      humidity: ${slug}_aht21_humid

text_sensor:
  - platform: template
    name: AQI Rating
    lambda: |-
      switch ( (int) ( id( ${slug}_aqi ).state ) ) {
        case 1: return {"Excellent"};
        case 2: return {"Good"};
        case 3: return {"Moderate"};
        case 4: return {"Poor"};
        case 5: return {"Unhealthy"};
        default: return {"N/A"};
      }

These resources helped out:

The project boxes had some standoffs on the bottom, which I snipped off and then sanded with a rotary tool. I pulled out my box of proto boards and found a size almost exactly double what I needed, so I cut out a sliver and ended up with a piece for each box. I also cut vent holes for the SEN50 sensors.

In order to get everything to fit I decided to put the microcontroller on the bottom of the board. After mocking things up I did all of the soldering. I was hoping to be able to mount everything with connectors so it could easily be taken apart, but there wasn’t enough room and I didn’t want bigger boxes.

I did some continuity testing along the way and everything worked when I connected power. With the boards ready I cut more access and ventilation holes in the boxes.

I soldered the Si7021 on to its wires outside of the enclosure so it wouldn’t be exposed to unnecessary heat and used hot gun to secure everything.

I’m really happy with how these turned out. Here’s a view of the office data on my Home Assistant dashboard.

This was definitely a project where I wished I had a 3D printer to design custom boxes. Some day, when I’m caught up on my project list and can give it proper attention. I know if I get one now I’ll spend a ton of time with it and neglect other projects in my pipeline.

Home Assistant Air Quality Monitors from IKEA Vindriktning

IKEA recently discontinued Vindriktning, their older air quality monitor.

Inside the device, they put a cubic PM1006K particle sensor. I bought three for $16.95 each last year, because I’d seen people hack them by adding sensors and a Wi-Fi microcontroller to send all of the data to Home Assistant. For my modding I bought:

The YouTube video linked above is a great guide to follow. I didn’t connect wires to the fan or the light sensor since I had no use for them. I also didn’t stack my sensors because I wanted the BME280 to be outside of the enclosure, where it would be less affected by the heat produced by the ENS160 and D1.

Even with the sensor outside of the case, the BME280 still reads high, because it heats itself up. I actually tested different lengths of wires and placements of the sensor before realizing I was still going to have to adjust the data. An ESPHome filter made the adjustment easy, which I did individually for each unit after comparing to a mobile Ecobee thermostat sensor. This is the code from the unit for my shop.

substitutions:
  slug: shop
  friendly: Shop

esphome:
  name: ${slug}-air-quality
  friendly_name: ${friendly} Air Quality

esp8266:
  board: d1_mini

logger:
  level: WARN

api:
  encryption:
    key: 'xxx'

ota:
  - platform: esphome
    password: 'xxx'

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  manual_ip:
    static_ip: xxx
    gateway: xxx
    subnet: 255.255.255.0

i2c:
  frequency: 100kHz

uart:
  - rx_pin: D7
    baud_rate: 9600

sensor:
  - platform: pm1006
    pm_2_5:
      name: PM 2.5µm

  - platform: bme280_i2c
    address: 0x76
    temperature:
      name: Temperature
      id: ${slug}_temp
      filters:
        - offset: -3.38
    humidity:
      name: Humidity
      id: ${slug}_humid
      filters:
        - offset: 7.63
    iir_filter: 16x

  - platform: aht10
    variant: AHT20
    temperature:
      name: AHT21 Temperature
      id: ${slug}_aht21_temp
    humidity:
      name: AHT21 Humidity
      id: ${slug}_aht21_humid

  - platform: ens160_i2c
    address: 0x53
    eco2:
      name: CO²
    tvoc:
      name: VOC
    aqi:
      id: ${slug}_aqi
      name: AQI
    compensation:
      temperature: ${slug}_aht21_temp
      humidity: ${slug}_aht21_humid

text_sensor:
  - platform: template
    name: AQI Rating
    lambda: |-
      switch ( (int) ( id( ${slug}_aqi ).state ) ) {
        case 1: return {"Excellent"};
        case 2: return {"Good"};
        case 3: return {"Moderate"};
        case 4: return {"Poor"};
        case 5: return {"Unhealthy"};
        default: return {"N/A"};
      }

These resources were a huge help when I wired everything up and made changes to the YAML code:

Here is how I’m displaying the data on one of my Home Assistant dashboards.

As I was working on this project I knew I wanted a couple more air quality monitors around the house, which will be finished soon.

Update: I’ve had to make a small update by adding a 47uF capacitor to each ENS160 board, because they have power issues, causing the reading to stop for periods of time. My boards matched up with the right ones in the picture at that link. Here’s a picture of another ENS160 I modified, since it was a tight squeeze to made the modification on the devices I posted about here with everything already wired up. I also realized I was powering these through the 3V3 pin instead of VIN, so I fixed that.

I’ve also improved the display of the data on my dashboard by using mini-graph-card.

Modding a Star Wars LED Sign

Several years ago I bought this sign from T.J.Maxx.

When I plugged it in, I was disappointed. By default it was off with a button on the side to toggle between bright, dim, and off.

I put the sign in a display cabinet with all of the LEGO and I had wanted it to automatically turn on with the rest of the LEDs in the cabinet. I never got to it, so it sat on the shelf for years. Fast forward to setting up home automations at the new house and it was time to fix the problem. The only screw on the back was for opening a battery compartment, so I figured the front had to be snapped in. With a little careful persuasion I gained entry.

I figured the electronics were pretty basic and I was right. The quick fix was to connect the sides of the button/switch.

That worked, but I noticed how flimsy all the wiring was. I replaced the wires going from the USB connector to the board, which had been causing some flickering when bumped.

I was sad at the lack of LEDs though. I could do better, with minimal effort. I took out the circuit boards and found an old five volt LED strip.

With the help of some double-sided tape, I wrapped the strip throughout the case and then also used hot glue.

Much better!

Creating an ESPHome Remote Control Device with Infrared & Radio Frequency

In order to automate the processes of getting the golf sim ready to play and shutting it all down when finished I needed to create a remote control device. I’m using Home Assistant (HA) to run my home smart system (more posts to come), but two things involved with the golf sim aren’t connected to the network:

The projector has an infrared (IR) remote and the light has a radio frequency (RF) remote. I’ve done some things with IR and still had a stash of IR LEDs (for transmitting) and receivers. I’ve never attempted any RF stuff, so I ordered a 5 pack of 433mhz wireless RF transmitter and receiver pairs.

Since I’m using HA, I let ESPHome handle all of the main programming. All I had to do was wire everything properly and get the configuration correct. I made use of an old ESP8266 NodeMCU microcontroller and worked on the IR aspect of the project first.

When I took the picture I was using a 470Ω resistor, which I eventually switched to 100Ω, to increase the strength of the IR signal. The transistor is a PN2222A. Here’s the ESPHome configuration:

esphome:
  name: golf-remote
  friendly_name: Golf Remote

esp8266:
  board: nodemcuv2

logger:

api:
  encryption:
    key: "xxxxxxxxxx"

ota:
  - platform: esphome
    password: "xxxxxxxxxx"

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  manual_ip:
    static_ip: x.x.x.x
    gateway: x.x.x.x
    subnet: 255.255.255.0

remote_receiver:
  - id: GOLF_IR_RX
    pin:
      number: D1
      inverted: True
      mode:
        input: True
        pullup: True
    dump: all

remote_transmitter:
  - id: GOLF_IR_TX
    pin: D2
    carrier_duty_percent: 50%

I used the receiver to intercept the codes sent by the projector’s actual remote when pressing the Power, Input, and OK buttons. Then I created some buttons.

button:
  - platform: template
    name: Projector Power
    on_press:
      - remote_transmitter.transmit_nec:
          transmitter_id: GOLF_IR_TX
          address: 0x3000
          command: 0xFD02
  - platform: template
    name: Projector Input
    on_press:
      - remote_transmitter.transmit_nec:
          transmitter_id: GOLF_IR_TX
          address: 0x3000
          command: 0xFB04
  - platform: template
    name: Projector OK
    on_press:
      - remote_transmitter.transmit_nec:
          transmitter_id: GOLF_IR_TX
          address: 0x7788
          command: 0xE619

It all went very smooth. Next I connected the circuits for the RF components, which was straightforward. Here are the pinouts from the Amazon product page.

I soldered on the antennas (smaller one to the transmitter) and connected everything on the breadboard.

By using examples from the documentation I was able to intercept RF codes.

When I tried to recreate those codes through the transmitter the results weren’t matching up and the spotlight wasn’t responding. It took some trial and error to configure the various parameters of the receiver. Here’s the end result, with the combined configuration for IR and RF.

esphome:
  name: golf-remote
  friendly_name: Golf Remote

esp8266:
  board: nodemcuv2

logger:

api:
  encryption:
    key: "xxxxxxxxxx"

ota:
  - platform: esphome
    password: "xxxxxxxxxx"

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  manual_ip:
    static_ip: x.x.x.x
    gateway: x.x.x.x
    subnet: 255.255.255.0

remote_receiver:
  - id: GOLF_IR_RX
    pin:
      number: D1
      inverted: True
      mode:
        input: True
        pullup: True
    dump: all
  - id: GOLF_RF_RX
    pin:
      number: D6
      mode:
        input: True
        pullup: True
    dump:
      - rc_switch
    tolerance: 50%
    filter: 250us
    idle: 4ms
    buffer_size: 2kb # only for ESP8266

remote_transmitter:
  - id: GOLF_IR_TX
    pin: D2
    carrier_duty_percent: 50%
  - id: GOLF_RF_TX
    pin: D6
    carrier_duty_percent: 100%

After using the remote_receiver instances to get the button press codes I needed, I commented out that section of the code. If I ever need to add more functionality to my remote, I can enable the receivers at that point. Here are the button codes for the spotlight.

  - platform: template
    name: Spotlight On
    on_press:
      - remote_transmitter.transmit_rc_switch_raw:
          transmitter_id: GOLF_RF_TX
          code: '111001000000100100000011'
          protocol: 1
          repeat:
            times: 10
            wait_time: 0s
  - platform: template
    name: Spotlight Off
    on_press:
      - remote_transmitter.transmit_rc_switch_raw:
          transmitter_id: GOLF_RF_TX
          code: '111001000000100100000001'
          protocol: 1
          repeat:
            times: 10
            wait_time: 0s
  - platform: template
    name: Spotlight Green
    on_press:
      - remote_transmitter.transmit_rc_switch_raw:
          transmitter_id: GOLF_RF_TX
          code: '111001000000100100000111'
          protocol: 1
          repeat:
            times: 10
            wait_time: 0s

Then I was able to use both sets of buttons in scripts, which can feed to Alexa for voice commands.

Once everything was tested I wired and soldered a more permanent circuitboard. I included a folded dollar bill for scale.

I was planning to mount it in the ceiling, but the IR was having trouble, because the projector’s receiver faces the ground. Mounting it to the side of the PC cart worked great.

This was a lot of fun!

Update: Less than a week later I’ve already modified it, by adding a DHT22, which reports temperature and humidity. Might as well use that empty D7 pin on the microcontroller.