Tooling

Tooling

This tooling is still a work in progress, and ideally I would like to build a sensible CI/CD build system that would compile the configs into programs and upload them to the ESP devices. The esph.py tool will eventually (and hopefully) move into its own project under homie along with (most of) the configuration, with the lofty goal of making that possible.

esph.py

Since I prefer to mange configuration in code whenever possible, I am not using the dashboard to manage ESPHome, instead preferring managing the configs with ansible and and my esph.py tool.

$ esph.py clean device_kind.yaml
$ esph.py compile device_kind.yml --name livingroom
$ esph.py upload device_kind.yml --name livingroom

esph.py --help

$ esph.py --help
usage: esph.py [-h] [-q] [-v] [-R] [-l] [-u] [-s key value] [-d DOMAIN] [-n NAME] [-D DEVICE] [-N]
               ACTION ... config

positional arguments:
  ACTION                Command to pass to ESPHome.
    clean               Delete all temporary build files and program binary.
    config              Validate and print full YAML config.
    compile             Read the config, validate it, compile a program and store a binary in target path.
    upload              Validate the current config and upload the latest binary in target path.
    logs                Connect to ESPHome device and print logs to stdout.
    run                 Validate, compile and upload the config.
    version             Print ESPHome version to stdout.
    esphome-help        Print ESPHome --help output.
    substitutions       Show substitutions.
  config                ESPHome config file.

options:
  -h, --help            show this help message and exit
  -q, --quiet           Quiet output. (default: False)
  -v, --verbose         Verbose output. (default: False)
  -R, --dry-run         Only log what would be run, without running it. (default: False)
  -l, --show-log        Print logs from ESPHome device to stdout. (default: False)
  -u, --user            Run as 'ben', otherwise sudo's to 'hass'. (default: False)
  -s key value, --substitution key value
                        ESPHome substitutions (default: None)
  -d DOMAIN, --domain DOMAIN
                        Domain for hostname. (default: s21.sudo.is)
  -n NAME, --name NAME  Device name. Hostname becomes 'esphome-${name}' (default: None)
  -D DEVICE, --device DEVICE
                        Device name to upload program to. Can be device file (example: /dev/ttyUSB0) or hostname
                        (can be used when renaming device). (default: None)
  -N, --no-device       Ignore found device files and computed hostnames, and do not attempt to write to them.
                        (default: False)

In order to minimize the number of config files and use my naming scheme, I've written a wrapper esph.py that wraps the esphome script (suportig the useful esphome commands).

Substitutions are used to set the hostname and to provide variables to set name attributes (which get used for the entity_id in Home Assistant).

$ esph.py substitutions radar.yaml -s "docs" "foobar"
{
  "lower_node_name": "radar",
  "friendly_node_name": "Radar",
  "device_type_name": "radar",
  "hostname": "esphome-radar",
  "domain": ".home.sudo.is",
  "config_name": "radar.yaml",
  "config_date": "2023-12-30",
  "build_path": "/srv/hass/esphome/target/radar",
  "docs": "foobar"
}

Each individual device does not need its own config file, instead each "kind" of ESPHome device has a config file and then each device of that kind is targeted by passing esph.py the --name argument.

$ esph.py run radar.yml --name livingroom

...

Uploading to: esphome-radar-livingroom.home.sudo.is

There are multiple radar "kind" devices, and each device is named esphome-${kind}-${area} (or if there is more than in in each ${area} it gets expanded to esphome-${kind}-${area}-${placement} instead).

Packages

I use "packages" to i re-use and organize my ESPHome configuration files. For example, the config for the "device kind" radar looks something like this:

packages:
  board: !include ../packages/boards/esp32s2mini.yaml
  esphome: !include ../packages/common/esphome.yaml
  network: !include ../packages/common/network.yaml
  http_server: !include ../packages/common/http_server.yaml
  sensors_esp32: !include ../packages/sensors/esp32.yaml
  sensors_esphome: !include ../packages/sensors/esphome.yaml
  sensors_wifi: !include ../packages/sensors/wifi.yaml

This example is radar.yaml on an ESP32 S2 Mini board with an LD2410 mmWave presence detection sensor1.

Working with entity_id in Home Assistant

By default, ESPHome wants you to set esphome.friendly_name, which then gets prefixed to all names and entity_ids in Home Assistant. This is a problem for me, since I want some entity_ids to not start with a "friendly name".

For example, using the default suggested naming standard and config and a minimal example with the Version Text Sensor2:

esphome:
    name: livingroom
    # This will get lowercased, and prefixed to any name attributes to
    # form the entity_id in Home Assistant
    friendly_name: "Livingroom"

wifi:
    # This is the default value
    domain: ".local"

mdns:
    # This is the default value
    disabled: false


text_sensor:
    - platform: version
      # This will get suffixed to the friendly_name
      name: "ESPHome Version"

This results in the following naming:

  • Hostname: livingroom.local
  • Version Text Sensor entity_id: sensor.livingroom_esphome_version

This is unsuitable for me for numerous reasons.

  1. I do not want to use .local or rely on mDNS. My ESPHome devices are segregated on a separate VLAN, and I dislike relying on auto-discovery in general (preferring to manage configuration in code)
  2. I want my hostnames to be prefixed with esphome-, and to have the fully qualified name that my DHCP server hands out (this can be fixed by simply setting esphome.name correctly).
  3. I don't want everything to be prefixed with the "friendly name". For the Version Text Sensor, I want it to use sensor.esphome_version_livingroom as entity_id in Home Assistant.

This is a small example, but this quickly gets out of hand. So instead, the config equivalent config that I use looks something like this:

esphome:
      name: ${hostname}

wifi:
    domain: ".home.sudo.is"

mdns:
    disabled: true

text_sensor:
    - platform: version
      name: "ESPHome version ${lower_node_name}"

This results in the following naming:

  • Hostname: esphome-livingroom.home.s21.sudo.is
  • Version Text Sensor entity_id: sensor.esphome_version_livingroom

The variables/substitutions ${hostname} and ${lower_node_name} are stitched together by esph.py using the config filename and --name parameter.

References

3

Configuration types - ESPHome. Details on id, pin schema, YAML operators/substitutions, packages ad etc.