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!