How to overengineer a vacuum cleaner
I own an Eufy Robovac 11s. This is an IR-controlled vacuum cleaner, and I have been commanding it via Home Assistant and a Broadlink IR-remote since as long as I can remember.
The automation was relatively simple:
- alias: When nobody is home, start Dobby
description: ''
trigger:
- platform: state
entity_id: group.family
to: not_home
for:
minutes: 5
- platform: time
at:
- '8:00:00'
- '12:00:00'
- '16:00:00'
- '20:00:00'
condition:
- condition: state
entity_id: group.family
state: not_home
for:
minutes: 5
- condition: time
after: '7:00'
before: '20:00'
- condition: state
entity_id: switch.dobby
state: off
for:
hours: 11
action:
- service: switch.turn_on
target:
entity_id: switch.dobby
- service: notify.notify
data:
title: Dobby
message: Stof zuigen zal ik doen!
mode: single
Basically, Dobby cleaned the appartement at most once a day, whenever nobody was there. This idea has a few drawbacks:
- During the weekends, I would often get to the shop for 10 minutes and return immediately. If I would leave again for a longer period the same day, Dobby would have only cleaned for five or ten minutes.
- Dobby would clean every day, even when on a vacation.
- Maybe I would like Dobby to clean during certain periods when I’m home, too. For example, Dobby could go around while I’m in the shower.
Simulating dust and batteries
At this point, it is clear that Dobby should become smarter. Remember, Dobby is an Eufy 11s, which has no connectivity whatsoever. This implies that Dobby is controlled 100% open-loop: Home Assistant can send a start command via IR, and a stop-and-go-home command, but I have no way of obtaining any information about Dobby’s state. At a certain point, I will probably slap an ESP32 with ESPHome on it to have some feedback about battery and cleaning, but currently, Dobby cannot talk to me.
In order to have an idea about the battery charge status,
I opted to build a simulator for it.
The dobby_charge_rate
is a function of whether Dobby is home or cleaning,
and the dobby_charge
is the integral of that function.
Note in the code below that I added a small random term for the integration to actually trigger.
Suggestions for (ahum) cleaning that up are welcome.
template:
- trigger:
- platform: time_pattern
minutes: "/2"
sensor:
- name: Dobby charge rate
unique_id: dobby_charge_rate
unit_of_measurement: "%/h"
# Home: 25%/h
# Running: 75%/h
state: >-
{% set dobby = is_state('switch.dobby', 'on') | int %}
{% set dobby_not_full = states('sensor.dobby_battery_charge') == "unknown" or (states('sensor.dobby_battery_charge') | int <= 100) %}
{% set dobby_not_empty = states('sensor.dobby_battery_charge') == "unknown" or (states('sensor.dobby_battery_charge') | int >= 0) %}
{{ -75 * dobby * (dobby_not_empty | int) + 25 * (1 - dobby) * (dobby_not_full | int) + (range(-9, 10) | random)/100 }}
sensor:
- platform: integration
source: sensor.dobby_charge_rate
unit_time: h
name: Dobby battery charge
unique_id: dobby_charge
Additionally, I made a dust_accumulation_rate
and accumulated_dust
, in the same fashion.
This is based on the number of people that are present at home.
The constants are based on the idea that Dobby should clean every four days in vacation mode, but again every day when I’m actually home for any significant time.
template:
- trigger:
- platform: time_pattern
minutes: "/2"
sensor:
- name: Dust accumulation rate
unique_id: dust_accumulation_rate
unit_of_measurement: "%/h"
# .5%/hour base rate = 50% every four days
# 10 hours * one person = 90% dust
# 1 hour * one dobby = -100% dust
state: >-
{% set dobby = is_state('switch.dobby', 'on') | int %}
{% set not_max_dust = states('sensor.accumulated_dust') == "unknown" or (states('sensor.accumulated_dust') | int <= 100) %}
{% set not_min_dust = states('sensor.accumulated_dust') == "unknown" or (states('sensor.accumulated_dust') | int >= 0) %}
{{ (((states('zone.home') | int) * 9 + 0.5 * (1 - dobby)) * not_max_dust - (100 * dobby) * not_min_dust) + (range(-9, 10) | random)/100 }}
sensor:
- platform: integration
source: sensor.dust_accumulation_rate
unit_time: h
name: Accumulated dust
unique_id: accumulated_dust
Based on these two properties, I changed the automation to trigger at 99% battery and 75% dust, still checking the presence status of inhabitants and guests.
Taking it a step further: shower detection
Now onto the most silly part: I would like Dobby to clean while I’m home alone and showering. During that time, Dobby would not bother anybody, and can freely roam about for about 15 minutes. This is especially useful if I don’t plan to be away for a long time, and I would have guests over the same day.
My first attempt at detecting shower presence was based on the bathroom humidity sensor. And so was my second and third attempt. I’m not sure which version this one is, but this is the last I tried:
sensor:
- platform: statistics
name: Bathroom Humidity Stats
entity_id: sensor.xiaomith2_humidity
state_characteristic: mean
sampling_size: 40
max_age:
hours: 1
binary_sensor:
- platform: template
sensors:
showering:
value_template: "{{ states('sensor.xiaomith2_humidity') | int > states('sensor.bathroom_humidity_stats') | int + 5 and states('sensor.bathroom_humidity_stats') != 'unknown' }}"
friendly_name: Showering
device_class: occupancy
Basically: if the humidity is over 5% higher than the hourly mean, I consider the shower to be occupied.
This worked, more or less. Dobby would start cleaning by the time I got out of the shower, and then probably started and stopped a few times more while I was in the living room.
Finding those exact offsets that work is annoying and unpleasent, and this process finally got me to touch machine learning. Over the course of today’s afternoon, I wrote a little KNN inference system that talks to Home Assistant, and bases its features and samples of Home Assistant’s logbook. To be more precice: Hasli keeps track of a list of entities, and runs inference when the tracked entities change their state. Hasli will then update its own entities in Home Assistant, for further use in automations.
Currently, a configuration for Hasli looks like this:
binary:
- name: "showering"
input_entities:
- name: "bathroom humidity"
entity_id: "sensor.xiaomith2_humidity"
- name: "bathroom temperature"
entity_id: "sensor.xiaomith2_temperature"
- name: "bedroom humidity"
entity_id: "sensor.slaapkamer_smart_air_humidity"
- name: "bedroom temperature"
entity_id: "sensor.slaapkamer_smart_air_temperature"
- name: "people counter"
entity_id: "zone.home"
- name: "bedside presence 3"
entity_id: binary_sensor.lidlpresence3_occupancy
- name: "toilet presence"
entity_id: binary_sensor.lidlpresence1_occupancy
- name: "bedside presence 2"
entity_id: binary_sensor.lidlpresence2_occupancy
- name: "living humidity"
entity_id: sensor.living_smart_air_humidity
- name: "living temperature"
entity_id: sensor.living_smart_air_temperature
Expanding Hasli
Currently, I have a local main.rs
that is tailored towards my own “showering” inference.
My hope is that Dobby will clean tomorrow morning while I’m in the shower.
If that works, the goal is to make Hasli a proper daemon, and integrate it with the Home Assistant UI to allow everyone to design inference systems based on their sensors, and annotate the required data through the Home Assistant UI.
Any help to get data annotation integrated in Home Assistant would be very welcome. I envision a UI element where you can annotate a timeline of your own sensor data, not unlike the History Explorer Card by alexarch21.
Hasli should also expose a service to Home Assistent, such that data annotation can be automated!