Two M5Stack Displays, One ESPHome Project - Scargill's Tech Blog

Years ago (2021) I ended up with a pair of rather different little M5Stack units — an M5 Tough and an M5Stack Core2/M2 — and yesterday decided they deserved something more interesting to do than sit around running demonstration software.The answer was ESPHome.The idea was simple enough: turn both units into compact, touch-sensitive information displays connected to Home Assistant.

Of course, as is often the way with these things, the implementation became rather more involved — but the results have been worth it.The larger M5 Tough has a touchscreen, which is now divided into simple control areas.Touch one side of the screen to move backwards through the information pages, the other side to move forwards, and the centre section toggles the display brightness.

The smaller M5Stack unit has three physical buttons (well, touch buttons anyway) along the bottom, so the same basic controls were adapted for those instead.Both devices now display useful information from Home Assistant, including time, date, indoor and outdoor temperatures and humidity.There’s also a detailed weather page showing current conditions, cloud cover, wind, UV index and atmospheric pressure.

And because these are connected to Home Assistant, I couldn’t resist adding a more practical feature: an automatic 5G router warning page.If the router goes offline, the display switches to a rather obvious red alert screen and returns to whatever it was showing when the connection comes back.What I particularly like about this project is that ESPHome makes these little devices genuinely useful without requiring a separate server or a complicated custom application.

They become part of the existing Home Assistant setup and can display practically anything that Home Assistant knows about.The two units are quite different physically, but the finished result is surprisingly consistent: a pair of smart little information displays that are always on, easy to read and easy to control.And, naturally, now that they are working properly, I’ve started thinking of more pages to add… For the sake of it, here is the code for the two of them – beware the code may be improved arbitrarily over time – and better self-documented – but it works – Core2 first then Tough.

You may notice a trend in my recent ESPHOME blog entries – I’ve starting to put all my WiFi SSIDs in for when moving devices around.Oh and they both have WebUIs – current TOUGH WebUI shown below.esphome: name: core2-clock friendly_name: Core2 Clock on_boot: priority: -100 then: - delay: 10s - lambda: |- id(router_monitor_ready) = true; esp32: variant: esp32 flash_size: 16MB framework: type: arduino psram: mode: quad speed: 80MHz logger: api: ota: - platform: esphome wifi: networks: - ssid: !secret wifi_ssid password: !secret wifi_password - ssid: !secret wifi_ssid2 password: !secret wifi_password - ssid: !secret wifi_ssid3 password: !secret wifi_password - ssid: !secret wifi_ssid4 password: !secret wifi_password - ssid: !secret wifi_ssid5 password: !secret wifi_password power_save_mode: none post_connect_roaming: true ap: ssid: "Core2 Clock Fallback" password: "5uKWDKfAoKjs" captive_portal: # Web interface and OTA update page.

# Useful if the display or touchscreen ever stops responding.web_server: version: 3 local: true button: - platform: template name: "Core2 Page A - Clock" id: web_page_a on_press: then: - lambda: |- id(current_page) = 0; # Display refreshes automatically every second.- platform: template name: "Core2 Page B - House Status" id: web_page_b on_press: then: - lambda: |- id(current_page) = 1; # Display refreshes automatically every second.

- platform: template name: "Core2 Page C - Weather" id: web_page_c on_press: then: - lambda: |- id(current_page) = 2; # Display refreshes automatically every second.external_components: - source: github://martydingo/esphome-axp192 components: [axp192] # BLE proxy esp32_ble_tracker: bluetooth_proxy: active: true globals: - id: current_page type: int restore_value: no initial_value: '0' - id: previous_page type: int restore_value: no initial_value: '0' - id: router_alert_active type: bool restore_value: no initial_value: 'false' - id: router_state_received type: bool restore_value: no initial_value: 'false' - id: router_monitor_ready type: bool restore_value: no initial_value: 'false' i2c: - id: bus_a sda: GPIO21 scl: GPIO22 scan: true touchscreen: - platform: ft63x6 i2c_id: bus_a interrupt_pin: GPIO39 binary_sensor: # Network health monitors from Home Assistant - platform: homeassistant id: living_room_5g_router name: "5G Router" entity_id: binary_sensor.living_room_5g_router entity_category: diagnostic on_state: then: - lambda: |- if (!id(router_monitor_ready)) { return; } if (!x && !id(router_alert_active)) { id(previous_page) = id(current_page); id(router_alert_active) = true; id(current_page) = 3; ESP_LOGI( "router_alert", "5G router DOWN - switching to alert page" ); } else if (x && id(router_alert_active)) { id(current_page) = id(previous_page); id(router_alert_active) = false; ESP_LOGI( "router_alert", "5G router restored - returning to previous page" ); } - component.update: core2_display - platform: homeassistant id: internet_1_1_1_1 name: "Internet 1.1.1.1" entity_id: binary_sensor.1_1_1_1 entity_category: diagnostic - platform: homeassistant id: living_room_smlight name: "Living Room SMLIGHT" entity_id: binary_sensor.living_room_smlight entity_category: diagnostic # En-suite bathroom hot water heater - platform: homeassistant id: ensuite_hot_water name: "En-suite Hot Water" entity_id: switch.athom_pg01_v2_2 # LEFT BUTTON - previous page - platform: touchscreen id: core2_button_a x_min: 10 x_max: 120 y_min: 240 y_max: 280 use_raw: true on_press: then: - lambda: |- if (!id(router_alert_active)) { id(current_page)--; if (id(current_page) < 0) { id(current_page) = 2; } ESP_LOGI("core2", "Page changed to %d", id(current_page)); } - component.update: core2_display # CENTRE BUTTON - dim / full brightness - platform: touchscreen id: core2_button_b x_min: 130 x_max: 200 y_min: 240 y_max: 280 use_raw: true on_press: then: - lambda: |- static bool dimmed = false; uint16_t mv; if (!dimmed) { // DIM mv = 2600; dimmed = true; ESP_LOGI("core2", "Backlight DIM"); } else { // FULL mv = 3300; dimmed = false; ESP_LOGI("core2", "Backlight FULL"); } // AXP192 DCDC3 voltage control.uint8_t reg = 0x27; uint8_t setting = (uint8_t)((mv - 700) / 25); uint8_t output[2] = { reg, setting }; id(bus_a).write( 0x34, output, 2 ); # RIGHT BUTTON - next page - platform: touchscreen id: core2_button_c x_min: 230 x_max: 310 y_min: 240 y_max: 280 use_raw: true on_press: then: - lambda: |- if (!id(router_alert_active)) { id(current_page)++; if (id(current_page) > 2) { id(current_page) = 0; } ESP_LOGI("core2", "Page changed to %d", id(current_page)); } - component.update: core2_display spi: clk_pin: GPIO18 mosi_pin: GPIO23 sensor: # Diagnostics - platform: wifi_signal name: "Core2 WiFi Signal" id: core2_wifi_signal update_interval: 60s entity_category: diagnostic - platform: uptime name: "Core2 Uptime" id: core2_uptime entity_category: diagnostic - platform: template name: "Core2 Current Page" id: core2_current_page accuracy_decimals: 0 update_interval: 1s entity_category: diagnostic lambda: |- return id(current_page) + 1; # Detailed weather attributes - platform: homeassistant id: weather_wind_speed entity_id: weather.forecast_home attribute: wind_speed - platform: homeassistant id: weather_wind_bearing entity_id: weather.forecast_home attribute: wind_bearing - platform: homeassistant id: weather_uv_index entity_id: weather.forecast_home attribute: uv_index - platform: homeassistant id: weather_pressure entity_id: weather.forecast_home attribute: pressure - platform: homeassistant id: weather_cloud_coverage entity_id: weather.forecast_home attribute: cloud_coverage # Keep the AXP192 component for Core2 power/display initialisation.

- platform: axp192 model: M5CORE2 address: 0x34 i2c_id: bus_a update_interval: 60s # Bedroom - platform: homeassistant id: bedroom_temperature name: "Bedroom Temperature" entity_id: sensor.airguard_th_temperature - platform: homeassistant id: bedroom_humidity name: "Bedroom Humidity" entity_id: sensor.airguard_th_humidity # Outside - platform: homeassistant id: outside_temperature name: "Outside Temperature" entity_id: sensor.outdoor_meter_65_temperature - platform: homeassistant id: outside_humidity name: "Outside Humidity" entity_id: sensor.outdoor_meter_65_humidity # Kitchen CO₂ - platform: homeassistant id: kitchen_co2 name: "Kitchen CO2" entity_id: sensor.meter_pro_co2_monitor_d6_carbon_dioxide # Garage CO₂ - platform: homeassistant id: garage_co2 name: "Garage CO2" entity_id: sensor.meter_pro_co2_monitor_new_carbon_dioxide text_sensor: # Diagnostics visible in the web interface - platform: wifi_info ip_address: name: "Core2 IP Address" id: core2_ip_address entity_category: diagnostic - platform: homeassistant id: weather_condition name: "Weather Condition" entity_id: weather.forecast_home - platform: template id: network_status name: "Network Status" update_interval: 10s lambda: |- if (!id(living_room_5g_router).state) { return {"5G ROUTER DOWN"}; } else if (!id(internet_1_1_1_1).state) { return {"INTERNET DOWN"}; } else if (!id(living_room_smlight).state) { return {"ZIGBEE OFFLINE"}; } else { return {"NETWORK OK"}; } time: - platform: sntp id: clock_time timezone: Europe/Madrid servers: - time.google.com - time.cloudflare.com - pool.ntp.org update_interval: 1h font: - file: "gfonts://Roboto" id: clock_font size: 96 - file: "gfonts://Roboto" id: date_font size: 18 - file: "gfonts://Roboto" id: label_font size: 16 - file: "gfonts://Roboto" id: value_font size: 26 - file: "gfonts://Roboto" id: weather_font size: 36 - file: "fonts/materialdesignicons-webfont.ttf" id: weather_icon size: 32 glyphs: - "\U000F0599" # sunny - "\U000F0590" # cloudy - "\U000F0595" # partly cloudy - "\U000F0597" # rainy - "\U000F0596" # pouring - "\U000F0593" # lightning - "\U000F067E" # lightning rainy - "\U000F0594" # clear night - "\U000F0591" # fog - "\U000F058E" # water / humidity display: - platform: mipi_spi id: core2_display model: M5CORE2 update_interval: 1s lambda: |- // Background it.fill(Color(5, 8, 20)); int page = id(current_page); // ================================================= // PAGE A - MAIN CLOCK // ================================================= if (page == 0) { auto now = id(clock_time).now(); if (!now.is_valid()) { it.print( 160, 120, id(date_font), Color(255, 255, 255), TextAlign::CENTER, "WAITING FOR TIME" ); } else { // --------------------------------------------- // WEATHER // --------------------------------------------- std::string weather = id(weather_condition).state; std::string weather_text = "WEATHER"; const char *weather_glyph = "\U000F0590"; if (weather == "sunny") { weather_text = "SUNNY"; weather_glyph = "\U000F0599"; } else if (weather == "partlycloudy") { weather_text = "PARTLY CLOUDY"; weather_glyph = "\U000F0595"; } else if (weather == "cloudy") { weather_text = "CLOUDY"; weather_glyph = "\U000F0590"; } else if (weather == "rainy") { weather_text = "RAIN"; weather_glyph = "\U000F0597"; } else if (weather == "pouring") { weather_text = "HEAVY RAIN"; weather_glyph = "\U000F0596"; } else if (weather == "lightning") { weather_text = "STORM"; weather_glyph = "\U000F0593"; } else if (weather == "lightning-rainy") { weather_text = "STORM"; weather_glyph = "\U000F067E"; } else if (weather == "clear-night") { weather_text = "CLEAR NIGHT"; weather_glyph = "\U000F0594"; } else if (weather == "fog") { weather_text = "FOG"; weather_glyph = "\U000F0591"; } // Weather icon - left side it.print( 35, 10, id(weather_icon), Color(100, 190, 255), TextAlign::TOP_LEFT, weather_glyph ); // Weather description it.print( 190, 28, id(label_font), Color(180, 220, 255), TextAlign::CENTER, weather_text.c_str() ); // --------------------------------------------- // BIG CENTRALISED CLOCK // --------------------------------------------- it.strftime( 160, 27, id(clock_font), Color(255, 255, 255), TextAlign::TOP_CENTER, "%H:%M", now ); // Date it.strftime( 160, 125, id(date_font), Color(160, 190, 220), TextAlign::TOP_CENTER, "%A, %d %B", now ); // Divider it.line( 10, 150, 310, 150, Color(40, 100, 140) ); // --------------------------------------------- // NETWORK WARNING - only shown when there is // a problem, so the clock normally stays clean.// --------------------------------------------- if (!id(living_room_5g_router).state) { it.print( 160, 143, id(label_font), Color(255, 90, 90), TextAlign::BOTTOM_CENTER, "5G ROUTER DOWN" ); } else if (!id(internet_1_1_1_1).state) { it.print( 160, 143, id(label_font), Color(255, 90, 90), TextAlign::BOTTOM_CENTER, "INTERNET DOWN" ); } else if (!id(living_room_smlight).state) { it.print( 160, 143, id(label_font), Color(255, 180, 0), TextAlign::BOTTOM_CENTER, "ZIGBEE OFFLINE" ); } // --------------------------------------------- // BEDROOM // --------------------------------------------- it.print( 80, 158, id(label_font), Color(120, 200, 255), TextAlign::TOP_CENTER, "BEDROOM" ); if (!isnan(id(bedroom_temperature).state)) { it.printf( 80, 174, id(value_font), Color(255, 255, 255), TextAlign::TOP_CENTER, "%.1f°C", id(bedroom_temperature).state ); } if (!isnan(id(bedroom_humidity).state)) { it.print( 42, 214, id(weather_icon), Color(100, 180, 220), TextAlign::CENTER, "\U000F058E" ); it.printf( 62, 214, id(label_font), Color(100, 180, 220), TextAlign::CENTER_LEFT, "%.0f%%", id(bedroom_humidity).state ); } // --------------------------------------------- // OUTSIDE // --------------------------------------------- it.print( 240, 158, id(label_font), Color(120, 200, 255), TextAlign::TOP_CENTER, "OUTSIDE" ); if (!isnan(id(outside_temperature).state)) { it.printf( 240, 174, id(value_font), Color(255, 255, 255), TextAlign::TOP_CENTER, "%.1f°C", id(outside_temperature).state ); } if (!isnan(id(outside_humidity).state)) { it.print( 202, 214, id(weather_icon), Color(100, 180, 220), TextAlign::CENTER, "\U000F058E" ); it.printf( 222, 214, id(label_font), Color(100, 180, 220), TextAlign::CENTER_LEFT, "%.0f%%", id(outside_humidity).state ); } } // End valid time } // End Page A // ================================================= // PAGE B - HOUSE STATUS // ================================================= else if (page == 1) { it.print( 160, 12, id(weather_font), Color(120, 200, 255), TextAlign::TOP_CENTER, "HOUSE STATUS" ); // --------------------------------------------- // CO2 READINGS // --------------------------------------------- it.print( 80, 55, id(label_font), Color(120, 200, 255), TextAlign::TOP_CENTER, "KITCHEN CO2" ); if (!isnan(id(kitchen_co2).state)) { it.printf( 80, 78, id(value_font), Color(255, 255, 255), TextAlign::TOP_CENTER, "%.0f ppm", id(kitchen_co2).state ); } else { it.print( 80, 78, id(label_font), Color(255, 120, 120), TextAlign::TOP_CENTER, "NO DATA" ); } it.print( 240, 55, id(label_font), Color(120, 200, 255), TextAlign::TOP_CENTER, "GARAGE CO2" ); if (!isnan(id(garage_co2).state)) { it.printf( 240, 78, id(value_font), Color(255, 255, 255), TextAlign::TOP_CENTER, "%.0f ppm", id(garage_co2).state ); } else { it.print( 240, 78, id(label_font), Color(255, 120, 120), TextAlign::TOP_CENTER, "NO DATA" ); } it.line(15, 110, 305, 110, Color(40, 100, 140)); // --------------------------------------------- // NETWORK HEALTH // --------------------------------------------- it.print( 160, 118, id(label_font), Color(120, 200, 255), TextAlign::TOP_CENTER, "NETWORK HEALTH" ); // Internet it.print( 35, 150, id(label_font), id(internet_1_1_1_1).state ? Color(100, 220, 120) : Color(255, 90, 90), TextAlign::CENTER_LEFT, id(internet_1_1_1_1).state ? "OK" : "DOWN" ); it.print( 150, 150, id(label_font), Color(220, 220, 220), TextAlign::CENTER, "INTERNET" ); // Zigbee coordinator it.print( 35, 170, id(label_font), id(living_room_smlight).state ? Color(100, 220, 120) : Color(255, 180, 0), TextAlign::CENTER_LEFT, id(living_room_smlight).state ? "OK" : "OFFLINE" ); it.print( 150, 170, id(label_font), Color(220, 220, 220), TextAlign::CENTER, "ZIGBEE" ); // 5G router it.print( 35, 190, id(label_font), id(living_room_5g_router).state ? Color(100, 220, 120) : Color(255, 90, 90), TextAlign::CENTER_LEFT, id(living_room_5g_router).state ? "OK" : "DOWN" ); it.print( 150, 190, id(label_font), Color(220, 220, 220), TextAlign::CENTER, "5G ROUTER" ); // --------------------------------------------- // EN-SUITE HOT WATER // Kept as a compact fourth status row so it // does not collide with the page indicators.// --------------------------------------------- it.print( 140, 212, id(label_font), Color(220, 220, 220), TextAlign::CENTER, "EN-SUITE HOT WATER" ); it.print( 270, 212, id(label_font), id(ensuite_hot_water).state ? Color(100, 220, 120) : Color(180, 180, 180), TextAlign::CENTER, id(ensuite_hot_water).state ? "ON" : "OFF" ); } // ================================================= // PAGE C - WEATHER // Detailed forecast to be added next.

// ================================================= else if (page == 2) { // ================================================= // PAGE C - DETAILED WEATHER // ================================================= std::string weather = id(weather_condition).state; std::string weather_text = weather; if (weather == "partlycloudy") { weather_text = "PARTLY CLOUDY"; } else if (weather == "clear-night") { weather_text = "CLEAR NIGHT"; } else if (weather == "sunny") { weather_text = "SUNNY"; } else if (weather == "cloudy") { weather_text = "CLOUDY"; } else if (weather == "rainy") { weather_text = "RAIN"; } else if (weather == "pouring") { weather_text = "HEAVY RAIN"; } else if (weather == "fog") { weather_text = "FOG"; } else if (weather == "unavailable" || weather == "unknown" || weather.empty()) { weather_text = "NO WEATHER DATA"; } else { for (auto &c : weather_text) { if (c == '_') c = ' '; if (c == '-') c = ' '; c = toupper(static_cast<unsigned char>(c)); } } it.print( 160, 10, id(weather_font), Color(120, 200, 255), TextAlign::TOP_CENTER, "WEATHER" ); it.print( 160, 48, id(value_font), Color(255, 255, 255), TextAlign::TOP_CENTER, weather_text.c_str() ); it.line( 10, 78, 310, 78, Color(40, 100, 140) ); // OUTSIDE CONDITIONS it.print( 80, 86, id(label_font), Color(120, 200, 255), TextAlign::TOP_CENTER, "OUTSIDE" ); if (!isnan(id(outside_temperature).state)) { it.printf( 80, 106, id(value_font), Color(255, 255, 255), TextAlign::TOP_CENTER, "%.1f°C", id(outside_temperature).state ); } if (!isnan(id(outside_humidity).state)) { it.printf( 80, 136, id(label_font), Color(100, 180, 220), TextAlign::TOP_CENTER, "%.0f%% humidity", id(outside_humidity).state ); } // CLOUD COVER it.print( 240, 86, id(label_font), Color(120, 200, 255), TextAlign::TOP_CENTER, "CLOUD" ); if (!isnan(id(weather_cloud_coverage).state)) { it.printf( 240, 106, id(value_font), Color(255, 255, 255), TextAlign::TOP_CENTER, "%.0f%%", id(weather_cloud_coverage).state ); } it.line( 10, 160, 310, 160, Color(40, 100, 140) ); // WIND it.print( 55, 168, id(label_font), Color(120, 200, 255), TextAlign::TOP_CENTER, "WIND" ); if (!isnan(id(weather_wind_speed).state)) { it.printf( 55, 188, id(label_font), Color(255, 255, 255), TextAlign::TOP_CENTER, "%.1f km/h", id(weather_wind_speed).state ); } if (!isnan(id(weather_wind_bearing).state)) { it.printf( 55, 212, id(label_font), Color(180, 220, 255), TextAlign::TOP_CENTER, "%.0f°", id(weather_wind_bearing).state ); } // UV INDEX it.print( 160, 168, id(label_font), Color(120, 200, 255), TextAlign::TOP_CENTER, "UV INDEX" ); if (!isnan(id(weather_uv_index).state)) { it.printf( 160, 196, id(value_font), Color(255, 255, 255), TextAlign::TOP_CENTER, "%.1f", id(weather_uv_index).state ); } // PRESSURE it.print( 265, 168, id(label_font), Color(120, 200, 255), TextAlign::TOP_CENTER, "PRESSURE" ); if (!isnan(id(weather_pressure).state)) { it.printf( 265, 190, id(label_font), Color(255, 255, 255), TextAlign::TOP_CENTER, "%.0f", id(weather_pressure).state ); it.print( 265, 212, id(label_font), Color(180, 220, 255), TextAlign::TOP_CENTER, "hPa" ); } } // ================================================= // PAGE D - 5G ROUTER ALERT // ================================================= else if (page == 3) { it.filled_rectangle( 0, 0, 320, 48, Color(220, 0, 0) ); it.print( 160, 24, id(value_font), Color(255, 255, 255), TextAlign::CENTER, "!!! ALERT !!!" ); it.print( 160, 82, id(weather_font), Color(255, 40, 40), TextAlign::TOP_CENTER, "5G ROUTER" ); it.print( 160, 120, id(weather_font), Color(255, 40, 40), TextAlign::TOP_CENTER, "DOWN" ); it.line( 25, 168, 295, 168, Color(220, 0, 0) ); it.print( 160, 172, id(label_font), Color(255, 180, 180), TextAlign::TOP_CENTER, "INTERNET CONNECTION" ); it.print( 160, 192, id(label_font), Color(255, 180, 180), TextAlign::TOP_CENTER, "MAY BE UNAVAILABLE" ); it.print( 160, 230, id(label_font), Color(255, 80, 80), TextAlign::BOTTOM_CENTER, "WAITING FOR ROUTER..." ); } and now the Tough… esphome: name: m5-tough-clock friendly_name: M5 Tough Clock esp32: variant: esp32 flash_size: 16MB framework: type: arduino psram: mode: quad speed: 80MHz logger: api: ota: - platform: esphome wifi: networks: - ssid: !secret wifi_ssid password: !secret wifi_password - ssid: !secret wifi_ssid2 password: !secret wifi_password - ssid: !secret wifi_ssid3 password: !secret wifi_password - ssid: !secret wifi_ssid4 password: !secret wifi_password - ssid: !secret wifi_ssid5 password: !secret wifi_password power_save_mode: none post_connect_roaming: true ap: ssid: "M5 Tough Clock Fallback" password: "5uKWDKfAoKjs" captive_portal: # Web interface and OTA update page.# Useful if the display or touchscreen ever stops responding.web_server: version: 3 local: true button: - platform: template name: "M5 Tough Page A - Clock" id: web_page_a on_press: then: - lambda: |- id(current_page) = 0; # Display refreshes automatically every second.

- platform: template name: "M5 Tough Page B - House Status" id: web_page_b on_press: then: - lambda: |- id(current_page) = 1; # Display refreshes automatically every second.- platform: template name: "M5 Tough Page C - Weather" id: web_page_c on_press: then: - lambda: |- id(current_page) = 2; # Display refreshes automatically every second.external_components: - source: github://martydingo/esphome-axp192 components: [axp192] # BLE proxy esp32_ble_tracker: bluetooth_proxy: active: true globals: - id: current_page type: int restore_value: no initial_value: '0' - id: touch_was_down type: bool restore_value: no initial_value: 'false' - id: touch_start_x type: int restore_value: no initial_value: '0' - id: touch_start_y type: int restore_value: no initial_value: '0' - id: touch_last_x type: int restore_value: no initial_value: '0' - id: previous_page type: int restore_value: no initial_value: '0' - id: router_alert_active type: bool restore_value: no initial_value: 'false' # M5 Tough hardware: # I2C: GPIO21/GPIO22 # LCD: ILI9342C on SPI GPIO23/GPIO18, CS GPIO5, DC GPIO15 # The AXP192 component performs the Tough-specific LCD power/reset setup.

i2c: - id: bus_a sda: GPIO21 scl: GPIO22 scan: true # M5 Tough capacitive touchscreen # CHSC6540 detected on I2C address 0x2E.interval: - interval: 50ms then: - lambda: |- uint8_t reg = 0x00; uint8_t data[7]; if (id(bus_a).write_readv( 0x2E, &reg, 1, data, 7 ) == i2c::ERROR_OK) { bool touched = (data[2] != 0); if (touched) { uint16_t x = ((uint16_t)(data[3] & 0x0F) << 8) | data[4]; uint16_t y = ((uint16_t)(data[5] & 0x0F) << 8) | data[6]; // Only act once when the finger first // touches the screen.if (!id(touch_was_down)) { // Only use the bottom third of the screen.

if (y >= 160 && !id(router_alert_active)) { // BOTTOM LEFT THIRD = previous page.if (x < 107) { id(current_page)--; if (id(current_page) < 0) { id(current_page) = 2; } } // BOTTOM RIGHT THIRD = next page.else if (x >= 213) { id(current_page)++; if (id(current_page) > 2) { id(current_page) = 0; } } // BOTTOM CENTRE THIRD = dim/full brightness.

else { static bool dimmed = false; uint8_t axp_reg = 0x28; uint8_t axp_data = 0x00; if (id(bus_a).write_readv( 0x34, &axp_reg, 1, &axp_data, 1 ) == i2c::ERROR_OK) { dimmed = !dimmed; // Full brightness = 3.0V // Dim brightness = 2.6V axp_data = (axp_data & 0xF0) | (dimmed ? 0x08 : 0x0C); uint8_t output[2] = { 0x28, axp_data }; id(bus_a).write( 0x34, output, 2 ); } } } } id(touch_was_down) = true; } else { id(touch_was_down) = false; } } binary_sensor: - platform: gpio name: "M5 Tough Touch Interrupt" id: tough_touch_interrupt pin: number: GPIO39 mode: input: true on_press: then: - logger.log: "GPIO39 TOUCH DETECTED" - platform: status name: "M5 Tough Online" id: tough_online entity_category: diagnostic # Network health monitors from Home Assistant - platform: homeassistant id: living_room_5g_router name: "5G Router" entity_id: binary_sensor.living_room_5g_router entity_category: diagnostic on_state: then: - lambda: |- ESP_LOGI( "ha_network", "living_room_5g_router = %s", x ? "ON" : "OFF" ); // Router has gone down.if (!x && !id(router_alert_active)) { // Remember whichever normal page was being viewed.id(previous_page) = id(current_page); // Activate the alert page.

id(router_alert_active) = true; id(current_page) = 3; ESP_LOGI( "router_alert", "5G router DOWN - switching to alert page" ); } // Router has recovered.else if (x && id(router_alert_active)) { // Return to the page that was being viewed.id(current_page) = id(previous_page); // Clear the alert.

id(router_alert_active) = false; ESP_LOGI( "router_alert", "5G router restored - returning to previous page" ); } // Router has gone down.if (!x && !id(router_alert_active)) { // Remember whichever normal page was being viewed.id(previous_page) = id(current_page); // Activate the alert page.

id(router_alert_active) = true; id(current_page) = 3; ESP_LOGI( "router_alert", "5G router DOWN - switching to alert page" ); } // Router has recovered.else if (x && id(router_alert_active)) { // Return to the page that was being viewed.id(current_page) = id(previous_page); // Clear the alert.

id(router_alert_active) = false; ESP_LOGI( "router_alert", "5G router restored - returning to previous page" ); } - platform: homeassistant id: internet_1_1_1_1 name: "Internet 1.1.1.1" entity_id: binary_sensor.1_1_1_1 entity_category: diagnostic on_state: then: - lambda: |- ESP_LOGI("ha_network", "internet_1_1_1_1 = %s", x ? "ON" : "OFF"); - platform: homeassistant id: living_room_smlight name: "Living Room SMLIGHT" entity_id: binary_sensor.living_room_smlight entity_category: diagnostic on_state: then: - lambda: |- ESP_LOGI("ha_network", "living_room_smlight = %s", x ? "ON" : "OFF"); # En-suite bathroom hot water heater - platform: homeassistant id: ensuite_hot_water name: "En-suite Hot Water" entity_id: switch.athom_pg01_v2_2 spi: clk_pin: GPIO18 mosi_pin: GPIO23 sensor: # Diagnostics - platform: wifi_signal name: "M5 Tough WiFi Signal" id: core2_wifi_signal update_interval: 60s entity_category: diagnostic - platform: uptime name: "M5 Tough Uptime" id: core2_uptime entity_category: diagnostic - platform: template name: "M5 Tough Current Page" id: core2_current_page accuracy_decimals: 0 update_interval: 1s entity_category: diagnostic lambda: |- return id(current_page) + 1; # AXP192 power/display initialisation for the M5 Tough.- platform: axp192 model: M5TOUGH address: 0x34 i2c_id: bus_a id: axp192_id update_interval: 60s # Bedroom - platform: homeassistant id: bedroom_temperature name: "Bedroom Temperature" entity_id: sensor.airguard_th_temperature on_value: then: - lambda: |- ESP_LOGI("ha_data", "bedroom_temperature = %.2f", x); - platform: homeassistant id: bedroom_humidity name: "Bedroom Humidity" entity_id: sensor.airguard_th_humidity on_value: then: - lambda: |- ESP_LOGI("ha_data", "bedroom_humidity = %.2f", x); # Outside - platform: homeassistant id: outside_temperature name: "Outside Temperature" entity_id: sensor.outdoor_meter_65_temperature on_value: then: - lambda: |- ESP_LOGI("ha_data", "outside_temperature = %.2f", x); - platform: homeassistant id: outside_humidity name: "Outside Humidity" entity_id: sensor.outdoor_meter_65_humidity on_value: then: - lambda: |- ESP_LOGI("ha_data", "outside_humidity = %.2f", x); # Met.no weather details - platform: homeassistant id: weather_wind_speed name: "Weather Wind Speed" entity_id: weather.forecast_home attribute: wind_speed - platform: homeassistant id: weather_wind_bearing name: "Weather Wind Bearing" entity_id: weather.forecast_home attribute: wind_bearing - platform: homeassistant id: weather_cloud_coverage name: "Weather Cloud Coverage" entity_id: weather.forecast_home attribute: cloud_coverage - platform: homeassistant id: weather_uv_index name: "Weather UV Index" entity_id: weather.forecast_home attribute: uv_index - platform: homeassistant id: weather_pressure name: "Weather Pressure" entity_id: weather.forecast_home attribute: pressure # Kitchen CO₂ - platform: homeassistant id: kitchen_co2 name: "Kitchen CO2" entity_id: sensor.meter_pro_co2_monitor_d6_carbon_dioxide on_value: then: - lambda: |- ESP_LOGI("ha_data", "kitchen_co2 = %.2f", x); # Garage CO₂ - platform: homeassistant id: garage_co2 name: "Garage CO2" entity_id: sensor.meter_pro_co2_monitor_new_carbon_dioxide on_value: then: - lambda: |- ESP_LOGI("ha_data", "garage_co2 = %.2f", x); text_sensor: # Diagnostics visible in the web interface - platform: wifi_info ip_address: name: "M5 Tough IP Address" id: core2_ip_address entity_category: diagnostic ssid: name: "M5 Tough WiFi SSID" id: tough_wifi_ssid entity_category: diagnostic - platform: homeassistant id: weather_condition name: "Weather Condition" entity_id: weather.forecast_home on_value: then: - lambda: |- ESP_LOGI("ha_data", "weather_condition = %s", x.c_str()); - platform: template id: network_status name: "Network Status" update_interval: 10s lambda: |- // Diagnostic note: these are Home Assistant entity states.// Page C shows each raw input separately.

if (!id(living_room_5g_router).state) { return {"5G ROUTER DOWN"}; } else if (!id(internet_1_1_1_1).state) { return {"INTERNET DOWN"}; } else if (!id(living_room_smlight).state) { return {"ZIGBEE OFFLINE"}; } else { return {"NETWORK OK"}; } time: - platform: sntp id: clock_time timezone: Europe/Madrid servers: - time.google.com - time.cloudflare.com - pool.ntp.org update_interval: 1h font: - file: "gfonts://Roboto" id: clock_font size: 96 - file: "gfonts://Roboto" id: date_font size: 18 - file: "gfonts://Roboto" id: label_font size: 16 - file: "gfonts://Roboto" id: value_font size: 26 - file: "gfonts://Roboto" id: weather_font size: 36 - file: "fonts/materialdesignicons-webfont.ttf" id: weather_icon size: 32 glyphs: - "\U000F0599" # sunny - "\U000F0590" # cloudy - "\U000F0595" # partly cloudy - "\U000F0597" # rainy - "\U000F0596" # pouring - "\U000F0593" # lightning - "\U000F067E" # lightning rainy - "\U000F0594" # clear night - "\U000F0591" # fog - "\U000F058E" # water / humidity display: - platform: ili9xxx id: tough_display # The M5STACK model uses the M5-specific ILI9342C initialisation.# This is the current ili9xxx equivalent of the configuration # recommended by the M5 Tough AXP192 component.model: M5STACK cs_pin: GPIO5 dc_pin: GPIO15 # The Tough photo showed normal geometry but mirrored text.

# Start with the M5STACK model's native orientation rather than # layering another transform over the controller initialisation.invert_colors: true color_order: BGR auto_clear_enabled: true data_rate: 40MHz update_interval: 1s lambda: |- // Background it.fill(Color(5, 8, 20)); int page = id(current_page); // ================================================= // PAGE A - MAIN CLOCK // ================================================= if (page == 0) { auto now = id(clock_time).now(); if (!now.is_valid()) { it.print( 160, 120, id(date_font), Color(255, 255, 255), TextAlign::CENTER, "WAITING FOR TIME" ); } else { // --------------------------------------------- // WEATHER // --------------------------------------------- std::string weather = id(weather_condition).state; std::string weather_text = weather; const char *weather_glyph = "\U000F0590"; // Convert Home Assistant weather states into readable text.if (weather == "sunny") { weather_text = "SUNNY"; weather_glyph = "\U000F0599"; } else if (weather == "partlycloudy") { weather_text = "PARTLY CLOUDY"; weather_glyph = "\U000F0595"; } else if (weather == "cloudy") { weather_text = "CLOUDY"; weather_glyph = "\U000F0590"; } else if (weather == "rainy") { weather_text = "RAIN"; weather_glyph = "\U000F0597"; } else if (weather == "pouring") { weather_text = "HEAVY RAIN"; weather_glyph = "\U000F0596"; } else if (weather == "lightning") { weather_text = "STORM"; weather_glyph = "\U000F0593"; } else if (weather == "lightning-rainy") { weather_text = "STORM"; weather_glyph = "\U000F067E"; } else if (weather == "clear-night") { weather_text = "CLEAR NIGHT"; weather_glyph = "\U000F0594"; } else if (weather == "fog") { weather_text = "FOG"; weather_glyph = "\U000F0591"; } else if (weather == "unknown" || weather == "unavailable" || weather.empty()) { weather_text = "NO WEATHER DATA"; } else { // Show the actual Home Assistant state rather than the old // hard-coded fallback of "WEATHER".

for (auto &c : weather_text) { if (c == '-') c = ' '; } for (auto &c : weather_text) { c = toupper(static_cast<unsigned char>(c)); } } // Weather icon - left side it.print( 35, 8, id(weather_icon), Color(100, 190, 255), TextAlign::TOP_LEFT, weather_glyph ); // Weather description - kept above the clock.it.print( 190, 8, id(label_font), Color(180, 220, 255), TextAlign::TOP_CENTER, weather_text.c_str() ); // --------------------------------------------- // BIG CENTRALISED CLOCK // --------------------------------------------- it.strftime( 160, 32, id(clock_font), Color(255, 255, 255), TextAlign::TOP_CENTER, "%H:%M", now ); // Date it.strftime( 160, 128, id(date_font), Color(160, 190, 220), TextAlign::TOP_CENTER, "%A, %d %B", now ); // Divider it.line( 10, 150, 310, 150, Color(40, 100, 140) ); // Network health is shown on the House Status page.// Keeping the main clock page clear avoids overlap with the date.

// --------------------------------------------- // BEDROOM // --------------------------------------------- it.print( 80, 158, id(label_font), Color(120, 200, 255), TextAlign::TOP_CENTER, "BEDROOM" ); if (!isnan(id(bedroom_temperature).state)) { it.printf( 80, 174, id(value_font), Color(255, 255, 255), TextAlign::TOP_CENTER, "%.1f°C", id(bedroom_temperature).state ); } if (!isnan(id(bedroom_humidity).state)) { it.print( 42, 214, id(weather_icon), Color(100, 180, 220), TextAlign::CENTER, "\U000F058E" ); it.printf( 62, 214, id(label_font), Color(100, 180, 220), TextAlign::CENTER_LEFT, "%.0f%%", id(bedroom_humidity).state ); } // --------------------------------------------- // OUTSIDE // --------------------------------------------- it.print( 240, 158, id(label_font), Color(120, 200, 255), TextAlign::TOP_CENTER, "OUTSIDE" ); if (!isnan(id(outside_temperature).state)) { it.printf( 240, 174, id(value_font), Color(255, 255, 255), TextAlign::TOP_CENTER, "%.1f°C", id(outside_temperature).state ); } if (!isnan(id(outside_humidity).state)) { it.print( 202, 214, id(weather_icon), Color(100, 180, 220), TextAlign::CENTER, "\U000F058E" ); it.printf( 222, 214, id(label_font), Color(100, 180, 220), TextAlign::CENTER_LEFT, "%.0f%%", id(outside_humidity).state ); } } // End valid time } // End Page A // ================================================= // PAGE B - HOUSE STATUS // ================================================= else if (page == 1) { it.print( 160, 12, id(weather_font), Color(120, 200, 255), TextAlign::TOP_CENTER, "HOUSE STATUS" ); // --------------------------------------------- // CO2 READINGS // --------------------------------------------- it.print( 80, 55, id(label_font), Color(120, 200, 255), TextAlign::TOP_CENTER, "KITCHEN CO2" ); if (!isnan(id(kitchen_co2).state)) { it.printf( 80, 78, id(value_font), Color(255, 255, 255), TextAlign::TOP_CENTER, "%.0f ppm", id(kitchen_co2).state ); } else { it.print( 80, 78, id(label_font), Color(255, 120, 120), TextAlign::TOP_CENTER, "NO DATA" ); } it.print( 240, 55, id(label_font), Color(120, 200, 255), TextAlign::TOP_CENTER, "GARAGE CO2" ); if (!isnan(id(garage_co2).state)) { it.printf( 240, 78, id(value_font), Color(255, 255, 255), TextAlign::TOP_CENTER, "%.0f ppm", id(garage_co2).state ); } else { it.print( 240, 78, id(label_font), Color(255, 120, 120), TextAlign::TOP_CENTER, "NO DATA" ); } it.line(15, 110, 305, 110, Color(40, 100, 140)); // --------------------------------------------- // NETWORK HEALTH // --------------------------------------------- it.print( 160, 118, id(label_font), Color(120, 200, 255), TextAlign::TOP_CENTER, "NETWORK HEALTH" ); // Internet it.print( 35, 150, id(label_font), id(internet_1_1_1_1).state ? Color(100, 220, 120) : Color(255, 90, 90), TextAlign::CENTER_LEFT, id(internet_1_1_1_1).state ? "OK" : "DOWN" ); it.print( 150, 150, id(label_font), Color(220, 220, 220), TextAlign::CENTER, "INTERNET" ); // Zigbee coordinator it.print( 35, 170, id(label_font), id(living_room_smlight).state ? Color(100, 220, 120) : Color(255, 180, 0), TextAlign::CENTER_LEFT, id(living_room_smlight).state ? "OK" : "OFFLINE" ); it.print( 150, 170, id(label_font), Color(220, 220, 220), TextAlign::CENTER, "ZIGBEE" ); // 5G router it.print( 35, 190, id(label_font), id(living_room_5g_router).state ? Color(100, 220, 120) : Color(255, 90, 90), TextAlign::CENTER_LEFT, id(living_room_5g_router).state ? "OK" : "DOWN" ); it.print( 150, 190, id(label_font), Color(220, 220, 220), TextAlign::CENTER, "5G ROUTER" ); // --------------------------------------------- // EN-SUITE HOT WATER // Kept as a compact fourth status row so it // does not collide with the page indicators.// --------------------------------------------- it.print( 140, 212, id(label_font), Color(220, 220, 220), TextAlign::CENTER, "EN-SUITE HOT WATER" ); it.print( 270, 212, id(label_font), id(ensuite_hot_water).state ? Color(100, 220, 120) : Color(180, 180, 180), TextAlign::CENTER, id(ensuite_hot_water).state ? "ON" : "OFF" ); } // ================================================= // PAGE C - WEATHER // ================================================= else if (page == 2) { it.print( 160, 8, id(weather_font), Color(120, 200, 255), TextAlign::TOP_CENTER, "WEATHER" ); std::string weather = id(weather_condition).state; std::string weather_text = weather; if (weather == "partlycloudy") { weather_text = "PARTLY CLOUDY"; } else if (weather == "clear-night") { weather_text = "CLEAR NIGHT"; } else if (weather == "lightning-rainy") { weather_text = "STORM"; } else if (weather == "sunny") { weather_text = "SUNNY"; } else if (weather == "cloudy") { weather_text = "CLOUDY"; } else if (weather == "rainy") { weather_text = "RAIN"; } else if (weather == "pouring") { weather_text = "HEAVY RAIN"; } else if (weather == "fog") { weather_text = "FOG"; } else if (weather == "unavailable" || weather == "unknown" || weather.empty()) { weather_text = "NO WEATHER DATA"; } else { for (auto &c : weather_text) { if (c == '_') c = ' '; if (c == '-') c = ' '; c = toupper(static_cast<unsigned char>(c)); } } it.print( 160, 48, id(value_font), Color(255, 255, 255), TextAlign::TOP_CENTER, weather_text.c_str() ); it.line( 10, 78, 310, 78, Color(40, 100, 140) ); // LOCAL OUTSIDE CONDITIONS it.print( 80, 86, id(label_font), Color(120, 200, 255), TextAlign::TOP_CENTER, "OUTSIDE" ); if (!isnan(id(outside_temperature).state)) { it.printf( 80, 106, id(value_font), Color(255, 255, 255), TextAlign::TOP_CENTER, "%.1f°C", id(outside_temperature).state ); } if (!isnan(id(outside_humidity).state)) { it.printf( 80, 136, id(label_font), Color(100, 180, 220), TextAlign::TOP_CENTER, "%.0f%% humidity", id(outside_humidity).state ); } // MET.NO CLOUD COVERAGE it.print( 240, 86, id(label_font), Color(120, 200, 255), TextAlign::TOP_CENTER, "CLOUD" ); if (!isnan(id(weather_cloud_coverage).state)) { it.printf( 240, 106, id(value_font), Color(255, 255, 255), TextAlign::TOP_CENTER, "%.0f%%", id(weather_cloud_coverage).state ); } it.line( 10, 160, 310, 160, Color(40, 100, 140) ); // WIND it.print( 55, 168, id(label_font), Color(120, 200, 255), TextAlign::TOP_CENTER, "WIND" ); if (!isnan(id(weather_wind_speed).state)) { it.printf( 55, 188, id(label_font), Color(255, 255, 255), TextAlign::TOP_CENTER, "%.1f km/h", id(weather_wind_speed).state ); } if (!isnan(id(weather_wind_bearing).state)) { it.printf( 55, 212, id(label_font), Color(180, 220, 255), TextAlign::TOP_CENTER, "%.0f°", id(weather_wind_bearing).state ); } // UV INDEX it.print( 160, 168, id(label_font), Color(120, 200, 255), TextAlign::TOP_CENTER, "UV INDEX" ); if (!isnan(id(weather_uv_index).state)) { it.printf( 160, 196, id(value_font), Color(255, 255, 255), TextAlign::TOP_CENTER, "%.1f", id(weather_uv_index).state ); } // PRESSURE it.print( 265, 168, id(label_font), Color(120, 200, 255), TextAlign::TOP_CENTER, "PRESSURE" ); if (!isnan(id(weather_pressure).state)) { it.printf( 265, 190, id(label_font), Color(255, 255, 255), TextAlign::TOP_CENTER, "%.0f", id(weather_pressure).state ); it.print( 265, 212, id(label_font), Color(180, 220, 255), TextAlign::TOP_CENTER, "hPa" ); } } // ================================================= // PAGE D - 5G ROUTER ALERT // ================================================= else if (page == 3) { // Top red alert banner.it.filled_rectangle( 0, 0, 320, 48, Color(220, 0, 0) ); it.print( 160, 24, id(value_font), Color(255, 255, 255), TextAlign::CENTER, "!!! ALERT !!!" ); // Main warning.

it.print( 160, 82, id(weather_font), Color(255, 40, 40), TextAlign::TOP_CENTER, "5G ROUTER" ); it.print( 160, 120, id(weather_font), Color(255, 40, 40), TextAlign::TOP_CENTER, "DOWN" ); // Warning divider.it.line( 25, 168, 295, 168, Color(220, 0, 0) ); it.print( 160, 172, id(label_font), Color(255, 180, 180), TextAlign::TOP_CENTER, "INTERNET CONNECTION" ); it.print( 160, 192, id(label_font), Color(255, 180, 180), TextAlign::TOP_CENTER, "MAY BE UNAVAILABLE" ); // Bottom status message.it.print( 160, 230, id(label_font), Color(255, 80, 80), TextAlign::BOTTOM_CENTER, "WAITING FOR ROUTER..." ); }

Read More
Related Posts