AlorAir Sentinel HD55S Dehumidifier for Home Assistant with ESPHome

Working from excellent information in a Home Assistant forum thread, I put together an ESPHome device to read information from and send commands to our AlorAir Sentinel HD55S Dehumidifier.

The dehumidifier has an RJ45 port on the front, which can be used to communicate over the CAN bus protocol.

To make my ESPHome device I used:

All testing was done on a breadboard.

Connections:

  • ESP32 G -> CAN Board G -> RJ45 Pin 8 -> Capacitor stripe side
  • ESP32 3.3 -> CAN Board 3.3 -> Capacitor non-stripe side
  • ESP32 Pin 4 -> CAN Board TX
  • ESP32 Pin 5 -> CAN Board RX
  • CAN Board CANH -> RJ45 Pin 5
  • CAN Board CANL -> RJ45 Pin 4

Start with this YAML for your ESPHome device:

# Add your standard esphome, ota, api, and wifi sections here at the top

logger:
  level: DEBUG

esp32:
  board: esp32-c3-devkitm-1
  framework:
    type: esp-idf

canbus:
  - platform: esp32_can
    id: can
    tx_pin: GPIO4
    rx_pin: GPIO5
    bit_rate: 125KBPS
    can_id: 0x05  
    use_extended_id: false

    on_frame:
      - can_id: 0x000
        can_id_mask: 0x000 # This catches EVERYTHING
        then:
          - lambda: |-
              std::string res = "";
              for (int i = 0; i < x.size(); i++) {
                char buf[5];
                sprintf(buf, "%02X ", x[i]);
                res += buf;
              }
              ESP_LOGD("ALORAIR_FINDER", "Received ID: 0x%03X, Data: %s", can_id, res.c_str());

# ====== Periodic Polling to Request Status ======
interval:
  - interval: 10s
    then:
      - canbus.send:
          can_id: 0x123
          use_extended_id: false
          data: [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]

This will test sending and receiving. After you flash the device and have it connected to the dehumidifier, open the ESPHome device log and look for lines like below.

If you see errors after the send or don’t receive messages back, try changing the bit_rate from 125 to 50, install, and recheck the logs.

If you do receive messages back, make note of the value after Received ID, which is 0x123 above. Then open up the ESPHome device’s YAML again and replace everything below type: esp-idf with the following.

# ──────────────────────────────────────────────
# CAN Bus Protocol
# ──────────────────────────────────────────────
# Bus: 125Kbps, CAN ID 0x123 for both directions
#
# Status frame (from unit):
#   Byte 0: Humidity (%)
#   Byte 1: Setpoint (%), 35 = Continuous mode
#   Byte 2: Temperature (°C)
#   Byte 3: Unused
#   Byte 4: Status bits
#     Bit 0 (0x01): Power on
#     Bit 1 (0x02): Compressor running
#     Bit 3 (0x08): Continuous mode / Defrost
#   Bytes 5-7: Unused?
#
# Command frame (to unit):
#   Byte 0: Setpoint + 128 (to change setpoint)
#   Byte 2: 0x01 = toggle power
#   Continuous mode: send setpoint 35 + 128 = 163 (0xA3)
#
# Echo filtering: commands have byte 0 > 128,
#   status frames have byte 0 < 100
# ──────────────────────────────────────────────

script:
  - id: refresh_status
    mode: restart
    then:
      - delay: 1s
      - canbus.send:
          can_id: 0x123
          data: [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]

interval:
  - interval: 60s
    then:
      - script.execute: refresh_status

time:
  - platform: homeassistant
    id: homeassistant_time

globals:
  - id: power
    type: bool
    restore_value: yes
  - id: continuous
    type: bool
    restore_value: yes

canbus:
  - platform: esp32_can
    id: can
    tx_pin: GPIO4
    rx_pin: GPIO5
    bit_rate: 125KBPS
    can_id: 0x05
    use_extended_id: false

    on_frame:
      - can_id: 0x123
        then:
          - lambda: |-
              if (x.size() < 5 || x[0] == 0) return;

              // Debug logging (uncomment the lines below if you need to debug the data packets)
              // char buf[64];
              // snprintf(buf, sizeof(buf), "%02X %02X %02X %02X %02X %02X %02X %02X",
              //   x[0], x.size() > 1 ? x[1] : 0,
              //   x.size() > 2 ? x[2] : 0, x.size() > 3 ? x[3] : 0,
              //   x.size() > 4 ? x[4] : 0, x.size() > 5 ? x[5] : 0,
              //   x.size() > 6 ? x[6] : 0, x.size() > 7 ? x[7] : 0);
              // ESP_LOGD("canbus", "0x123 [%d]: %s", x.size(), buf);

              // Filter out command echoes (byte 0 > 128)
              if (x[0] >= 100) return;

              // Byte 0: Humidity (%)
              id(humidity).publish_state(x[0]);

              // Byte 1: Setpoint (%), 35 = continuous mode
              id(setpoint_value).publish_state(x[1]);

              // Byte 2: Temperature (°C, convert to °F)
              id(temperature).publish_state(x[2] * 9.0 / 5.0 + 32.0);

              // Byte 4: Status bits
              bool power_on    = x[4] & 0x01;
              bool compressor  = x[4] & 0x02;
              bool cont_defrost = x[4] & 0x08;
              bool is_continuous = (x[1] == 35);

              // Update sensors
              id(compressor_status).publish_state(compressor);
              id(defrost_status).publish_state(cont_defrost && !is_continuous);

              // Sync globals
              id(power) = power_on || is_continuous;
              id(continuous) = is_continuous;

              // Build status string
              std::string status_str;
              if (!power_on && !is_continuous) {
                status_str = "Off";
              } else if (is_continuous && cont_defrost) {
                status_str = "Continuous";
                if (compressor) status_str += ", Compressor On";
              } else if (cont_defrost) {
                status_str = "Defrosting";
              } else if (compressor) {
                status_str = "Running";
              } else if (power_on) {
                status_str = "Idle";
              }
              id(status_text).publish_state(status_str.c_str());

              // Update timestamp
              auto time = id(homeassistant_time).now();
              if (time.is_valid()) {
                char time_str[20];
                time_t now = time.timestamp;
                strftime(time_str, sizeof(time_str), "%H:%M:%S", localtime(&now));
                id(last_update).publish_state(time_str);
              }

              // Alert on unexpected bits in byte 4
              uint8_t unknown_b4 = x[4] & ~0x0B; // mask out known bits 0,1,3
              if (unknown_b4 != 0) {
                ESP_LOGW("canbus", "Unknown bits in byte 4: 0x%02X", unknown_b4);
                id(unknown_data).publish_state("Byte 4 unknown bits: 0x" + 
                  std::string(1, "0123456789ABCDEF"[(unknown_b4 >> 4) & 0x0F]) +
                  std::string(1, "0123456789ABCDEF"[unknown_b4 & 0x0F]));
              }

              // Alert on any non-zero data in bytes 3, 5, 6, 7
              if (x[3] != 0 || x[5] != 0 || x[6] != 0 || x[7] != 0) {
                ESP_LOGW("canbus", "Unexpected data in unused bytes: B3=0x%02X B5=0x%02X B6=0x%02X B7=0x%02X", x[3], x[5], x[6], x[7]);
                char msg[64];
                snprintf(msg, sizeof(msg), "B3:%02X B5:%02X B6:%02X B7:%02X", x[3], x[5], x[6], x[7]);
                id(unknown_data).publish_state(msg);
              }

text_sensor:
  - platform: template
    name: "Status"
    id: status_text
  - platform: template
    name: "Last CAN Update"
    id: last_update
  - platform: template
    name: "Unknown CAN Data"
    id: unknown_data
    entity_category: diagnostic

sensor:
  - platform: template
    name: "Humidity"
    id: humidity
    unit_of_measurement: "%"
    device_class: humidity
    state_class: measurement
    accuracy_decimals: 0
    filters:
      - median:
          window_size: 5
          send_every: 1
      - delta: 1

  - platform: template
    name: "Temperature"
    id: temperature
    unit_of_measurement: "°F"
    device_class: temperature
    state_class: measurement
    accuracy_decimals: 0
    filters:
      - median:
          window_size: 5
          send_every: 1
      - delta: 1

  - platform: template
    name: "Setpoint"
    id: setpoint_value
    unit_of_measurement: "%"
    accuracy_decimals: 0

binary_sensor:
  - platform: template
    name: "Compressor"
    id: compressor_status
    device_class: power

  - platform: template
    name: "Defrost"
    id: defrost_status
    device_class: cold

number:
  - platform: template
    name: "Change Setpoint"
    id: setpoint_control
    min_value: 36
    max_value: 90
    step: 1
    optimistic: true
    set_action:
      then:
        - lambda: |-
            uint8_t sp = (uint8_t)(x + 128);
            std::vector<uint8_t> data = { sp, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
            id(can).send_data(0x123, false, data);
        - script.execute: refresh_status

switch:
  - platform: template
    name: "Power"
    id: enable_dehumid
    lambda: return id(power);
    turn_on_action:
      - lambda: |-
          if (!id(power)) {
            std::vector<uint8_t> data = {0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00};
            id(can).send_data(0x123, false, data);
          }
      - script.execute: refresh_status
    turn_off_action:
      - lambda: |-
          if (id(power)) {
            // If in continuous mode, exit continuous first by setting a normal setpoint
            if (id(continuous)) {
              float sp_val = id(setpoint_control).state;
              if (std::isnan(sp_val) || sp_val < 36) sp_val = 55;
              uint8_t sp = (uint8_t)(sp_val + 128);
              std::vector<uint8_t> data = { sp, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
              id(can).send_data(0x123, false, data);
              delay(500);
            }
            std::vector<uint8_t> data = {0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00};
            id(can).send_data(0x123, false, data);
          }
      - script.execute: refresh_status

  - platform: template
    name: "Continuous Mode"
    id: continuous_mode_enable
    lambda: return id(continuous);
    turn_on_action:
      - lambda: |-
          // Send setpoint 35 (below minimum) to enter continuous mode
          uint8_t sp = (uint8_t)(35 + 128);
          std::vector<uint8_t> data = { sp, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
          id(can).send_data(0x123, false, data);
      - script.execute: refresh_status
    turn_off_action:
      - lambda: |-
          // Exit continuous by sending a normal setpoint
          float sp_val = id(setpoint_control).state;
          if (std::isnan(sp_val) || sp_val < 36) {
            sp_val = id(setpoint_value).state;
          }
          if (std::isnan(sp_val) || sp_val < 36) {
            sp_val = 55;
          }
          uint8_t sp = (uint8_t)(sp_val + 128);
          std::vector<uint8_t> data = { sp, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
          id(can).send_data(0x123, false, data);
      - script.execute: refresh_status

automation:
  - alias: "Alorair Unknown CAN Data Alert"
    trigger:
      - platform: state
        entity_id: sensor.alorair_dehumidifier_unknown_can_data
        not_to:
          - ""
          - "unknown"
          - "unavailable"
    action:
      - service: notify.mobile_app_YOUR_PHONE
        data:
          title: "Alorair: Unknown CAN Data"
          message: "{{ trigger.to_state.state }}"
          data:
            tag: alorair_unknown_can
            importance: low

If your ID was different than 0x123 (0x3b0 seems common), change the can_id under on_frame. Update mobile_app_YOUR_PHONE for your phone. Install this new code to the device. Go to Settings -> Devices & services -> ESPHome. If your device isn’t already listed, add it, and click through to see the entities. Hopefully you see the Controls and Sensors with data.

Try to toggle the power and change the setpoint. If it’s working, the dehumidifier should respond. Good luck! If all is working, I suggest adjusting your logging level to WARN. There is some stuff in the YAML to alert if the unit sends data that isn’t recognized, because there might be flags or statuses not implemented.

If you install the stack-in-card via HACS, here’s some YAML for your dashboard. Make sure to update the entity names to match what you have.

type: custom:stack-in-card
title: Dehumidifier
cards:
  - type: markdown
    content: >
      Status: {{ states('sensor.alorair_dehumidifier_status') }}

  - type: entities
    entities:
      - entity: switch.alorair_dehumidifier_power
        name: Power
      - entity: sensor.alorair_dehumidifier_setpoint
        name: Setpoint
      - entity: number.alorair_dehumidifier_change_setpoint
        name: Change Setpoint
      - entity: switch.alorair_dehumidifier_continuous_mode
        name: Continuous Mode
  - type: glance
    entities:
      - entity: sensor.alorair_dehumidifier_humidity
        name: Humidity
      - entity: sensor.alorair_dehumidifier_temperature
        name: Temperature
    columns: 2

  - type: glance
    entities:
      - entity: binary_sensor.alorair_dehumidifier_compressor
        name: Compressor
      - entity: binary_sensor.alorair_dehumidifier_defrost
        name: Defrost
    columns: 2

Once I got everything working, I soldered to a permanent board and hot glued it inside a project box.

I haven’t worked on automations yet, but I have time before spring arrives when I need to run the dehumidifier.

Using Home Assistant to Balance Humidity for Comfort and Window Condensation Risk

The first winter in our house was a challenge figuring out where to set the house humidifier connected to our furnace. How do you balance comfortable humidity against the risk of window condensation? I really had no idea and it was common to see water or ice on the inside of our windows during the coldest days.

I had wired our house humidifier through the furnace so I could use the Ecobee thermostat to control it. The Ecobee has a setting called “Front Control,” but it does a poor job balancing for comfort.

Screenshot

Remember this recommended humidity level for later.

Screenshot

Going from 36% to 45% is a large difference. From my testing, the humidity reported by Ecobee is 4-5 percentage points too high, which is also part of the problem and makes the air even drier. I wanted to do better and let Home Assistant handle everything.

First I bought a device with good reviews to be my source of truth and set it in the living room. It’s the Govee H5075 Bluetooth Digital Hygrometer and connects to Home Assistant via an ESPHome Bluetooth Proxy.

Since we don’t use the humidifier all year, the first step was to create a Toggle (or Input Boolean) helper. I’ll manually change this when I enable/disable the humidifier in the Ecobee settings.

I installed the Thermal Comfort integration to calculate the indoor dew point and the OpenWeatherMap integration with their One Call API 3.0 for fetching weather forecasts. I use a trigger template in templates.yaml to store the forecast every hour and whenever Home Assistant starts.

- trigger:
    - platform: time_pattern
      minutes: "/59"
    - platform: homeassistant
      event: start
  action:
    - service: weather.get_forecasts
      data:
        type: hourly
      target:
        entity_id: weather.openweathermap
      response_variable: hourly_forecast
  sensor:
    - name: "Weather Forecast Storage"
      unique_id: weather_forecast_storage
      icon: mdi:weather-cloudy
      state: "{{ states('weather.openweathermap') }}"
      attributes:
        forecast: "{{ hourly_forecast['weather.openweathermap'].forecast }}"

The forecast is important because I don’t want to set the humidity too high and not have enough time for the house to dry out when a cold front moves in. I added another trigger template in templates.yaml, which grabs the low temperature over the next 12 hours, estimates window glass temperature, calculates an ideal indoor humidity, and determines an offset between the trusted and Ecobee humidity values. It does all of this every 15 minutes, when Home Assistant starts, and whenever there is a change to various temperature or humidity values.

- trigger:
    - platform: time_pattern
      minutes: "/15"
    - platform: homeassistant
      event: start
    - platform: state
      entity_id: 
        - sensor.living_room_govee_temperature
        - sensor.living_room_govee_humidity
        - sensor.openweathermap_temperature
        - sensor.thermostat_current_temperature
        - sensor.thermostat_current_humidity
      for:
        seconds: 10
  sensor:
    - name: "Calculated Ideal Humidity"
      unique_id: calculated_ideal_humidity
      unit_of_measurement: "%"
      variables:
        forecast: "{{ state_attr('sensor.weather_forecast_storage', 'forecast') }}"
        has_data: "{{ forecast is not none }}"
        outdoor: "{{ (forecast[1:12] | map(attribute='temperature') | min | float(50)) if has_data else 50 }}"
        indoor: "{{ states('sensor.living_room_govee_temperature') | float(70) }}"
        glass: "{{ (outdoor + (indoor - outdoor) * 0.6) | round(1) }}" # 0.6 should be adjusted based on window efficiency
        ideal_raw: "{{ (0.8 * glass) | int(35) }}"
        ideal_final: "{{ ([25, ideal_raw, 50] | sort)[1] }}"
        
        govee_h: "{{ states('sensor.living_room_govee_humidity') | float(30) }}"
        ecobee_h: "{{ state_attr('climate.thermostat', 'current_humidity') | float(30) }}"
        offset: "{{ ecobee_h - govee_h }}"
        comp_target: "{{ ([25, (ideal_final + offset) | int(35), 50] | sort)[1] }}"

      state: "{{ ideal_final if has_data else states('sensor.calculated_ideal_humidity') }}"
      
      attributes:
        glass_temp_estimate: "{{ glass }}"
        predicted_outdoor_temp: "{{ outdoor }}"
        forecast_window: "Next 12 Hours"
        ecobee_compensated_target: "{{ comp_target }}"
        last_calculation: "{{ now().strftime('%Y-%m-%d %H:%M:%S') }}"

With this data calculated and saved, an automation takes care of updating the humidity setpoint on the Ecobee thermostat. Whenever I have the Humidifier Enabled toggle on, it’ll update the setpoint every 30 minutes and whenever the Calculated Ideal Humidity changes.

alias: Set Thermostat Humidity
triggers:
  - entity_id: sensor.calculated_ideal_humidity
    trigger: state
  - minutes: /30
    trigger: time_pattern
conditions:
  - condition: and
    conditions:
      - condition: state
        entity_id: input_boolean.humidifier_enabled
        state:
          - "on"
      - condition: template
        value_template: >-
          {{ state_attr('sensor.calculated_ideal_humidity',
          'ecobee_compensated_target') is not none }}
        alias: Ecobee Compensated Target is not None
actions:
  - target:
      entity_id: climate.thermostat
    data:
      humidity: >-
        {{ state_attr('sensor.calculated_ideal_humidity',
        'ecobee_compensated_target') | int(35) }}
    action: climate.set_humidity
mode: restart

I added a card on my dashboard to see the status of everything. This was invaluable when I was testing and tweaking for several weeks.

Remember the Ecobee’s recommended 36%?! If I went by that, the actual humidity in the house at this time would have about 32%, a whole 10 percentage points dryer! Here’s the YAML for the dashboard card.

type: conditional
conditions:
  - condition: state
    entity: input_boolean.humidifier_enabled
    state: "on"
card:
  type: markdown
  content: |2-
      {% set glass_temp = state_attr('sensor.calculated_ideal_humidity', 'glass_temp_estimate') | float(0) | round (0) %}
      {% set dew_point = states('sensor.thermal_comfort_dew_point') | float(0) | round(0) %}
      {% set ideal_hum = states('sensor.calculated_ideal_humidity') | float(0) | round(0) %}
      {% set actual_hum = states('sensor.living_room_govee_humidity') | float(0) | round(0) %}
      {% set thermostat_hum = state_attr( 'climate.thermostat', 'current_humidity' ) | float(0) | round(0) %}
      {% set target_hum = state_attr( 'sensor.calculated_ideal_humidity', 'ecobee_compensated_target' ) | float(0) | round(0) %}
      {% set future_temp = state_attr('sensor.calculated_ideal_humidity', 'predicted_outdoor_temp') | round (0) %}

      ## Window Condensation Status
      {% if ( glass_temp > dew_point ) %}
      #### ✅ No Risk
      {% else %}
      #### ⚠️ Risk
      {% endif %}

      **Glass:** {{ glass_temp }}°
      **Dew Point:** {{ dew_point }}°

      **12hr Forecast Low:** {{ future_temp }}°

      ---

      ### Humidity
      {% if ( actual_hum > ideal_hum ) %}
      ⚠️ Too Humid
      {% elif ( actual_hum < ideal_hum ) %}
      💧 Too Dry
      {% else %}
      ✅ Good
      {% endif %}

      | | Current | Target |
      | :--- | :---: | :---: |
      | **Actual** | {{ actual_hum }}% | {{ ideal_hum }}% |
      | **Ecobee** | {{ thermostat_hum }}% | {{ target_hum }}% |

The windows are so much better this winter and our static shocks are limited. If you have any suggested improvements, leave a comment.

Reading Badger Water Meter Data for Home Assistant

If you have Badger water meters with the Orion Endpoint GIF2014W-OSE, this post should help you read the data for use in Home Assistant. Here is what my meters look like. I have separate lines and meters for the house and outdoor, since they get charged at different rates.

The GIF2014W-OSE broadcasts data from the water meter over radio frequency (RF). Monthly or quarterly the township water department drives around neighborhoods reading these RF signals to determine everyone’s water usage. You can read the same data with a software defined radio (SDR). I’m using a RTL-SDR Blog V4 dongle with a 915MHz LoRa Antenna and ferrite beads (to filter noise) on a USB extension cable. It plugs directly in to my Home Assistant server.

If you want to test the SDR on your computer before setting everything up in Home Assistant check out the rtl_433 repo on GitHub.

To get started in HA, make sure you’re running a MQTT broker. I use the recommended Mosquitto broker. Install the MQTT Explorer and rtl_433 (next) apps. Note, the main rtl_433 HA app wasn’t updated yet with the latest version of rtl_433, which is needed for the GIF2014W-OSE, so that’s why I had to use the (next) version. It may be updated by the time you’re reading this.

The configuration for the rtl_433 (next) app is stored in a file. I have mine at /homeassistant/rtl_433/rtl_433.conf and here is how my app configuration looks.

Below is a good start for the config file’s contents. Put in your IP address, user, and password.

sample_rate 1600k
gain 28
hop_interval 22
pulse_detect autolevel
pulse_detect minlevel=-35
report_meta level
report_meta noise
report_meta stats:1
output mqtt://[MQTT_IP],user=[MQTT_USER],pass=[MQTT_PASSWORD],retain=1

protocol 282

frequency 905.3M
frequency 906.1M
frequency 906.9M
frequency 907.7M
frequency 908.5M
frequency 909.3M
frequency 910.1M
frequency 910.9M
frequency 911.7M
frequency 912.5M
frequency 913.3M
frequency 914.1M
frequency 914.9M
frequency 915.7M
frequency 916.5M
frequency 917.3M
frequency 918.1M
frequency 918.9M
frequency 919.7M
frequency 920.5M
frequency 921.3M
frequency 922.1M
frequency 922.9M
frequency 923.7M

The water meter endpoint broadcasts across the range of 904.4 to 924.6Mhz and this is going to hop between the 24 frequencies (taken from a GitHub comment) listed in the config every 22 seconds. You may need to tweak the gain, pulse_detect, and other settings depending on your setup. Use your favorite AI to help. If you these next couple of lines to the config file you can see more of what happening, but I wouldn’t leave these in after everything is working.

verbose 7
output json
output log

You can view the HA app’s logs to see more details. When everything is running, open up MQTT Explorer and look for the rtl_433 topics. Hopefully within an hour or two you’ll see something similar to this.

In this screenshot 40338908 and 40313961 are the ids for my meter endpoints. You may see a bunch of other ids for other meters in your neighborhood. You may have physical tags on the devices with their IDs or you can compare the reading values to what is displayed on the water meter.

Now this data can be brought in to Home Assistant. It’ll take some tinkering and customization to fit it all for your HA install. I’m also writing this over a week after setting everything up, so I don’t remember the order of adding everything, but you should be able to do most of it at once and restart. Make sure to update the name, unique_id, source, and state everywhere to fit your needs. The state_topic‘s need to match what you see in MQTT Explorer, and any other cross references have to match your renames. Only use what applies to your situation. If you only have one meter, remove all the outdoor/sprinkler stuff. I’m scraping the Saginaw Township water rates from their web site, so that stuff is going to be very custom, but gives you an idea of what you can do.

In configuration.yaml:

mqtt:
  binary_sensor:
    - name: "House Water Meter Leak"
      unique_id: "house_water_meter_leak"
      state_topic: "rtl_433/9b13b3f4-rtl433-next/devices/Orion-Endpoint/40313908/leaking"
      payload_on: "1"
      payload_off: "0"
      device_class: moisture
    - name: "Outdoor Water Meter Leak"
      unique_id: "outdoor_water_meter_leak"
      state_topic: "rtl_433/9b13b3f4-rtl433-next/devices/Orion-Endpoint/40313961/leaking"
      payload_on: "1"
      payload_off: "0"
      device_class: moisture
  sensor:
    - name: "House - Water Meter"
      unique_id: "house_water_meter"
      state_topic: "rtl_433/9b13b3f4-rtl433-next/devices/Orion-Endpoint/40313908/reading"
      unit_of_measurement: "gal"
      device_class: water
      state_class: total_increasing
      icon: mdi:water-pump
      value_template: "{{ value | float(0) / 10 }}"
    - name: "House - Water Meter Daily Snap"
      unique_id: "house_water_meter_daily_snap"
      state_topic: "rtl_433/9b13b3f4-rtl433-next/devices/Orion-Endpoint/40313908/daily_reading"
      device_class: water
      state_class: total
      unit_of_measurement: "gal"
      entity_category: diagnostic
      icon: mdi:history
      value_template: "{{ value | float(0) / 10 }}"
    - name: "House - Water Meter - Freq 1"
      unique_id: "house_water_meter_freq1"
      state_topic: "rtl_433/9b13b3f4-rtl433-next/devices/Orion-Endpoint/40313908/freq1"
      value_template: "{{ value | float(0) }}"
      unit_of_measurement: "MHz"
      state_class: measurement
      device_class: frequency
    - name: "House - Water Meter - Freq 2"
      unique_id: "house_water_meter_freq2"
      state_topic: "rtl_433/9b13b3f4-rtl433-next/devices/Orion-Endpoint/40313908/freq2"
      value_template: "{{ value | float(0) }}"
      unit_of_measurement: "MHz"
      state_class: measurement
      device_class: frequency
    - name: "Outdoor - Water Meter"
      unique_id: "water_meter_outdoor"
      state_topic: "rtl_433/9b13b3f4-rtl433-next/devices/Orion-Endpoint/40313961/reading"
      unit_of_measurement: "gal"
      device_class: water
      state_class: total_increasing
      icon: mdi:water-pump
      value_template: "{{ value | float(0) / 10 }}"
    - name: "Outdoor - Water Meter Daily Snap"
      unique_id: "outdoor_water_meter_daily_snap"
      state_topic: "rtl_433/9b13b3f4-rtl433-next/devices/Orion-Endpoint/40313961/daily_reading"
      device_class: water
      state_class: total
      unit_of_measurement: "gal"
      entity_category: diagnostic
      icon: mdi:history
      value_template: "{{ value | float(0) / 10 }}"
    - name: "Outdoor - Water Meter - Freq 1"
      unique_id: "outdoor_water_meter_freq1"
      state_topic: "rtl_433/9b13b3f4-rtl433-next/devices/Orion-Endpoint/40313961/freq1"
      value_template: "{{ value | float(0) }}"
      unit_of_measurement: "MHz"
      state_class: measurement
      device_class: frequency
    - name: "Outdoor - Water Meter - Freq 2"
      unique_id: "outdoor_water_meter_freq2"
      state_topic: "rtl_433/9b13b3f4-rtl433-next/devices/Orion-Endpoint/40313961/freq2"
      value_template: "{{ value | float(0) }}"
      unit_of_measurement: "MHz"
      state_class: measurement
      device_class: frequency

scrape:
  - resource: https://saginawtownship.org/departments/public_services/water_distribution.php
    scan_interval: 86400 
    sensor:
      - name: "Saginaw House Water Rate Raw"
        select: "article ul li"
        index: 2
        value_template: >
          {{ value | regex_findall_index('Combined:\s*\$?\s*(\d+\.\d+)', ignorecase=True) }}
      - name: "Saginaw Outdoor Water Rate Raw"
        select: "article ul li"
        index: 2
        value_template: >
          {{ value | regex_findall_index('\(Sprinkler Meter\)\s*\$?\s*(\d+\.\d+)', ignorecase=True) }}

In templates.yaml:

- binary_sensor:
  - name: "Outdoor Water Flowing Hourly"
    unique_id: outdoor_water_flowing_hourly
    device_class: moving
    state: >
      {{ states('sensor.outdoor_water_hourly_change') | float(0) > 0 }}
  - name: "House Water Flowing Hourly"
    unique_id: house_water_flowing_hourly
    device_class: moving
    state: >
      {{ states('sensor.house_water_hourly_change') | float(0) > 0 }}
  - name: "Sprinklers Watering"
    unique_id: sprinklers_watering
    device_class: running
    state: >
      {{ is_state('binary_sensor.sprinkler_zone_1_watering', 'on')
         or is_state('binary_sensor.sprinkler_zone_2_watering', 'on')
         or is_state('binary_sensor.sprinkler_zone_3_watering', 'on')
         or is_state('binary_sensor.sprinkler_zone_4_watering', 'on')
         or is_state('binary_sensor.sprinkler_zone_5_watering', 'on')
         or is_state('binary_sensor.sprinkler_zone_6_watering', 'on') }}
  - name: "Outdoor Water Leak Persistent"
    unique_id: outdoor_water_leak_persistent
    device_class: problem
    delay_on: "02:00:00"
    state: >
      {% set water_usage = states('sensor.outdoor_water_hourly_change') | float(0) > 0 %}
      {% set sprinklers_active = states('sensor.sprinklers_active_last_hour') | float(0) > 0 %}

      {{ water_usage and not sprinklers_active }}
  - name: "House Water Nightly Leak Possible"
    unique_id: house_water_nightly_leak_possible
    device_class: problem
    state: >
    
      {% if now().hour == 6 and now().minute == 0 %}
        {# If water usage increased 7/9 hours from 9p-6a #}
        {# (allowing for minor signal lag and missed reads) #}
        {{ states('sensor.house_water_flowing_9_hours') | float(0) > 6.8 }}
      {% else %}
        {{ states('binary_sensor.house_water_nightly_leak_possible') }}
      {% endif %}

In automations.yaml:

- id: '1770044944973'
  alias: Notify - Water Meter Leak Detected
  description: ''
  triggers:
  - trigger: state
    entity_id:
    - binary_sensor.house_water_meter_leak
    - binary_sensor.outdoor_water_meter_leak
    from:
    - 'off'
    to:
    - 'on'
  actions:
  - action: notify.mobile_app_nickphone
    metadata: {}
    data:
      title: "\U0001F6B0 Water Leak Flag Detected!"
      message: The internal leak flag for {{ trigger.to_state.name }} has been triggered.            This
        usually indicates continuous flow for the last 24 hours.
      data:
        priority: high
        ttl: 0
- id: '1770082584926'
  alias: Notify - Outdoor Water Leak
  description: Alerts family when unexplained water flow persists for 2 hours
  triggers:
  - entity_id: binary_sensor.outdoor_water_leak_persistent
    from: 'off'
    to: 'on'
    trigger: state
  conditions: []
  actions:
  - action: notify.mobile_app_nickphone
    metadata: {}
    data:
      data:
        priority: high
        ttl: 0
        tag: outdoor-leak-alert
        color: '#ff0000'
      title: "\U0001F6B0 Outdoor Water Leak Detected"
      message: 'Unexplained water flow has been detected for over 2 hours. Current
        hourly rate: {{ states(''sensor.outdoor_water_hourly_change'') }} gal.'
- id: '1770084282355'
  alias: Notify - House Water Leak Possible
  description: Water meter usage increased every hour from 9p-6a
  triggers:
  - at: 07:01:00
    trigger: time
  conditions:
  - condition: state
    entity_id: binary_sensor.house_water_nightly_leak_possible
    state: 'on'
  actions:
  - data:
      title: "\U0001F3E0 House Water Alert"
      message: Potential water leak detected! Water usage increased every hour from
        9 PM to 6 AM.
      data:
        priority: high
        ttl: 0
        tag: house-leak-alert
        color: '#ffa500'
    action: notify.mobile_app_nickphone
- id: '1770485349188'
  alias: Count House Water Meter Frequency Updates
  triggers:
  - entity_id: sensor.house_water_meter_freq_1
    trigger: state
  actions:
  - target:
      entity_id: input_number.house_water_meter_freq1_updates_per_hour
    data:
      value: '{{ states(''input_number.house_water_meter_freq1_updates_per_hour'')
        | float(0) + 1 }}'
    action: input_number.set_value
- id: '1770485394121'
  alias: Count Outdoor Water Meter Frequency Updates
  triggers:
  - entity_id: sensor.outdoor_water_meter_freq_1
    trigger: state
  actions:
  - target:
      entity_id: input_number.outdoor_water_meter_freq1_updates_per_hour
    data:
      value: '{{ states(''input_number.outdoor_water_meter_freq1_updates_per_hour'')
        | float(0) + 1 }}'
    action: input_number.set_value
- id: '1770485452657'
  alias: Set - Reset Water Meter Freq Hourly
  description: ''
  triggers:
  - hours: /1
    trigger: time_pattern
    id: scheduled
  - trigger: homeassistant
    event: start
    id: restart
  conditions:
  - condition: or
    conditions:
    - condition: trigger
      id: scheduled
    - condition: template
      value_template: "{{ now().minute < 5 and (last_triggered is none or last_triggered < now().replace(minute=0, second=0, microsecond=0)) }}"
  actions:
  - action: input_number.set_value
    metadata: {}
    target:
      entity_id:
      - input_number.house_water_meter_freq1_updates_per_hour
      - input_number.outdoor_water_meter_freq1_updates_per_hour
    data:
      value: 0
- id: '1770038217308'
  alias: Notify - Saginaw Water Rate Changed
  description: ''
  triggers:
  - entity_id:
    - sensor.saginaw_house_water_rate
    - sensor.saginaw_outdoor_water_rate
    id: rate_changed
    trigger: state
    alias: Saginaw Water Rate Changes
  - entity_id:
    - sensor.saginaw_house_water_rate_raw
    - sensor.saginaw_outdoor_water_rate_raw
    to: unavailable
    for:
      seconds: 30
    id: scrape_failed
    trigger: state
    alias: Saginaw Water Rate Raw Scrape Changes
  actions:
  - choose:
    - conditions:
      - condition: trigger
        id: rate_changed
      - condition: template
        value_template: "{{ trigger.from_state.state not in ['unknown', 'unavailable']
          and \n  trigger.to_state.state not in ['unknown', 'unavailable'] }}\n"
      sequence:
      - data:
          title: "\U0001F4B0 Water Rate Change"
          message: 'Rate for {{ trigger.to_state.name }} has changed! Old Value: ${{
            trigger.from_state.state }} New Value: ${{ trigger.to_state.state }}'
          data:
            priority: high
            ttl: 0
        action: notify.mobile_app_nickphone
    - conditions:
      - condition: trigger
        id: scrape_failed
      sequence:
      - data:
          title: '⚠️ Scrape Error: Saginaw Water'
          message: 'The scrape for {{ trigger.to_state.name }} failed or returned
            non-numeric data.  Stable value of ${{ states(trigger.entity_id.replace(''_raw'',
            '''')) }} is being maintained. Check: https://saginawtownship.org/departments/public_services/water_distribution.php'
          data:
            priority: high
            ttl: 0
        action: notify.mobile_app_nickphone

In Devices -> Helpers there are a bunch, but some come from templates above and others when you add the water data to your Energy dashboard (after a reboot and data starts coming in). Here’s my list of helpers:

Some key findings when I started looking at the data. My meters are broadcasting many times per minute, but the gallons used reading only changes at 11 minutes after every hour. They do this to save the endpoint battery I guess. So no realtime data, but hourly is better than nothing.

In the data, there is a leaking flag. From what I’ve read this flag only gets turned on if the usage increases for 24 hours straight. Not great for leak detection, but would have been useful last summer when something busted on a sprinkler line and I didn’t find out for a week or so. I have leak notifications based on these flags, but my custom leak notifications should trigger sooner. If the sprinklers have been off and the outdoor usage increases two hours in a row, I’ll get an alert. If the house usage have been increasing all through the overnight I’ll get an alert in the morning.

Once everything is setup, let it run. After several days, check out the freq1 sensor data, which should look similar to these graphs.

Your meters might be different, but mine hop between three frequencies at a time. There’s a low, medium, and high. Every 8 hours (3:11am, 11:11am, and 7:11pm), one of the frequencies increases by 400MHz, so over the course of a day they all increase. Then one day they all peaked and reset to the lower end of a frequency band.

From all this data I knew:

  • Low Band: 904.8 – 910.8 MHz with changes at 7:11 pm
  • Med Band: 911.2 – 917.2 MHz with changes at 3:11 am
  • High Band: 917.6 – 923.6 MHz with changes at 11:11 am

I also tracked how many times per hour I caught data from each meter.

Not bad, but some hours I’d only get one or two data packets for a meter. Technically enough, but there was a risk of missing data. Since I knew the pattern I felt I could do smarter frequency hopping with my SDR to listen for data and then capture a lot more. It worked!

I was getting a lot more reads and rarely less than 10 in any hour. In order to do this I updated the rtl_433.conf file to replace the 24 frequencies with three bands.

# Low band - shifts at 7:11 PM ET (channels 904.8 - 910.8 MHz)
#frequency 904.7M
#frequency 904.9M
#frequency 905.1M
#frequency 905.3M
#frequency 905.5M
#frequency 905.7M
#frequency 905.9M
#frequency 906.1M
#frequency 906.3M
#frequency 906.5M
#frequency 906.7M
#frequency 906.9M
#frequency 907.1M
#frequency 907.3M
#frequency 907.5M
#frequency 907.7M
#frequency 907.9M
#frequency 908.1M
#frequency 908.3M
#frequency 908.5M
#frequency 908.7M
#frequency 908.9M
#frequency 909.1M
#frequency 909.3M
#frequency 909.5M
#frequency 909.7M
#frequency 909.9M
#frequency 910.1M
#frequency 910.3M
#frequency 910.5M
#frequency 910.7M
#frequency 910.9M

# Mid band - shifts at 3:11 AM ET (channels 911.2 - 917.2 MHz)
#frequency 911.1M
#frequency 911.3M
#frequency 911.5M
#frequency 911.7M
#frequency 911.9M
#frequency 912.1M
#frequency 912.3M
#frequency 912.5M
#frequency 912.7M
#frequency 912.9M
#frequency 913.1M
#frequency 913.3M
#frequency 913.5M
#frequency 913.7M
#frequency 913.9M
#frequency 914.1M
#frequency 914.3M
#frequency 914.5M
#frequency 914.7M
#frequency 914.9M
#frequency 915.1M
#frequency 915.3M
#frequency 915.5M
#frequency 915.7M
#frequency 915.9M
#frequency 916.1M
#frequency 916.3M
#frequency 916.5M
#frequency 916.7M
#frequency 916.9M
#frequency 917.1M
#frequency 917.3M

# High band - shifts at 11:11 AM ET (channels 917.6 - 923.6 MHz)
#frequency 917.5M
#frequency 917.7M
#frequency 917.9M
#frequency 918.1M
#frequency 918.3M
#frequency 918.5M
#frequency 918.7M
#frequency 918.9M
#frequency 919.1M
#frequency 919.3M
#frequency 919.5M
#frequency 919.7M
#frequency 919.9M
#frequency 920.1M
#frequency 920.3M
#frequency 920.5M
#frequency 920.7M
#frequency 920.9M
#frequency 921.1M
#frequency 921.3M
#frequency 921.5M
#frequency 921.7M
#frequency 921.9M
#frequency 922.1M
#frequency 922.3M
#frequency 922.5M
#frequency 922.7M
#frequency 922.9M
#frequency 923.1M
#frequency 923.3M
#frequency 923.5M
#frequency 923.7M

# ^ keep blank line

The two uncommented frequencies in each band are the ones on either side of the frequency currently used by the meters. I added a scripts folder and a new update_rtl433_channels.sh file in there.

#!/bin/bash
# update_rtl433_channels.sh
#
# Advances one band's frequency pair in the rtl_433 config.
# Two adjacent frequencies are active per band (channel ± 0.1 MHz).
# Each shift: comment both active, uncomment the next two commented lines.
# At wrap (no more commented lines after active): jump to first two.
#
# Usage: update_rtl433_channels.sh <low|mid|high> [config_path]

BAND="$1"
CONFIG_PATH="${2:-/config/rtl_433/rtl_433.conf}"

if [ -z "$BAND" ]; then
    echo "Usage: $0 <low|mid|high> [config_path]"
    exit 1
fi

if [ ! -f "$CONFIG_PATH" ]; then
    echo "Error: Config file not found: $CONFIG_PATH"
    exit 1
fi

case "$BAND" in
    low)  SECTION="# Low band -" ;;
    mid)  SECTION="# Mid band -" ;;
    high) SECTION="# High band -" ;;
    *)    echo "Error: band must be low, mid, or high"; exit 1 ;;
esac

SECTION_START=$(grep -n "$SECTION" "$CONFIG_PATH" | head -1 | cut -d: -f1)
if [ -z "$SECTION_START" ]; then
    echo "Error: Could not find section '$SECTION' in config"
    exit 1
fi

NEXT_SECTION=$(tail -n +$((SECTION_START + 1)) "$CONFIG_PATH" | grep -n "^# .* band -" | head -1 | cut -d: -f1)
if [ -z "$NEXT_SECTION" ]; then
    SECTION_END=$(wc -l < "$CONFIG_PATH")
else
    SECTION_END=$((SECTION_START + NEXT_SECTION - 1))
fi

echo "Band: $BAND (lines $SECTION_START-$SECTION_END)"

# Find all active (uncommented) frequency lines
ACTIVE_ABS=()
for LINE_NUM in $(seq "$SECTION_START" "$SECTION_END"); do
    if sed -n "${LINE_NUM}p" "$CONFIG_PATH" | grep -q "^frequency "; then
        ACTIVE_ABS+=("$LINE_NUM")
    fi
done

if [ "${#ACTIVE_ABS[@]}" -ne 2 ]; then
    echo "Error: Expected 2 active frequencies, found ${#ACTIVE_ABS[@]}"
    exit 1
fi

FIRST_ABS=${ACTIVE_ABS[0]}
SECOND_ABS=${ACTIVE_ABS[1]}

echo "Active:"
echo "  Line $FIRST_ABS: $(sed -n "${FIRST_ABS}p" "$CONFIG_PATH")"
echo "  Line $SECOND_ABS: $(sed -n "${SECOND_ABS}p" "$CONFIG_PATH")"

# Find the next TWO commented frequency lines after the LAST active line
NEXT_COMMENTED=()
for LINE_NUM in $(seq $((SECOND_ABS + 1)) "$SECTION_END"); do
    if sed -n "${LINE_NUM}p" "$CONFIG_PATH" | grep -q "^#frequency "; then
        NEXT_COMMENTED+=("$LINE_NUM")
        if [ "${#NEXT_COMMENTED[@]}" -eq 2 ]; then
            break
        fi
    fi
done

if [ "${#NEXT_COMMENTED[@]}" -lt 2 ]; then
    # At end of section — wrap to first two
    echo "End of section, wrapping to first two"

    sed -i "${FIRST_ABS}s/^frequency /#frequency /" "$CONFIG_PATH"
    sed -i "${SECOND_ABS}s/^frequency /#frequency /" "$CONFIG_PATH"

    WRAP_LINES=()
    for LINE_NUM in $(seq "$SECTION_START" "$SECTION_END"); do
        if sed -n "${LINE_NUM}p" "$CONFIG_PATH" | grep -q "^#frequency "; then
            WRAP_LINES+=("$LINE_NUM")
            if [ "${#WRAP_LINES[@]}" -eq 2 ]; then
                break
            fi
        fi
    done

    sed -i "${WRAP_LINES[0]}s/^#frequency /frequency /" "$CONFIG_PATH"
    sed -i "${WRAP_LINES[1]}s/^#frequency /frequency /" "$CONFIG_PATH"
else
    echo "Advancing: commenting $FIRST_ABS,$SECOND_ABS, uncommenting ${NEXT_COMMENTED[0]},${NEXT_COMMENTED[1]}"

    sed -i "${FIRST_ABS}s/^frequency /#frequency /" "$CONFIG_PATH"
    sed -i "${SECOND_ABS}s/^frequency /#frequency /" "$CONFIG_PATH"
    sed -i "${NEXT_COMMENTED[0]}s/^#frequency /frequency /" "$CONFIG_PATH"
    sed -i "${NEXT_COMMENTED[1]}s/^#frequency /frequency /" "$CONFIG_PATH"
fi

echo ""
echo "Active frequencies:"
grep "^frequency " "$CONFIG_PATH"

This file needs a permissions change. Install the Terminal & SSH app in HA, start it, open it, and type:
chmod +x /config/config/update_rtl433_channels.sh.

In configuration.yaml:

shell_command:
  rtl433_shift_low: 'bash /config/scripts/update_rtl433_channels.sh low /config/rtl_433/rtl_433.conf'
  rtl433_shift_mid: 'bash /config/scripts/update_rtl433_channels.sh mid /config/rtl_433/rtl_433.conf'
  rtl433_shift_high: 'bash /config/scripts/update_rtl433_channels.sh high /config/rtl_433/rtl_433.conf'

In automations.yaml:

- id: '1770813439694'
  alias: RTL 433 - Shift Low Band Frequencies
  description: Advances low band frequencies at 7:11 PM ET daily
  triggers:
  - trigger: time
    at: '19:11:00'
    id: scheduled
  - trigger: homeassistant
    event: start
    id: restart
  conditions:
  - condition: or
    conditions:
    - condition: trigger
      id: scheduled
    - condition: and
      conditions:
      - condition: time
        after: '19:11:00'
        before: '19:16:00'
      - condition: template
        value_template: >-
              {% set lt = state_attr('automation.rtl_433_shift_low_band_frequencies', 'last_triggered') %}
              {{ lt is none or lt.date() < now().date() }}
  actions:
  - action: shell_command.rtl433_shift_low
  - delay:
      seconds: 5
  - action: hassio.addon_restart
    data:
      addon: 9b13b3f4_rtl433-next
  mode: single
- id: '1770813477228'
  alias: RTL 433 - Shift Mid Band Frequencies
  description: Advances mid band frequencies at 3:11 AM ET daily
  triggers:
  - trigger: time
    at: '03:11:00'
    id: scheduled
  - trigger: homeassistant
    event: start
    id: restart
  conditions:
  - condition: or
    conditions:
    - condition: trigger
      id: scheduled
    - condition: and
      conditions:
      - condition: time
        after: '03:11:00'
        before: '03:16:00'
      - condition: template
        value_template: >-
              {% set lt = state_attr('automation.rtl_433_shift_mid_band_frequencies', 'last_triggered') %}
              {{ lt is none or lt.date() < now().date() }}
  actions:
  - action: shell_command.rtl433_shift_mid
  - delay:
      seconds: 5
  - action: hassio.addon_restart
    data:
      addon: 9b13b3f4_rtl433-next
  mode: single
- id: '1770813511146'
  alias: RTL 433 - Shift High Band Frequencies
  description: Advances high band frequencies at 11:11 AM ET daily
  triggers:
  - trigger: time
    at: '11:11:00'
    id: scheduled
  - trigger: homeassistant
    event: start
    id: restart
  conditions:
  - condition: or
    conditions:
    - condition: trigger
      id: scheduled
    - condition: and
      conditions:
      - condition: time
        after: '11:11:00'
        before: '11:16:00'
      - condition: template
        value_template: >-
              {% set lt = state_attr('automation.rtl_433_shift_high_band_frequencies', 'last_triggered') %}
              {{ lt is none or lt.date() < now().date() }}
  actions:
  - action: shell_command.rtl433_shift_high
  - delay:
      seconds: 5
  - action: hassio.addon_restart
    data:
      addon: 9b13b3f4_rtl433-next
  mode: single

Make sure to uncomment the correct frequencies in each band, update the automation trigger times, and make sure the slug for the rtl433-next app is correct. Each time an automation runs the active frequencies are updated for the associated band. Since they should be on both sides of the frequency used by the water meter both can read the RF broadcasts. When it gets to the end of the band’s frequency list, it shifts up to the top of the list. Since I need to make sure these three automations run, even if a restart happens at trigger time, there is some retry logic built in.

A lot of this is dependent on your water meters and situation, but hopefully it gives you enough information to make it work.

Update: After getting everything setup, I had some issues, collected more data, tested, collaborated with AI, improved the location of my SDR, and got it working much much better. The code is updated above and now I’m collecting 100 or more data points per meter during some hours. Check out the hourly data read counters and how much taller the bars at at the end of this chart!

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?

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!

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.

Doorbell Views

I finally bought a doorbell camera when I saw some specials last week. I went with the combination Ring Video Doorbell Pro and Ring Chime Pro, which came with a free 3rd generation Alexa Echo Dot. The Chime is great for the basement, where I can’t always hear the doorbell, especially if I’m using a loud tool. My 2nd generation Alexa Echo Dot moved to the basement as well since I spend so much time in the workshop.

Home Automation Part 2: Software

Part 1 covered the devices I’m using for home automation. It’s been over a year and there have been some changes. I added a Leviton switch and got rid of the Wink hub.

This post will be about the software that brings things together, making it easy for me to manage and allows the devices to “talk” to each other. Maybe it’s a good thing it’s taken me almost a year to write this part 2 because there have been some dramatic improvements to how everything runs. I was using homebridge for a period of time, but found I never really used HomeKit/Siri. Home Assistant has been updating a new HomeKit component, so I’ll have to give it a try.

I’ve talked about Home Assistant in a lot of posts; it’s the software running on the Raspberry Pi 3 Model B to handle everything. It’s very powerful software and has been a lot of fun for me to configure everything. Being on a Linux box and having to configure everything through YAML files makes it a tough entry for the average homeowner though. It’s improved a lot in the year I’ve been using it with better documentation and some UI configuration tools, so it’ll be interesting to watch for the next 12 months.

Being Open Source was a big draw of HA for me. Open Sourcecraft has a good article about the creator. The project operates on an aggressive schedule you don’t see much; every 2 weeks a new version oh Home Assistant is released. These aren’t small updates either. Each update fixes a ton of bugs and adds support for new devices.

I did install HA using Hass.io, which simplifies the process and makes it easy to do upgrades and install add-ons. Here are the add-ons I’m using.

ha-hassio-add-ons.png

In addition to running Home Assistant, I wrote a couple of small services I run on Pis. The first is the temperature sensor/monitor I use in my garage. I’ve improved it a lot in the last year. The second service is home-assistant-pi, which reports a bunch of data about each Raspberry Pi on my network back to Home Assistant, which you’ll see in the screenshot below.

The beauty of Home Assistant is you can set everything up the way you want. Your limitation is your imagination and your comfort level with configuration files and code. I took a bunch of screenshots of my setup.

ha-day-home.png
The main screen, with a daytime theme active.

ha-night-home
The same screen, but at night.

ha-night-temps
All of the weather and indoor climate data.

ha-night-pis
Status of my Raspberry Pis.

ha-night-rooms
Groupings by room.

ha-night-multimedia
Sort of a multimedia group, showing my TVs and cameras.

ha-night-sensors
A catchall sensor screen with less frequently used info.

img_0757
The iOS app gives access to everything and works the same way.

I keep all of my configuration (except the secrets file) in a GitHub repo in case I mess something up. I’ve learned a lot by looking at other examples, so my repo is public as well. Maybe my config will help out someone else. The repo is home-assistant-config. If you have any questions about anything you see in this post or in my configuration, let me know.

In the final post of this series, I’ll explain the cool part of this whole thing, the actual automations. I need to go through my ideas list and implement all of them though.

From Rest API to MQTT

After the recent changes to my home network and Home Assistant server, I noticed the logs in Pi-Hole were being dominated by the domain I use for dynamic DNS on the box. Roughly 15,000 DNS requests a day out of 30,000 on my entire network. Really skews the ad blocking stats.

Why so many DNS requests? Because home-assistant-pi and home-assistant-temperature-monitor were both using the Home Assistant REST API to fetch data. That’s 15k requests/day with only two of my seven other Raspberry Pis turned on, so it would get worse when I put some of the other Pis into “production” around the house. The temperature project only runs on one box, but the first project in installed on every Pi.

I briefly tried to switch those two projects over to using the local IP address of the server and continue to use the REST API, but with SSL enabled it was complicated. MQTT was already running on the server and those projects publish updates to Home Assistant over MQTT, so it was an easy decision to use it for subscribing as well.

While I was at it, I took the opportunity to simplify a lot of the code and have the devices update more frequently. So far it seems to have solved a couple of lingering stability or connection issues I was having with home-assistant-pi. All of the code changes are available in the respective GitHub repos linked earlier.

Update: I forgot I had home-assistant-pi connecting to google.com in to help with determining the local IP address in Python. Another update to clear that up will eliminate over five thousand DNS requests per day.