Home Assistant Matrix Display for Indicators

Where I live we have three types of waste collection. One week we get our refuse collected and the next week we get our recycling collected. For about eight or nine months of the year we also get garden waste collected along with the recycling. I have linked up my Home Assistant instance so that every week, the night before collection I get a notification on my mobile phone as to what is being collected the next day. It has worked great, but I often found myself having to open the Home Assistant app on my phone and find what the collection was going to be ahead of time. As a result I decided I would like to have some sort of visual indicator in my kitchen to let me know what collection was due next. In this post I am going to set out my little journey and what I ended up with.

Background

I was delighted last year when I discovered the community integration called Waste Collection Schedule. This is an integration you can add to your Home Assistant instance via HACS, the Home Assistant Community Store. This allowed me to surface my local waste collection services (after I appropriately configured the integration) as an entity in Home Assistant, and specifically access a sensor definition for what the next waste collection will be.

I figured that having some sort of simple LED based indicator in my kitchen next to the back door would be ideal as that is where we have our kitchen bin and recycling.

Hardware

I have a couple of information display on my desk that I have written about in the past, that are based on the LILYGO® T-Display-S3 ESP32-S3. A desk clock, and an information display. However these are more suited to graphics and text. They would be overkill for what I was looking for. Really all I was looking for was an LED to light up.

An ESP32/ESPHome-based solution seemed logical as I could reuse a lot of the principles of the information display to change the output based on a helper entity on the Home Assistant side that I could set based on the output of the waste collection sensor.

I have some spare ESP32s available, and some LEDs, wiring those up, 3D printing a case from my rather poor 3D printer I decided was something I could address by spending a little bit of money to get something a little bit nicer.

I have been really impressed by the M5Stack ATOM series of ESP32 devices. They are modular, nicely presented and offer a range of functionality in a tiny package. One of the options they have is the M5Stack ATOM Matrix ESP32.

Of course this has more than just one RGB LED so this inexpensive hardware actually opened up some additional options for me. I could display more than one piece of information (by separating the display into zones) or more complex information (e.g. a simple icon/character). It would also give me a chance to work with and learn a bit more about a different type of display, so this was the option I went for rather than the barbaric 3D-printed monstrosity I would no doubt have otherwise ended up with.

Note, you don't have to buy these from the M%Stack shop. They have stockists around the world, including in the UK where I live.

Zoning

I decided in the end that there were maybe some other useful things I could give an indication of. We tend to use our kitchen door for entering and leaving the house, so I figured if I could give a glanceable indication of the main weather condition and temperature, that would be useful too. For that reason, the approach I decided to adopt was to split the 25 x 25 matrix of RGB LEDs into five zones. These are four quadrants and a grid (consisting of a simple central cross).

Each zone could represent different information, and different colours could then convey a different meaning, with multiple sequential LEDs making things easy to distinguish at a distance. In fact the cross I have chosen to typically leave unlit to really let the quadrants show distinctly, but if I find a need to convey something via the cross, my set up allows for that.

ESP32 Device Configuration

As noted above, the intention from the start was to use the Home Assistant ESPHome integration to drive this project from the next waste collection sensor. I also have local weather information, so that can be used to drive the logic to display in a couple of the quadrants.

Quadrant:

  1. Weather Condition
  2. Temperature
  3. Next Waste Collection
  4. As yet undefined

For each quadrant, you will see that I have things set up to display one of a particular set of colours. This is based on a very similar approach to setting the text colour of outputs on my information display. However, for the grid, while this will typically be off (LED displaying ‘black’), I actually decided to make this more flexible and allow any colour to be specified. This allowed me to test different colours to see if there was enough difference when displayed via the LEDs to let me distinguish the colour from others I was making available, and it also means I have the code available for amending this or creating future projects where I want more fine level control on setting the colour of the LED from Home Assistant rather than just choosing from presets.

As you might expect, I already had the ESPHome integration installed in Home Assistant, but should you want to set it up yourself, ESPHome have a wonderful getting started guide.

I carried out the base set up on the ATOM Matrix and then set about putting together my ESPHome YAML configuration. Of course this took several iterations and lots of testing and research to get working as I wanted, but a large proportion of this was based on other ESP configuration I had completed - so if you have read through some of my other posts on this already, you may note there is a good chunk of duplication in what I am running through here. Particularly around the more standard aspects.

Substitutions

For ease of reuse, I do endeavour to place substitutions at the top of my ESP32 configurations. The substitution definitions allow me to specify data once and reuse it via a template elsewhere in the configuration.

# Specify substitutions to allow easy name re-use
substitutions:
  device_name: espinfo
  friendly_name: ESPINFO

ESPHome

The ESPHome configuration specifies the name and a project reference for my configuration. The names are of course based on the earlier substitutions.

# Main config begins
esphome:
  name: ${device_name}
  friendly_name: ${friendly_name}
  project:
    name: "sylumer.espinfo"
    version: "1.0.0"

ESP32 Board

This section defines the device being used to Home Assistant’s ESPHome integration.

# ESP32 definition
esp32:
  board: m5stack-atom
  framework:
    type: arduino

Logging

We want logging enabled so we can see feedback in ESPHome’s logging functionality for what is going on. In this instance we don’t have any overly noisy aspects to rule out like I had to for the other display projects I’ve used so far, so just enabling debug-level logging is sufficient.

# Enable logging
logger:
  level: DEBUG

WiFi Connectivity

The substitutions once again come into play as I use my standard set of content for my WiFi connectivity definitions.

# Over the air
ota:
  password: !secret ota_password

# Details of default Wifi and fallback hotspot
wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: ${device_name} Fallback Hotspot
    password: !secret ap_password

# No captive portal defined
captive_portal:

Note that I am using secrets to store the more sensitive common information. These are held in my /conf/esphome/secrets.yaml file on my Home Assistant server. This works the same way the secrets.yaml file does for standard Home Assistant configuration does.

To support the above, an example secrets.yaml file might look something like this.

# Your Wi-Fi SSID and password
wifi_ssid: "HomeNetwork"
wifi_password: "67TG^*Vh_0"

# Home Assistant
ota_password: 3fff3ffe000000a11111f00f5e0fffaa"

# Fallback hotspot
ap_password: "E-S_p.32-WIFIpwd"

Home Assistant API

The ATOM Matrix needs to be able to read data from Home Assistant about each of its zones in the same sort of way the information display would read information to display in its sections.

# Enable Home Assistant API
api:
  encryption:
    key: !secret api_encryption_key

Once again I am making use of another secret in the secrets.yaml file for specifying an encryption key to use for communicating with the Home Assistant API.

Sensors

There are quite a few sensors I have defined for the ATOM Matrix, though it actually has quite a few more available to it if you check the device component specifications.

Sensor ID Locale Purpose
Generic wifi_value Device Signal strength for the currently connected WiFi network
Binary mainbutton Device The matrix rests atop a switch which can be used as a trigger
Text info_quad1_colour Home Assistant The name of the colour for the first quadrant of the matrix
Text info_quad2_colour Home Assistant The name of the colour for the second quadrant of the matrix
Text info_quad3_colour Home Assistant The name of the colour for the third quadrant of the matrix
Text info_quad4_colour Home Assistant The name of the colour for the fourth quadrant of the matrix
Text info_grid Home Assistant The hex colour for the grid zone of the matrix
Text host_ip Device The IP address assigned to the device

These are defined by the following configuration.

# Generic Sensors
sensor:
  - platform: wifi_signal
    id: wifi_value
    name: ${device_name} WiFi Signal
    icon: mdi:icon-strength-2

# Binary Sensors
binary_sensor:
  - platform: gpio
    name: "Main Button"
    id: mainbutton
    pin:
      number: 39
      inverted: true

# Text Sensors
text_sensor:
  # LED Quadrants
  - platform: homeassistant
    id: info_quad1_colour
    entity_id: input_select.info_quad1_colour
  - platform: homeassistant
    id: info_quad2_colour
    entity_id: input_select.info_quad2_colour
  - platform: homeassistant
    id: info_quad3_colour
    entity_id: input_select.info_quad3_colour
  - platform: homeassistant
    id: info_quad4_colour
    entity_id: input_select.info_quad4_colour
  # LED grid
  - platform: homeassistant
    id: info_grid
    entity_id: input_text.info_grid
  # WiFi IP Address
  - platform: wifi_info
    ip_address:
      name: ${device_name} IP Address
      id: host_ip
      entity_category: diagnostic
      icon: mdi:router-wireless

Lights

The RGB LED matrix is really a strip of LED lights that are laid out so as to form a grid. But in terms of how they are defined and addressed at the most basic level, they are a single “bus” of addressable (neopixel) RGB LEDs.

Note that neopixels are very bright and running them at a setting lower than 100% brightness is strongly recommended. For my environment a 30% brightness is sufficient to be visible when the room is lit and not overpowering when the room is dark or dim. This has been done for each colour channel via the colour_correct configuration variable.

# Lights
light:
  # LED Matrix as a strip
  - platform: neopixelbus
    type: GRB
    variant: WS2812x
    pin: 27
    num_leds: 25
    id: led_matrix_light
    color_correct: [30%, 30%, 30%]
    restore_mode: ALWAYS_OFF

Matrix Display

With the light strip for the matrix of LEDs defined, we now need to do some configuration to let ESPHome know that the light strip is a 5 x 5 addressable matrix of LEDS. The configuration to do this is very simple, but yet this section is the longest and most complex. Why? Well this section also includes a ‘lambda’, a C++ construction for an anonymous function. So this is certainly where things have the potential to step up a gear in technical terms. However, I’ve added lots of comments and I don’t think the lambda is too hard to follow, but I have added some additional sections below the code to explain what the lambda does.

# Displays
display:
  # LED Matrix as an addressable grid
  - platform: addressable_light
    id: led_matrix_display
    addressable_light_id: led_matrix_light
    width: 5
    height: 5
    rotation: 0
    update_interval: 16ms
    lambda: |-
      // Define available colours
      Color Red = Color(0xFF0000);
      Color Green = Color(0x00FF00);
      Color Blue = Color(0x0000FF);
      Color White = Color(0xFFFFFF);
      Color Black = Color(0x000000);
      Color Orange = Color(0xFFA500);
      Color Yellow = Color(0xFFFF00);
      Color Magenta = Color(0xFF00FF);
      Color Cyan = Color(0x00FFFF);
      Color Silver = Color(0xC0C0C0);

      // Get the hexadecimal colour string from info_grid
      std::string gridColorStr = id(info_grid).state.c_str();

      // Convert the hexadecimal string to a 'Color' object
      uint32_t gridColorValue;
      sscanf(gridColorStr.c_str(), "%x", &gridColorValue);

      // Define the colour 'Grid'
      Color Grid = Color(gridColorValue);

      // Set colour for quad 1
      if(id(info_quad1_colour).state == "Red") {it.rectangle(0, 0, 2, 2, Red);}
      if(id(info_quad1_colour).state == "Green") {it.rectangle(0, 0, 2, 2, Green);}
      if(id(info_quad1_colour).state == "Blue") {it.rectangle(0, 0, 2, 2, Blue);}
      if(id(info_quad1_colour).state == "White") {it.rectangle(0, 0, 2, 2, White);}
      if(id(info_quad1_colour).state == "Black") {it.rectangle(0, 0, 2, 2, Black);}
      if(id(info_quad1_colour).state == "Orange") {it.rectangle(0, 0, 2, 2, Orange);}
      if(id(info_quad1_colour).state == "Yellow") {it.rectangle(0, 0, 2, 2, Yellow);}
      if(id(info_quad1_colour).state == "Purple") {it.rectangle(0, 0, 2, 2, Magenta);}
      if(id(info_quad1_colour).state == "Cyan") {it.rectangle(0, 0, 2, 2, Cyan);}
      if(id(info_quad1_colour).state == "Grey") {it.rectangle(0, 0, 2, 2, Silver);}

      // Set colour for quad 2
      if(id(info_quad2_colour).state == "Red") {it.rectangle(3, 0, 2, 2, Red);}
      if(id(info_quad2_colour).state == "Green") {it.rectangle(3, 0, 2, 2, Green);}
      if(id(info_quad2_colour).state == "Blue") {it.rectangle(3, 0, 2, 2, Blue);}
      if(id(info_quad2_colour).state == "White") {it.rectangle(3, 0, 2, 2, White);}
      if(id(info_quad2_colour).state == "Black") {it.rectangle(3, 0, 2, 2, Black);}
      if(id(info_quad2_colour).state == "Orange") {it.rectangle(3, 0, 2, 2, Orange);}
      if(id(info_quad2_colour).state == "Yellow") {it.rectangle(3, 0, 2, 2, Yellow);}
      if(id(info_quad2_colour).state == "Purple") {it.rectangle(3, 0, 2, 2, Magenta);}
      if(id(info_quad2_colour).state == "Cyan") {it.rectangle(3, 0, 2, 2, Cyan);}
      if(id(info_quad2_colour).state == "Grey") {it.rectangle(3, 0, 2, 2, Silver);}

      // Set colour for quad 3
      if(id(info_quad3_colour).state == "Red") {it.rectangle(0, 3, 2, 2, Red);}
      if(id(info_quad3_colour).state == "Green") {it.rectangle(0, 3, 2, 2, Green);}
      if(id(info_quad3_colour).state == "Blue") {it.rectangle(0, 3, 2, 2, Blue);}
      if(id(info_quad3_colour).state == "White") {it.rectangle(0, 3, 2, 2, White);}
      if(id(info_quad3_colour).state == "Black") {it.rectangle(0, 3, 2, 2, Black);}
      if(id(info_quad3_colour).state == "Orange") {it.rectangle(0, 3, 2, 2, Orange);}
      if(id(info_quad3_colour).state == "Yellow") {it.rectangle(0, 3, 2, 2, Yellow);}
      if(id(info_quad3_colour).state == "Purple") {it.rectangle(0, 3, 2, 2, Magenta);}
      if(id(info_quad3_colour).state == "Cyan") {it.rectangle(0, 3, 2, 2, Cyan);}
      if(id(info_quad3_colour).state == "Grey") {it.rectangle(0, 3, 2, 2, Silver);}

      // Set colour for quad 4
      if(id(info_quad4_colour).state == "Red") {it.rectangle(3, 3, 2, 2, Red);}
      if(id(info_quad4_colour).state == "Green") {it.rectangle(3, 3, 2, 2, Green);}
      if(id(info_quad4_colour).state == "Blue") {it.rectangle(3, 3, 2, 2, Blue);}
      if(id(info_quad4_colour).state == "White") {it.rectangle(3, 3, 2, 2, White);}
      if(id(info_quad4_colour).state == "Black") {it.rectangle(3, 3, 2, 2, Black);}
      if(id(info_quad4_colour).state == "Orange") {it.rectangle(3, 3, 2, 2, Orange);}
      if(id(info_quad4_colour).state == "Yellow") {it.rectangle(3, 3, 2, 2, Yellow);}
      if(id(info_quad4_colour).state == "Purple") {it.rectangle(3, 3, 2, 2, Magenta);}
      if(id(info_quad4_colour).state == "Cyan") {it.rectangle(3, 3, 2, 2, Cyan);}
      if(id(info_quad4_colour).state == "Grey") {it.rectangle(3, 3, 2, 2, Silver);}

      // Draw grid
      it.line(0, 2, 4, 2, Grid);
      it.line(2, 0, 2, 4, Grid);

The first section of the lambda defines ten standard colours in a hexadecimal format. The 0x defines that it is a hexadecimal representation, and the following characters are two character hex-codes for the red, green, and blue respectively. Note that black is defined as 0x000000, and using this colour effectively turns the red, green and blue components of a neopixel LED off.

// Define available colours
Color Red = Color(0xFF0000);
Color Green = Color(0x00FF00);
Color Blue = Color(0x0000FF);
Color White = Color(0xFFFFFF);
Color Black = Color(0x000000);
Color Orange = Color(0xFFA500);
Color Yellow = Color(0xFFFF00);
Color Magenta = Color(0xFF00FF);
Color Cyan = Color(0x00FFFF);
Color Silver = Color(0xC0C0C0);

These are the colours that will be used in the quadrants, but you may recall that I mentioned giving myself a more granular level of control over the grid zone. The first line of code here gets the state of the info_grid sensor, which you may recall from the sensor configuration is linked to the input_text.info_grid entity in Home Assistant (we’ll cover defining that later in this post). The second line of code defines gridColorValue as an unsigned 32-bit integer. The third line of code then takes the state string that was retrieved from Home Assistant, and converts that into a hexadecimal value, which is then stored in gridColorValue. The final line of code then defines that as a colour called Grid.

// Get the hexadecimal colour string from info_grid
std::string gridColorStr = id(info_grid).state.c_str();

// Convert the hexadecimal string to a 'Color' object
uint32_t gridColorValue;
sscanf(gridColorStr.c_str(), "%x", &gridColorValue);

// Define the colour 'Grid'
Color Grid = Color(gridColorValue);

The next four blocks of lambda code are almost identical, and are repeated for each of the quadrants (1-4). Each simply works through a list of colour names that could be retrieved from Home Assistant as the colour of the quadrant, and then fills the quadrant by lighting a 2 x 2 LED rectangle based on a coordinate for the top left corner LED of the quadrant.

// Set colour for quad 1
if(id(info_quad1_colour).state == "Red") {it.rectangle(0, 0, 2, 2, Red);}
if(id(info_quad1_colour).state == "Green") {it.rectangle(0, 0, 2, 2, Green);}
if(id(info_quad1_colour).state == "Blue") {it.rectangle(0, 0, 2, 2, Blue);}
if(id(info_quad1_colour).state == "White") {it.rectangle(0, 0, 2, 2, White);}
if(id(info_quad1_colour).state == "Black") {it.rectangle(0, 0, 2, 2, Black);}
if(id(info_quad1_colour).state == "Orange") {it.rectangle(0, 0, 2, 2, Orange);}
if(id(info_quad1_colour).state == "Yellow") {it.rectangle(0, 0, 2, 2, Yellow);}
if(id(info_quad1_colour).state == "Purple") {it.rectangle(0, 0, 2, 2, Magenta);}
if(id(info_quad1_colour).state == "Cyan") {it.rectangle(0, 0, 2, 2, Cyan);}
if(id(info_quad1_colour).state == "Grey") {it.rectangle(0, 0, 2, 2, Silver);}

Note that most but not all colours defined in home Assistant match the colour being applied to the matrix. I used recognisable colour names in Home Assistant, but directly applying the RGB associated with those names did not look great, so where necessary I deviated a little, but still maintained the correct name in the YAML configuration. It is effectively a mapping of object colour to a useful light colour.

Quadrant Top Left LED Coordinates
1 0,0
2 3,0
3 0,3
4 3,3

The final couple of lines of code draw two lines. One from 0,2 to 4,2, and one from 2,0 to 4,2. These two lines are lit in the Grid colour and will bisect the matrix on each axis to separate the quadrants.

// Draw grid
it.line(0, 2, 4, 2, Grid);
it.line(2, 0, 2, 4, Grid);

Full Configuration File

Below is what the amalgamated configuration file looks like. If you skipped down to this section and did not bother reading the above sections, I would recommend that you go back and check out the details as there will probably be some aspects of the configuration you would want to tailor to your own requirements.

# Specify substitutions to allow easy name re-use
substitutions:
  device_name: espinfo
  friendly_name: ESPINFO

# Main config begins
esphome:
  name: ${device_name}
  friendly_name: ${friendly_name}
  project:
    name: "sylumer.espinfo"
    version: "1.0.0"

# ESP32 definition
esp32:
  board: m5stack-atom
  framework:
    type: arduino

# Enable logging
logger:
  level: DEBUG

# Over the air
ota:
  password: !secret ota_password

# Details of default Wifi and fallback hotspot
wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: ${device_name} Fallback Hotspot
    password: !secret ap_password

# Portal
captive_portal:

# Enable Home Assistant API
api:
  encryption:
    key: !secret api_encryption_key

# Generic Sensors
sensor:
  - platform: wifi_signal
    id: wifi_value
    name: ${device_name} WiFi Signal
    icon: mdi:icon-strength-2

# Binary Sensors
binary_sensor:
  - platform: gpio
    name: "Main Button"
    id: mainbutton
    pin:
      number: 39
      inverted: true

# Text Sensors
text_sensor:
  # LED Quadrants
  - platform: homeassistant
    id: info_quad1_colour
    entity_id: input_select.info_quad1_colour
  - platform: homeassistant
    id: info_quad2_colour
    entity_id: input_select.info_quad2_colour
  - platform: homeassistant
    id: info_quad3_colour
    entity_id: input_select.info_quad3_colour
  - platform: homeassistant
    id: info_quad4_colour
    entity_id: input_select.info_quad4_colour
  # LED grid
  - platform: homeassistant
    id: info_grid
    entity_id: input_text.info_grid
  # WiFi IP Address
  - platform: wifi_info
    ip_address:
      name: ${device_name} IP Address
      id: host_ip
      entity_category: diagnostic
      icon: mdi:router-wireless

# Lights
light:
  # LED Matrix as a strip
  - platform: neopixelbus
    type: GRB
    variant: WS2812x
    pin: 27
    num_leds: 25
    id: led_matrix_light
    color_correct: [30%, 30%, 30%]
    restore_mode: ALWAYS_OFF

# Displays
display:
  # LED Matrix as an addressable grid
  - platform: addressable_light
    id: led_matrix_display
    addressable_light_id: led_matrix_light
    width: 5
    height: 5
    rotation: 0
    update_interval: 16ms
    lambda: |-
      // Define available colours
      Color Red = Color(0xFF0000);
      Color Green = Color(0x00FF00);
      Color Blue = Color(0x0000FF);
      Color White = Color(0xFFFFFF);
      Color Black = Color(0x000000);
      Color Orange = Color(0xFFA500);
      Color Yellow = Color(0xFFFF00);
      Color Magenta = Color(0xFF00FF);
      Color Cyan = Color(0x00FFFF);
      Color Silver = Color(0xC0C0C0);

      // Get the hexadecimal colour string from info_grid
      std::string gridColorStr = id(info_grid).state.c_str();

      // Convert the hexadecimal string to a 'Color' object
      uint32_t gridColorValue;
      sscanf(gridColorStr.c_str(), "%x", &gridColorValue);

      // Define the colour 'Grid'
      Color Grid = Color(gridColorValue);

      // Set colour for quad 1
      if(id(info_quad1_colour).state == "Red") {it.rectangle(0, 0, 2, 2, Red);}
      if(id(info_quad1_colour).state == "Green") {it.rectangle(0, 0, 2, 2, Green);}
      if(id(info_quad1_colour).state == "Blue") {it.rectangle(0, 0, 2, 2, Blue);}
      if(id(info_quad1_colour).state == "White") {it.rectangle(0, 0, 2, 2, White);}
      if(id(info_quad1_colour).state == "Black") {it.rectangle(0, 0, 2, 2, Black);}
      if(id(info_quad1_colour).state == "Orange") {it.rectangle(0, 0, 2, 2, Orange);}
      if(id(info_quad1_colour).state == "Yellow") {it.rectangle(0, 0, 2, 2, Yellow);}
      if(id(info_quad1_colour).state == "Purple") {it.rectangle(0, 0, 2, 2, Magenta);}
      if(id(info_quad1_colour).state == "Cyan") {it.rectangle(0, 0, 2, 2, Cyan);}
      if(id(info_quad1_colour).state == "Grey") {it.rectangle(0, 0, 2, 2, Silver);}

      // Set colour for quad 2
      if(id(info_quad2_colour).state == "Red") {it.rectangle(3, 0, 2, 2, Red);}
      if(id(info_quad2_colour).state == "Green") {it.rectangle(3, 0, 2, 2, Green);}
      if(id(info_quad2_colour).state == "Blue") {it.rectangle(3, 0, 2, 2, Blue);}
      if(id(info_quad2_colour).state == "White") {it.rectangle(3, 0, 2, 2, White);}
      if(id(info_quad2_colour).state == "Black") {it.rectangle(3, 0, 2, 2, Black);}
      if(id(info_quad2_colour).state == "Orange") {it.rectangle(3, 0, 2, 2, Orange);}
      if(id(info_quad2_colour).state == "Yellow") {it.rectangle(3, 0, 2, 2, Yellow);}
      if(id(info_quad2_colour).state == "Purple") {it.rectangle(3, 0, 2, 2, Magenta);}
      if(id(info_quad2_colour).state == "Cyan") {it.rectangle(3, 0, 2, 2, Cyan);}
      if(id(info_quad2_colour).state == "Grey") {it.rectangle(3, 0, 2, 2, Silver);}

      // Set colour for quad 3
      if(id(info_quad3_colour).state == "Red") {it.rectangle(0, 3, 2, 2, Red);}
      if(id(info_quad3_colour).state == "Green") {it.rectangle(0, 3, 2, 2, Green);}
      if(id(info_quad3_colour).state == "Blue") {it.rectangle(0, 3, 2, 2, Blue);}
      if(id(info_quad3_colour).state == "White") {it.rectangle(0, 3, 2, 2, White);}
      if(id(info_quad3_colour).state == "Black") {it.rectangle(0, 3, 2, 2, Black);}
      if(id(info_quad3_colour).state == "Orange") {it.rectangle(0, 3, 2, 2, Orange);}
      if(id(info_quad3_colour).state == "Yellow") {it.rectangle(0, 3, 2, 2, Yellow);}
      if(id(info_quad3_colour).state == "Purple") {it.rectangle(0, 3, 2, 2, Magenta);}
      if(id(info_quad3_colour).state == "Cyan") {it.rectangle(0, 3, 2, 2, Cyan);}
      if(id(info_quad3_colour).state == "Grey") {it.rectangle(0, 3, 2, 2, Silver);}

      // Set colour for quad 4
      if(id(info_quad4_colour).state == "Red") {it.rectangle(3, 3, 2, 2, Red);}
      if(id(info_quad4_colour).state == "Green") {it.rectangle(3, 3, 2, 2, Green);}
      if(id(info_quad4_colour).state == "Blue") {it.rectangle(3, 3, 2, 2, Blue);}
      if(id(info_quad4_colour).state == "White") {it.rectangle(3, 3, 2, 2, White);}
      if(id(info_quad4_colour).state == "Black") {it.rectangle(3, 3, 2, 2, Black);}
      if(id(info_quad4_colour).state == "Orange") {it.rectangle(3, 3, 2, 2, Orange);}
      if(id(info_quad4_colour).state == "Yellow") {it.rectangle(3, 3, 2, 2, Yellow);}
      if(id(info_quad4_colour).state == "Purple") {it.rectangle(3, 3, 2, 2, Magenta);}
      if(id(info_quad4_colour).state == "Cyan") {it.rectangle(3, 3, 2, 2, Cyan);}
      if(id(info_quad4_colour).state == "Grey") {it.rectangle(3, 3, 2, 2, Silver);}

      // Draw grid
      it.line(0, 2, 4, 2, Grid);
      it.line(2, 0, 2, 4, Grid);

Now we have a configuration for our ESP32 device to display content, we need to do some configuration in Home Assistant to make content available. Once the device is registered as configured in Home Assistant, its settings look something like this.

Key things to note right now are the button and the diagnostic WiFi information. You might also note I have an automation configured, and I will cover that before the end of the post.

Home Assistant Helper Configuration

The ATOM Matrix configuration is covered, but it is no use without the data in Home Assistant to drive it. Just like for my information display I have elected to use helper entities in Home Assistant. It makes things really easy for testing, and using a consistent approach I am sure will make things easier for myself in regard to long term maintenance.

The helpers are configured within Settings > Devices and Services > Helpers in Home Assistant, and I use two different types for this project.

Static Colour Dropdown

The names of the ten colours programmed into the display lambda are set up as “dropdown” helpers for each of the quadrants.

An identical helper was created for each of the four quadrants. Unfortunately, Home Assistant currently has no convenient duplication option, so each of them was created manually.

Dynamic Colour String

The colour for the grid you may recall is to be specified as a hexadecimal colour string. Only one such helper is required and this was defined as a text helper. The key element to note is the regular expression pattern that is defined. ^0x[0-9A-Fa-f]{6}$ is used to ensure the field begins 0x, and is followed by exactly six hexadecimal characters.

Adding the Helpers to the User Interface

For testing purposes I find it useful to have these helper entities accessible for modification on a dashboard page. The following dashboard widget was configured using the YAML below.

type: entities
entities:
  - entity: input_select.info_quad1_colour
    name: Q1 Weather
  - entity: input_select.info_quad2_colour
    name: Q2 Temperature
  - entity: input_select.info_quad3_colour
    name: Q3 Waste Collection
  - entity: input_select.info_quad4_colour
    name: Q4 TBC
  - entity: input_text.info_grid
title: Info

Home Assistant Automation

At this point all of the configuration is in place to allow the ATOM Matrix display to have its zone colours set via this widget. However, the whole point of this was to automate the setting of the colouration of the zones, so there is more work to do to hook up some automatic data feeds.

Quadrant 1 - Weather

The first quadrant is to display a set a colour based on the primary weather condition. To set this I created an automation that runs every hour. It uses data from a local Weather Underground station that I’ve surfaced in Home Assistant using the wundergroundpws custom integration. It uses value templates to identify if keywords are present in the current weather status. When present, it sets a colour in the input_select.info_quad1_colour by using Call Service to specify the value for the input select.

Check State For Colour Selection
“sun” Orange
“snow” White
“cloud” Grey
“rain” Cyan
“storm” Blue
“wind” Purple

The default colour is set to ‘Red’ each time the automation runs and then is modified by the matched type of weather. This way if a weather condition occurs that I have not accounted for, it will show as Red and I can check on what it is and update the automation to account for it.

The YAML for this automation is shown below.


alias: Time Hourly - Info - Weather
description: Set Quad 1 colour based on type of weather from Weather Underground station.
trigger:
  - platform: time_pattern
    minutes: "01"
    seconds: "10"
condition: []
action:
  - service: input_select.select_option
    target:
      entity_id: input_select.info_quad1_colour
    data:
      option: Red
    alias: Default to Red to flag if we get an unaccounted for condition
  - alias: Sun => Orange
    if:
      - condition: template
        value_template: "{{ 'sun' in states('weather.iyork000') }}"
    then:
      - service: input_select.select_option
        metadata: {}
        data:
          option: Orange
        target:
          entity_id: input_select.info_quad1_colour
  - alias: Snow => White
    if:
      - condition: template
        value_template: "{{ 'snow' in states('weather.iyork000') }}"
    then:
      - service: input_select.select_option
        metadata: {}
        data:
          option: White
        target:
          entity_id: input_select.info_quad1_colour
  - alias: Cloud => Grey
    if:
      - condition: template
        value_template: "{{ 'cloud' in states('weather.iyork000') }}"
    then:
      - service: input_select.select_option
        metadata: {}
        data:
          option: Grey
        target:
          entity_id: input_select.info_quad1_colour
  - if:
      - condition: template
        value_template: "{{ 'rain' in states('weather.iyork000') }}"
    then:
      - service: input_select.select_option
        metadata: {}
        data:
          option: Cyan
        target:
          entity_id: input_select.info_quad1_colour
    alias: Rain => Cyan
  - if:
      - condition: template
        value_template: "{{ 'storm' in states('weather.iyork000') }}"
    then:
      - service: input_select.select_option
        metadata: {}
        data:
          option: Blue
        target:
          entity_id: input_select.info_quad1_colour
    alias: Storm => Blue
  - if:
      - condition: template
        value_template: "{{ 'wind' in states('weather.iyork000') }}"
    then:
      - service: input_select.select_option
        metadata: {}
        data:
          option: Purple
        target:
          entity_id: input_select.info_quad1_colour
    alias: Wind => Purple
mode: single

Quadrant 2 - Temperature

The temperature once again uses a local weather station available via Weather Underground. This time a range check is carried out on the temperature (in Celsius and integer values), and a colour is displayed in line with the temperature. Again an hourly check is carried out via an automation, and the mapping is based on the following.

Range Lower Bound Upper Bound Colour
0 and below - 1 White
1 to 10 0 11 Cyan
11 to 15 10 16 Green
16 to 20 15 21 Yellow
21 to 25 20 26 Orange
26 and above 25 - Red

The YAML for this automation is shown below.


alias: Time Hourly - Info - Temperature
description: >-
  Set Quad 2 colour based on current temperature from Weather Underground
  station.
trigger:
  - platform: time_pattern
    minutes: "01"
condition: []
action:
  - if:
      - condition: numeric_state
        entity_id: weather.iyork000
        attribute: temperature
        below: 1
    then:
      - service: input_select.select_option
        metadata: {}
        data:
          option: White
        target:
          entity_id: input_select.info_quad2_colour
    alias: 0 or below, set quad 2 white
  - alias: Between 0 and 10, set quad 2 cyan
    if:
      - condition: numeric_state
        entity_id: weather.iyork000
        attribute: temperature
        below: 11
        above: 0
    then:
      - service: input_select.select_option
        metadata: {}
        data:
          option: Cyan
        target:
          entity_id: input_select.info_quad2_colour
  - alias: Between 10 and 15, set quad 2 green
    if:
      - condition: numeric_state
        entity_id: weather.iyork000
        attribute: temperature
        below: 16
        above: 10
    then:
      - service: input_select.select_option
        metadata: {}
        data:
          option: Green
        target:
          entity_id: input_select.info_quad2_colour
  - alias: Between 16 and 20, set quad 2 yellow
    if:
      - condition: numeric_state
        entity_id: weather.iyork000
        attribute: temperature
        below: 21
        above: 15
    then:
      - service: input_select.select_option
        metadata: {}
        data:
          option: Yellow
        target:
          entity_id: input_select.info_quad2_colour
  - alias: Between 21 and 25, set quad 2 orange
    if:
      - condition: numeric_state
        entity_id: weather.iyork000
        attribute: temperature
        below: 26
        above: 20
    then:
      - service: input_select.select_option
        metadata: {}
        data:
          option: Orange
        target:
          entity_id: input_select.info_quad2_colour
  - alias: Over 25, set quad 2 red
    if:
      - condition: numeric_state
        entity_id: weather.iyork000
        attribute: temperature
        above: 25
    then:
      - service: input_select.select_option
        metadata: {}
        data:
          option: Red
        target:
          entity_id: input_select.info_quad2_colour
mode: single

Quadrant 3 - Waste Collection

As noted right at the start I use the Waste Collection Schedule integration to get the details for my local waste collection, and specifically the details for the next collection. The collection types are mapped as follows to colours as follows.

Collection Type Contains Colour
Refuse Refuse White
Recycling and Garden Garden Green
Recycling   Orange

Note that recycling is not a unique term, but because there is always a next collection, I can default anything that does not contain “Refuse” or “Garden” to be recycling only.

The YAML for this automation, that is run daily, is shown below.


alias: Time 00:20:00 - Info - Waste Collection
description: Q3 info for waste collection
trigger:
  - platform: time
    at: "00:20:00"
condition: []
action:
  - alias: Bins or Greens?
    if:
      - alias: Next collection is bins
        condition: template
        value_template: "{{ 'Refuse' in states('sensor.next_waste_collection') }}"
    then:
      - service: input_select.select_option
        metadata: {}
        data:
          option: White
        target:
          entity_id: input_select.info_quad3_colour
        alias: Refuse -> Set quad 3 white
    else:
      - alias: Next collection is greens
        if:
          - alias: Next collection includes garden waste
            condition: template
            value_template: "{{ 'Garden' in states('sensor.next_waste_collection') }}"
        then:
          - alias: Garden and Recycling -> Set quad 3 green
            service: input_select.select_option
            metadata: {}
            data:
              option: Green
            target:
              entity_id: input_select.info_quad3_colour
        else:
          - alias: Recycling only -> Set quad 3 orange
            service: input_select.select_option
            metadata: {}
            data:
              option: Orange
            target:
              entity_id: input_select.info_quad3_colour
mode: single

Bonus: Clarification Announcement

When I configured the ATOM Matrix I set up the switch beneath the LED matrix as a button. I have assigned this button to a final automation that is intended to help my family and I recall what the less frequently displayed colours might mean. After all, after a period of UK winter weather we might not recall that orange in quadrant 1 corresponds to a sunny day.

When triggered, this automation uses a set of value templates to take the raw data used in each of the three previous automations and deliver a spoken summary via the Amazon Alexa located close by in the kitchen.


alias: Speak Info in Kitchen
description: ""
trigger:
  - type: turned_on
    platform: device
    device_id: 3baaaa012d0000000000b03aaaa3abba
    entity_id: 04aaaaad0000000000054f5aaaaabaaf
    domain: binary_sensor
condition: []
action:
  - service: notify.alexa_media_kitchen
    data:
      message: >-
        Weather is {{ states('weather.iyork000') }}. Temperature is {{
        states('sensor.iyork000_temperature') }} degrees. Next collection is {{
        states('sensor.next_waste_collection') }}.
mode: single

Conclusion

The ATOM Matrix is now installed in my kitchen in a small unused space between some kitchen cupboards and the kitchen door. At some point I will replace and restrain the current USB C cable with something a little more subtle. Certainly a more neutral coloured cable, and probably one with a right angle connection so I can then run it neat against the cupboard edge for most of its length.

That is my latest little side-project with Home Assistant, and while I wasn’t sure what the family would think, everyone independently noted that it was really useful. I couldn’t really have asked for higher praise, and it is particularly surprising given how it looks with the long red tail of a red USB cable hanging out the bottom.

I hope that you have found this post useful and even if you do not plan to implement something exactly the same as this that it has given you some ideas and inspiration for how you could use an ESP32 powered LED matrix with your own instance of Home Assistant. Do let me know over on Mastodon if you have found this helpful and what you are planning to do or have done along similar lines.

Author: Stephen Millard
Tags: | home assistant |

Buy me a coffeeBuy me a coffee



Related posts that you may also like to read