7219 Dot Matrix display and esphome

So, there are a few messages about how to do this but they are scattered around so I thought it might help someone if I put together what I’ve done in one spot for reference.

Firstly, a big shout out to those who have worked on this in the past, and who are continuing to do so. I’d like to in particular give kudos to:

Qrome who created this:

That I used initially many years ago, and used the 3D print file to make the case. This thing works very well, and I made a few extra that I gave to people as presents, but I wanted to be able to display specific information from Home Assistant, so esphome seemed perfect for that.

Then there was also wida - who provided this:

and indeed I was able to use this to create a display I could then send messages to it via mqtt. This also worked well, with the ability to send any message you want to one or more displays. I however also wanted something that I could ‘set and forget’ that was independent from mqtt. So, on to my build.

The hardware:
I cannot really improve on the instructions provided by Qrome. The standard case provided to house a 4 panel display also works perfectly. There is the option to make bigger cases to house multiple displays, with the understanding that you will need to make joins so is a little hard to get to look perfect plus you can run into more power issues as you drive more panels with a single power supply.

Speaking of power issues though - even when running just 4 panels, sometimes you might find the screen develops weird artefacts that can only be cleared by rebooting the device. How often (if at all) this happens depends a lot on the power supply being used, the USB cable quality, fluctuations in grid power, and other random factors. What I recommend is to add a small capacitor to effectively provide a small UPS - if you are having issues, this should reduce or even remove the problem entirely. Although over the years I have seen them pretty rarely, I now tend to always add in a capacitor for safety. This is of course definitely optional. I’d suggest 1000uF, 10V or greater capacitor. FWIW Positive = VCC = V+ = longer leg; Negative = G = ground = shorter leg = stripe. The capacitor is probably best on the same end you are pumping power in through, but (as I tend to do to make life a little easier) it can go on the other end of the row of panels.

For the controller, you can of course use pretty much any esp device, although the 3d case is designed for the wemos d1 mini, so that is the easiest route to take. If you plan to connect multiple external sensors though, you may end up running out of GPIO ports in which case may need to use something else. There is however plenty of room in the case if you use something bigger, in particular if you directly solder the wires rather than use dupont cables.

The software:
Once you have physically made the device, then create the required yaml file, and install as usual. The easiest way is to compile the binary code and save it locally, then use https://web.esphome.io/ to prepare the device and upload the new binary to it. From then on, you will be able to manage the display OTA via esphome.

Which brings me to the code. I have commented my example a bit to help give you an idea of what you can do. In this case I want it to:
Normally show how much power is being consumed from the grid - it displays that in W if it’s less than 1000W, or in kW if it’s greater.
When my solar panels are generating more power than I am consuming, then the display starts a scrolling display that shows other information such as how much solar power is being generated, an estimate of how much the power has cost today (taking into account any credit from power fed back into the grid), the current outside temperature, the weather forecast for today, etc… obviously, if you use this code as a starting point you will need to change the sensors to match what you have, and so they show the info you want to display.

# Compiled and tested on esphome 2024.6.6 and HA 2024.7.2

substitutions:
  devicename: matrix7219
  friendname: 7219 Matrix Display
  board: d1_mini
# Display pins
  clpin: D5 # D5 is connected to CLK of MAX7219
  cspin: D6 # D6 is connected to CS of MAX7219
  mopin: D7 # D7 is connected to MOSI of MAX7219

esphome:
  name: $devicename
  friendly_name: $friendname

esp8266:
  board: $board

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

ota:
  password: !secret ota_password
  platform: esphome

wifi:
  networks:
  - ssid: !secret wifIoT_ssid
    password: !secret wifIoT_password
    priority: 2
# Backup SSID just in case
  - ssid: !secret wifi_ssid
    password: !secret wifi_password
    priority: 1
  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "$devicename Fallback Hotspot"
    password: !secret ota_password

# Enable logging
# Change to avoid "Components should block for at most 20-30ms" warning messages in the log - an issue since 2023.7.0
# Not really a breaking change - it's an issue I suspect due to the device being slow and this error previously
# simply not being reported
logger:
  logs:
    component: ERROR

# If you want to use MQTT can put the details in here and make the required changes to the display code
#mqtt:
#  broker: 192.168.0.30
#  broker: !secret mqtt_broker
#  username: !secret mqttuser
#  password: !secret mqttpass

time:
  - platform: homeassistant
    timezone: "Australia/Melbourne"
    id: homeassistant_time
# Note - do not need to specify the time zone as it will get that from the server, but I tend to include it anyhow

spi:
  clk_pin: $clpin 
  mosi_pin: $mopin 

display:
  - platform: max7219digit
    id: display_max
    cs_pin: $cspin
    num_chips: 4
    intensity: 3
    scroll_speed: 100ms
    update_interval: 100ms
    lambda: |-
      if(id(power_on_off).state) {
      if(id(power_grid_inst).state >= 1000) {
      it.printf(0, 0, id(digit_font), TextAlign::TOP_LEFT, "%.1f KW" , id(power_grid_inst).state/1000);
      it.intensity(id(intensity).state);  
      }
      else if(id(power_grid_inst).state > 0) {
      it.printf(0, 0, id(digit_font), TextAlign::TOP_LEFT, "%.0f W" , id(power_grid_inst).state);
      it.intensity(id(intensity).state);
      }
      else {
      it.printf(0, 0, id(digit_font), TextAlign::TOP_LEFT, " Sol now: %.1fkW Generated: %.0fkWh Fed: %.0fkWh Grid Used: %.0fkWh Est Cost: $%.2f Now: %.1f° Today: %.1f°/%.1f° %s", 
      id(power_solar_inst).state/1000, 
      id(power_solar_gen).state/1000, 
      id(power_grid_received_total).state, id(power_grid_delivered_total).state, 
      0.973500 + id(power_energy_offpeak).state*0.214500 + id(power_energy_peak).state*0.332200 - 0.054000*id(power_grid_received_total).state,
      id(outdoor_temperature).state, 
      id(min_temperature).state, 
      id(max_temperature).state,
      id(forecast_ext0).state.c_str());
      it.intensity(id(intensity).state);
      it.scroll_left();
      }
      }

# Notes
# %d is a decimal number eg 10
# %f is a floating point number eg %.2f is 10.12, %.1f is 10.1, %.0f is 10
# %c is a character value eg a
# %s is a string hello

# working
# It is super easy to break the display string by a misplaced semi comma or bracket. I like to keep a copy
# of the most recent working code so if or rather when I do something stupid it is quick to roll back

#      if(id(power_on_off).state) {
#      if(id(power_grid_inst).state >= 1000) {
#      it.printf(0, 0, id(digit_font), TextAlign::TOP_LEFT, "%.1f KW" , id(power_grid_inst).state/1000);
#      it.intensity(atoi(id(habri).state.c_str()));  
#      }
#      else if(id(power_grid_inst).state > 0) {
#      it.printf(0, 0, id(digit_font), TextAlign::TOP_LEFT, "%.0f W" , id(power_grid_inst).state);
#      it.intensity(atoi(id(habri).state.c_str()));
#      }
#      else {
#      it.printf(0, 0, id(digit_font), TextAlign::TOP_LEFT, " Sol now: %.1fkW Generated: %.0fkWh Fed: %.0fkWh Grid Used: %.0fkWh Est Cost: $%.2f Now: %.1f° Today: %.1f°/%.1f° %s", 
#      id(power_solar_inst).state/1000, 
#      id(power_solar_gen).state/1000, 
#      id(power_grid_received_total).state, id(power_grid_delivered_total).state, 
#      0.973500 + id(power_energy_offpeak).state*0.214500 + id(power_energy_peak).state*0.332200 - 0.054000*id(power_grid_received_total).state,
#      id(outdoor_temperature).state, 
#      id(min_temperature).state, 
#      id(max_temperature).state,
#      id(forecast_ext0).state.c_str());
#      it.intensity(atoi(id(habri).state.c_str()));
#      it.scroll_left();
#      }
#      }


binary_sensor:
  - platform: template
    name: "Power State"
    id: power_on_off
    internal: true

switch:
  - platform: template
    name: "max7219 Display Power Switch"
    icon: mdi:dots-grid
    optimistic: true
    restore_mode: ALWAYS_ON
    turn_on_action:
      - binary_sensor.template.publish:
          id: power_on_off
          state: ON
    turn_off_action:
      - binary_sensor.template.publish:
          id: power_on_off
          state: OFF

number:
  - platform: template
    name: "Brightness"
    id: intensity
    optimistic: true
    min_value: 0
    max_value: 15
    step: 1

# Note - the font can be obtained from https://www.dafont.com/font-comment.php?file=pixelmix
font:
  - file: "fonts/pixelmix.ttf"
    id: digit_font
    size: 8
    glyphs:
      ['$','&', '@', '!', ',', '.', '"', '%', '(', ')', '+', '-', '_', ':', '°', '0',
       '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E',
       'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S',
       'T', 'U', 'V', 'W', 'X', 'Y', 'Z', ' ', 'a', 'b', 'c', 'd', 'e', 'f',
       'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't',
       'u', 'v', 'w', 'x', 'y', 'z','å', 'ä', 'ö', '/']
       
text_sensor:
  - platform: wifi_info
    ip_address:
      name: "max7219 Display IP Address"
    ssid:
      name: "max7219 Display Connected SSID"
    bssid:
      name: "max7219 Display Connected BSSID"
# Example of using a sensor state to change the device eg
# Go into developer-tools/state from the gui and create the sensor input_number.max7219_bri with a state of, say, 1
# Then would need to modify the display to use
# it.intensity(atoi(id(habri).state.c_str()));
# instead of
# it.intensity(id(intensity).state);
# and add in the following
#  - platform: homeassistant
#    name: "HA Brightness"
#    id: habri
#    entity_id: input_number.max7219_bri
  - platform: homeassistant
    id: forecast_short0
    entity_id: sensor.brighton_east_short_text_0
  - platform: homeassistant
    id: forecast_ext0
    entity_id: sensor.brighton_east_extended_text_0
  - platform: homeassistant
    id: forecast_short1
    entity_id: sensor.brighton_east_short_text_1


sensor:
  - platform: wifi_signal
    name: "WiFi Signal Sensor"
    id: wifisignal
    update_interval: 60s
    unit_of_measurement: dBm
    accuracy_decimals: 0
    device_class: signal_strength
    state_class: measurement
    entity_category: diagnostic
  - platform: copy # Reports the WiFi signal strength in %
    source_id: wifisignal
    id: wifipercent
    name: "WiFi Signal Percent"
    filters:
      - lambda: return min(max(2 * (x + 100.0), 0.0), 100.0);
    unit_of_measurement: "Signal %"
    entity_category: "diagnostic"
  - platform: uptime
    name: "$devicename Uptime"
  - platform: homeassistant
    id: outdoor_temperature
    entity_id: sensor.gw1000_v1_7_6_outdoor_temperature
  - platform: homeassistant
    id: max_temperature
    entity_id: sensor.brighton_east_temp_max_0
  - platform: homeassistant
    id: min_temperature
    entity_id: sensor.brighton_east_temp_min_0
  - platform: homeassistant
    id: max_temperature_next
    entity_id: sensor.brighton_east_temp_max_1
  - platform: homeassistant
    id: min_temperature_next
    entity_id: sensor.brighton_east_temp_min_1
  - platform: homeassistant
    id: power_grid_inst
    entity_id: sensor.instantaneousdemand
  - platform: homeassistant
    id: power_energy_peak
    entity_id: sensor.daily_energy_peak
  - platform: homeassistant
    id: power_energy_offpeak
    entity_id: sensor.daily_energy_offpeak
  - platform: homeassistant
    id: power_grid_received_total
    entity_id: sensor.currentsummationreceived_kwh_daily
  - platform: homeassistant
    id: power_grid_delivered_total
    entity_id: sensor.currentsummationdelivered_kwh_daily    
  - platform: homeassistant
    id: power_solar_inst
    entity_id: sensor.symo_15_0_3_m_1_ac_power  
  - platform: homeassistant
    id: power_solar_gen
    entity_id: sensor.symo_15_0_3_m_1_energy_day

Separate to that I have an automation that turns the screen on in the morning with the brightness set to the minimum “0” (which is frankly more than bright enough) and another that turns it off at night.

eg:

alias: 7219 - turn on at 9am
description: ""
trigger:
  - platform: time
    at: "09:00:00"
condition: []
action:
  - service: switch.turn_on
    target:
      entity_id:
        - switch.max7219_display_power_switch
    data: {}
  - service: number.set_value
    metadata: {}
    data:
      value: "0"
    target:
      entity_id: number.matrix7219_brightness
mode: single

End result is something like this:


By the way, the blur around the digits is an artefact from the photo - it looks a lot more ‘crisp’ in real life. :slight_smile:

2 Likes