Sharing enlightening moments of home automation

Smarter Swimming Pool 5: Under the Surface

My ESP8266 based device that is measuring water temperature, water level and liquid chlorine level is happily running for quite some time now.

However, the eco-system around Home Assistant and ESP has evolved, and I just wanted to quickly share how that has changed the setup of my smart swimming pool.


A great new project has emerged called ESPHome which vastly simplifies the setup and integration of ESP based devices into Home Assistant. The main selling points for me for migrating from ESPEasy to ESPHome are:

  • File based configuration of the device which makes backing up or versioning the configuration so much easier. No more need for fiddling around in a web based user interface.
  • Super simple OTA (over the air) updates of software and configuration.
  • Wide range built-in support for devices and sensors for ESP32 and ESP8266.
  • Great extension points with generic device support, template sensors, custom code, simple automations.
  • Made for Home Assistant which means that the sensors just appear without any further configuration required.
  • Its native API and UI support makes the integration of new devices a very seamless experience.
  • Better device monitoring within Home Assistant – built-in uptime sensor, connection status sensor, version sensor, restart switch.

Device configuration

Without making any hardware modifications to the device I built and described in the previous posts, I developed the following ESPHome configuration to get the same (or better) outcome in Home Assistant.

As you can see, I stored all my passwords in a separate file and only reference the password by key in here. You will have to find out your Dallas sensor’s ID; it is automatically shown after uploading the firmware in the start-up log, and you can copy and paste the ID (format: 0x1234567890123456) into your configuration and upload the firmware again.

  name: esp_01_pool
  platform: ESP8266
  board: d1_mini_pro

  ssid: <insert your Wifi SSID here>
  password: !secret wifi_password

  password: !secret api_password


  password: !secret ota_password

  port: 80

  - pin: D7

  - platform: restart
    name: "ESP 01 Pool Restart"

  - platform: status
    name: "ESP 01 Pool Status"
  # Water Level
  - platform: gpio
    pin: D2
    name: "ESP 01 Pool Water Level Low"
    device_class: problem
  - platform: gpio
    pin: D1
    name: "ESP 01 Pool Water Level Critical"
    device_class: problem

  - platform: uptime
    name: "ESP 01 Pool Uptime"
  # Water Temperature
  - platform: dallas
    address: <insert your sensor's ID here>
    name: "ESP 01 Pool Water Temperature"
  # Liquid Chlorine Level
  - platform: ultrasonic
    trigger_pin: D5
    echo_pin: D6
    name: "ESP 01 Pool Liquid Chlorine Distance Level"

  - platform: version
    name: "ESP 01 Pool Version"

Uploading firmware

As a prerequisite for turning the above configuration into a firmware binary you will have to set up a development environment – from the command line or from within If you are, like me, coming from a device currently running ESPEasy, please follow this simple guide for the initial upload.

Updated Home Assistant configuration

Entity IDs generated by the ESPHome integration are of course different to what my ESPEasy based entity IDs looked like, so all template sensors and customisations required some small changes. Also, ESPHome’s ultrasonic sensor integration measures the distance in metres by default, I had to tweak the way I calculate the liquid chlorine level in percent a bit.

Water Level

In the following configuration snippet I really only had to change the entity IDs in the value_templates to make this work again.

  - platform: template
        value_template: "{{ is_state('binary_sensor.esp_01_pool_water_level_low', 'on') }}"
          minutes: 5
          minutes: 5
        friendly_name: "Water Level Low"
        device_class: safety
        value_template: "{{ is_state('binary_sensor.esp_01_pool_water_level_critical', 'on') }}"
          minutes: 5
          minutes: 5
        friendly_name: "Water Level Critical"
        device_class: safety

Liquid Chlorine level

In the following I had to change the entity_id of the statistics sensor. The template sensor is now working in metres (hence the change from 60cm to 0.6m – the height of my drum), and I am dealing with unknown values of the statistics sensor which caused the chlorine level to spike to 100% each time Home Assistant was restarted.

  - platform: statistics
    entity_id: sensor.esp_01_pool_liquid_chlorine_distance_level
    name: Pool Liquid Chlorine Level
    sampling_size: 180
  - platform: template
        value_template: >
          {% if (states('sensor.pool_liquid_chlorine_level_mean') != 'unknown') %}
            {% if (states.sensor.pool_liquid_chlorine_level_mean.state | float >= 0.6) %}
            {% else %}
              {{ ((0.6 - states.sensor.pool_liquid_chlorine_level_mean.state | float) | float / 0.6 * 100) | round(1) }}
            {% endif %}
          {% else %}
          {% endif %}
        unit_of_measurement: '%'
        friendly_name: 'Liquid Chlorine Level'

In addition to the above, all my MQTT sensor configurations related to this device could now go, and I manually removed the retained MQTT messages from my broker.

ESPHome specific configuration

ESPHome comes with an uptime sensor that provides the uptime in seconds. I found it more useful to translate that into a more human-readable form, and added the following (adopted from the HA community forum):

  - platform: template
        friendly_name: "ESP 01 Pool Uptime"
        value_template: >-
          {% set uptime = states.sensor.esp_01_pool_uptime.state | int %}
          {% set days = (uptime / 86400) | int %}
          {%- if days > 0 -%}
            {{ days }} days, {{ (uptime - (days * 86400)) | int | timestamp_custom('%H:%M:%S', false) }}
          {%- else -%}
            {{ uptime | int | timestamp_custom('%H:%M:%S', false) }}
          {%- endif -%}


I had been using HADashboard for some time, but recently decided to switch to TileBoard. This solution does not require a server application , but instead is purely based on JavaScript and can be served by the web server built into Home Assistant. There is already a wide variety of components included out-of-the-box, and can easily be customised and extended through JavaScript and CSS.


I decided to adapt a modular approach for configuring TileBoard where I split the components into manageable portions and which is based around reusable tiles in the form of JavaScript functions.

In the following I am showing you the relevant tiles and other configuration elements relevant to my pool setup.


The advantage of defining each tile as a function is that you can reuse a tile on multiple screens. The x-y-coordinates are passed in as parameters, i.e. I can decide where to show each tile at the place where it is used.

// Pool Water Temperature Sensor
var tile_sensor_pool_water_temperature = function(x, y) {
  return {
    position: [x, y],
    type: TYPES.SENSOR,
    id: 'sensor.esp_01_pool_water_temperature',
    title: 'Water Temperature',
    state: false,

// Pool Pump Sensor
var tile_sensor_pool_pump = function(x, y) {
  return {
    position: [x, y],
    id: 'binary_sensor.pool_pump_running',
    title: 'Pool Pump',
    states: { on: 'running', off: 'off' },
    icons: { on: 'mdi-engine', off: 'mdi-engine' }

// Pool Pump Running Today Sensor
var tile_sensor_pool_pump_history = function(x, y) {
  return {
    position: [x, y],
    type: TYPES.SENSOR,
    id: 'sensor.pool_pump_running_today',
    title: 'Today',
    subtitle: 'Pool Pump',
    state: false,
    classes: ["-value-font-size-medium"],

// Pool Water Level Sensor
var tile_sensor_pool_water_level = function(x, y) {
  return {
    position: [x, y],
    type: TYPES.SENSOR,
    id: 'sensor.pool_water_level',
    title: 'Water Level',
    subtitle: 'Pool',
    state: false,
    classes: ["-value-font-size-medium"],

// Pool Liquid Chlorine Level Sensor
var tile_sensor_pool_liquid_chlorine_level = function(x, y) {
  return {
    position: [x, y],
    type: TYPES.SENSOR,
    id: 'sensor.pool_liquid_chlorine_level',
    title: 'Liquid Chlorine',
    subtitle: 'Pool',
    state: false,
    filter: function (value) { return valueToFixed(value, 0); }

// Pool Swimming Season
var tile_input_boolean_pool_swimming_season = function(x, y) {
  return {
    position: [x, y],
    id: 'input_boolean.swimming_season',
    title: 'Swimming Season',
    subtitle: 'Pool',
    icons: { on: "mdi-swim", off: "mdi-swim" },

// Pool Pump Mode Input Select
var tile_input_select_pool_pump_mode = function(x, y) {
  return {
    position: [x, y],
    id: 'input_select.pool_pump',
    title: 'Mode',
    subtitle: 'Pool Pump',
    state: false,


The following JavaScript function is a reusable filter that I am using in the tiles to round numbers to save some space on the screen.

// Format provided value to the have number of decimal digits.
function valueToFixed(value, digits) {
  var num = parseFloat(value);
  return num && !isNaN(num) ? num.toFixed(digits) : value;

Custom CSS

For the sake of completeness, here the CSS styles I have used in the tiles above. Of course, depending on the style and layout you use in TileBoard, you may need to adapt the actual sizes and distances, etc.

.-value-font-size-medium .item-entity--value {
  font-size: 24px;
  white-space: normal;
  padding-top: 5px;
.-value-font-size-medium .item-entity {
  line-height: normal;


The following shows an example page where all the tiles defined above are displayed in one row.

var page_outside = {
  title: 'Outside',
  icon: 'mdi-flower',
  groups: [
      width: 7,
      height: 5,
      items: [
        <...other tiles...>
        tile_sensor_pool_water_temperature(0, 3),
        tile_sensor_pool_pump(1, 3),
        tile_sensor_pool_pump_history(2, 3),
        tile_input_select_pool_pump_mode(3, 3),
        tile_sensor_pool_water_level(4, 3),
        tile_sensor_pool_liquid_chlorine_level(5, 3),
        tile_input_boolean_pool_swimming_season(6, 3),

Main configuration

The following just demonstrates how the page defined above is included in the main configuration.

var CONFIG = {
   pages: [
      <...other pages...>

Serving TileBoard from Home Assistant’s webserver

The easiest way to serve the TileBoard files is from Home Assistant’s own webserver. To do that create folders <configuration folder>/www/tileboard and copy all files and folders including your customisations into that new folder. After a restart of Home Assistant you can access the TileBoard dashboard – depending on your own setup – under http://<server name>:8123/local/tileboard/index.html.

How the above looks like in the UI

TileBoard pool tiles

Development approach and how to keep in sync

TileBoard is not a packaged application, so I came up with the following configuration and customisation approach to keep my own code manageable while not losing the ability to update to the latest code base.

Initial setup

  • Clone the TileBoard GitHub repository onto the local development machine.
  • Configure this remote repository as upstream.
  • Create a private Git repository (because you may want to store a long-lived access token in the config file), and configure this as origin.
  • Push current state to origin.
  • Create a branch, e.g. custom-home that will contain all your customisations. Keep the original code on the master.

Configuration and customisations

  • Keep changes to the original code to a minimum. I only had to add links to my own custom JavaScript and CSS into the index.html file.
  • Test any changes locally.
  • Copy changed files to your Home Assistant server.
  • Commit onto your custom-homebranch and push to your own repository.

Catching up

  • Fetch updates from upstream repository, and update your master branch:
    • git fetch upstream
    • git checkout master
    • git reset --hard upstream/master
    • git push origin master --force
  • Apply all your customisations
    • git checkout custom-home
    • git rebase origin/master
    • The above may fail, requiring some manual merging.
    • git push origin custom-home --force


Apart from the various small improvements to my existing setup like the ones described above, I am still looking at integrating a turbidity sensor. It turned out that my first turbidity sensor was broken and hence did not work as expected at all – waiting for a replacement now.


At the time of writing this post, I used:

  • Home Assistant 0.89.1 with Python 3.6.3
  • ESPHome v1.11.2
  • Wemos D1 mini Pro

Smarter Swimming Pool Series

  1. Pool Pump
  2. Water Temperature
  3. Water Level
  4. Liquid Chlorine Level
  5. Improvements Under the Surface
  6. Chlorinator Refurbishment
  7. Pool Gate


2 responses to “Smarter Swimming Pool 5: Under the Surface”

  1. Sonny Avatar


    You mentioned that you’d use a turbidity sensor once you got hold of one in your setup – did you ever get around to doing that? I’m tying to add a turbidity sensor and am not sure how to about it.


    1. malte Avatar

      Hi Sonny, yes, I do have a turbidity sensor and it does work in principle. The sensor comes with a little board that provides an analogue output. I then tried to compute the NTU value from that analogue value measured. I could easily produce values – 0 NTU with clear water and 3000 NTU with lots of coffee ground in the water. However, so far I couldn’t get suitable readings with pool water that was just a little bit green-ish, i.e. just around the time when I would like to get an alert that something is wrong with the water. I’ll keep testing the sensor in my lab, and let you know when I have a working configuration.

Leave a Reply

Your email address will not be published. Required fields are marked *