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.