mirror of
https://github.com/randybb/esphome-configs.git
synced 2026-10-14 16:35:12 +02:00
mcu-m5paper: rework on M5Stack components, battery mode with forecast
Rewrite of the old config on the core ESPHome components (it8951, gt911, bm8563, sht3xd): - USB: always on, forecast screen redrawn every 15 min, forecast refreshed every hour and when Home Assistant connects - battery: BM8563 timer wakes the board every 15 min (off 1:00-5:00), it draws the stored forecast and cuts power; WiFi only when the forecast is older than 6 h - mode detected by trying to power off; battery voltage drop on USB triggers the battery cycle - hourly forecast via weather.get_forecasts, 48 h kept in the FM24C02 EEPROM (include/m5paper_weather.h) - OTA image confirmed before power-off, battery wake-ups are shorter than boot_is_good_after - status screen with "Made for ESPHome" logo, IPv6 addresses first
This commit is contained in:
1 parent
d813717d2c
commit
329830e553
4 files changed
+742
-75
No files matched your search
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" id="Layer_2" data-name="Layer 2" viewBox="0 0 681.98 201.98"><defs><style>.cls-1{fill:none;stroke:#1d2126;stroke-miterlimit:10;stroke-width:1.01px}.cls-2{fill:#1d2126}.cls-2,.cls-3,.cls-4{stroke-width:0}.cls-3{fill:#f2f4f9}.cls-4{fill:#18bcf2}</style></defs><g id="Layer_1-2" data-name="Layer 1"><g><g><rect width="680.97" height="200.97" x=".51" y=".51" class="cls-3" rx="40.48" ry="40.48"/><rect width="680.97" height="200.97" x=".51" y=".51" class="cls-1" rx="40.48" ry="40.48"/></g><g><path d="M236.13,43.11v26.53h-5.29v-14.5l-5.68,14.5h-5.08l-5.61-14.4v14.4h-5.12v-26.53h5.12l8.16,20.27,8.21-20.27h5.29Z" class="cls-4"/><path d="M259.54,63.84h-9.04l-2.02,5.8h-5.59l9.51-26.53h5.33l9.47,26.53h-5.7l-1.97-5.8ZM258,59.34l-2.95-8.7-2.99,8.7h5.94Z" class="cls-4"/><path d="M296.93,56.42c.02,1.89-.3,3.66-.98,5.31s-1.61,3.06-2.8,4.22-2.61,2.07-4.26,2.73-3.4.98-5.26.96h-9.65v-26.53h9.65c2.43-.04,4.66.52,6.71,1.68s3.67,2.76,4.86,4.82,1.78,4.33,1.74,6.81ZM291.25,56.42c0-2.46-.71-4.46-2.12-5.99s-3.27-2.29-5.58-2.29h-4.22v16.52h4.22c2.34,0,4.21-.75,5.61-2.26s2.09-3.5,2.09-5.99Z" class="cls-4"/><path d="M321.38,64.59v5.04h-16.7v-26.53h16.7v5.1h-11.34v5.66h10.07v4.75h-10.07v5.98h11.34Z" class="cls-4"/><path d="M348.35,48.17v6.1h9.58l-.04,4.82h-9.54v10.55h-5.36v-26.53h16.73l-.04,5.06h-11.34Z" class="cls-4"/><path d="M378.62,42.64c1.9,0,3.66.34,5.27,1.03s2.99,1.63,4.11,2.84,2,2.66,2.63,4.35.94,3.52.94,5.49c.02,1.95-.28,3.77-.91,5.48s-1.52,3.17-2.65,4.39-2.52,2.17-4.15,2.86-3.39,1.03-5.27,1c-2.5.04-4.74-.54-6.72-1.72s-3.52-2.82-4.6-4.92-1.6-4.45-1.56-7.05c-.02-1.95.28-3.77.91-5.48s1.51-3.18,2.65-4.39,2.51-2.17,4.13-2.87,3.37-1.03,5.23-1ZM378.65,64.82c2.24,0,4.01-.76,5.32-2.28s1.96-3.58,1.96-6.2-.65-4.71-1.95-6.22-3.08-2.27-5.33-2.27-4.03.76-5.33,2.27-1.95,3.59-1.95,6.22.65,4.71,1.95,6.21,3.08,2.26,5.33,2.26Z" class="cls-4"/><path d="M419.5,69.64h-6.01l-4.08-9.58h-4.89v9.58h-5.36v-26.53h10.21c3.28.01,5.76.74,7.44,2.19s2.51,3.55,2.51,6.3c0,1.79-.39,3.34-1.17,4.65s-1.89,2.28-3.33,2.91l4.68,10.48ZM413.63,51.6c0-1.15-.36-2.05-1.07-2.72s-1.78-.99-3.18-.99h-4.85v7.49h4.85c1.41,0,2.47-.34,3.18-1.01s1.07-1.6,1.07-2.77Z" class="cls-4"/></g><g><path d="M251.92,146.83v11.25h-43.5v-70.08h43.5v11.34h-31.59v18.14h28.22v10.55h-28.22v18.8h31.59Z" class="cls-2"/><path d="M285,86.74c6.41,0,11.73,1.56,15.98,4.69,4.25,3.12,6.86,7.3,7.83,12.52l-11.2,3.05c-.62-3-2.09-5.31-4.38-6.94-2.3-1.62-5.13-2.44-8.51-2.44-3.62,0-6.5.87-8.62,2.6s-3.19,4.04-3.19,6.91c0,4.53,2.81,7.47,8.44,8.81l11.44,2.86c5.91,1.53,10.33,3.96,13.27,7.29s4.41,7.43,4.41,12.3c0,6.25-2.27,11.3-6.8,15.16-4.53,3.86-10.59,5.79-18.19,5.79-6.84,0-12.62-1.61-17.34-4.83-4.66-3.34-7.41-7.75-8.25-13.22l11.2-2.91c.41,3.09,1.95,5.52,4.62,7.27,2.67,1.75,6.07,2.62,10.2,2.62s7.09-.84,9.35-2.51c2.27-1.67,3.4-3.94,3.4-6.82,0-4.5-2.81-7.47-8.44-8.91l-11.44-2.77c-5.88-1.34-10.29-3.73-13.24-7.15s-4.43-7.6-4.43-12.54c0-6.19,2.2-11.21,6.59-15.07,4.39-3.86,10.16-5.79,17.32-5.79Z" class="cls-2"/><path d="M371.55,110.83c0,6.88-2.09,12.44-6.26,16.69-4.17,4.25-9.79,6.38-16.85,6.38h-15.75v24.19h-11.91v-70.08h27.75c7.12,0,12.74,2.06,16.85,6.16,4.11,4.11,6.16,9.66,6.16,16.66ZM359.08,110.55c0-3.53-1.1-6.4-3.3-8.6s-5.32-3.3-9.35-3.3h-13.73v24.8h13.55c4.12,0,7.3-1.17,9.52-3.52s3.33-5.47,3.33-9.38Z" class="cls-2"/><path d="M381.23,88h11.91v29.3l30.89.09v-29.39h12v70.08h-12v-30.14l-30.89-.09v30.23h-11.91v-70.08Z" class="cls-2"/><path d="M470.88,109.43c7.25,0,13.16,2.33,17.72,6.98,4.56,4.66,6.84,10.64,6.84,17.95s-2.28,13.25-6.84,17.91c-4.56,4.66-10.47,6.98-17.72,6.98s-13.25-2.33-17.81-6.98c-4.56-4.66-6.84-10.62-6.84-17.91s2.28-13.34,6.84-17.98,10.5-6.96,17.81-6.96ZM470.88,149.04c3.88,0,7.02-1.37,9.45-4.1,2.42-2.73,3.63-6.29,3.63-10.66s-1.21-7.91-3.63-10.62c-2.42-2.7-5.57-4.05-9.45-4.05s-7.17,1.35-9.61,4.05c-2.44,2.7-3.66,6.24-3.66,10.62s1.22,7.93,3.66,10.66c2.44,2.73,5.64,4.1,9.61,4.1Z" class="cls-2"/><path d="M576.42,127.94v30.14h-11.02v-28.41c0-3.28-.84-5.84-2.53-7.69-1.69-1.84-3.98-2.77-6.89-2.77-3.09,0-5.56,1.01-7.41,3.02s-2.77,4.84-2.77,8.46v27.38h-11.16v-28.41c0-3.28-.82-5.84-2.46-7.69-1.64-1.84-3.91-2.77-6.82-2.77-3.09,0-5.58,1.01-7.45,3.02s-2.81,4.84-2.81,8.46v27.38h-11.34v-47.34h10.55l.38,4.55c2.75-4.03,7.23-6.05,13.45-6.05,3.62,0,6.77.75,9.42,2.25s4.67,3.69,6.05,6.56c1.16-2.75,3.07-4.91,5.74-6.47,2.67-1.56,5.85-2.34,9.54-2.34,5.38,0,9.64,1.66,12.8,4.97,3.16,3.31,4.73,7.89,4.73,13.73Z" class="cls-2"/><path d="M631.43,138.58h-36.28c.38,3.56,1.72,6.28,4.03,8.16s5.25,2.81,8.81,2.81c5.78,0,9.83-2.41,12.14-7.22l9.47,3.75c-1.78,4.16-4.59,7.41-8.41,9.75s-8.23,3.52-13.2,3.52c-7,0-12.68-2.29-17.04-6.87-4.36-4.58-6.54-10.59-6.54-18.02s2.2-13.48,6.59-18.14c4.39-4.66,10.13-6.98,17.23-6.98s12.58,2.3,16.83,6.89,6.38,10.64,6.38,18.14v4.22ZM595.34,130.61h24.56c-.22-3.84-1.38-6.77-3.49-8.79s-4.95-3.02-8.51-3.02-6.41,1.02-8.62,3.07c-2.22,2.05-3.53,4.96-3.94,8.74Z" class="cls-2"/></g><g><path d="M170.55,153.49c0,4.12-3.38,7.5-7.5,7.5H58.05c-4.12,0-7.5-3.38-7.5-7.5v-45c0-4.12,2.39-9.89,5.3-12Line truncated
|
||||
|
After Width: | Height: | Size: 5.3 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" id="Layer_2" data-name="Layer 2" viewBox="0 0 681.98 201.98"><defs><style>.cls-1{fill:#1d2126}.cls-1,.cls-2,.cls-3{stroke-width:0}.cls-2{fill:#f2f4f9}.cls-3{fill:#18bcf2}.cls-4{fill:none;stroke:#f2f4f9;stroke-miterlimit:10;stroke-width:1.01px}</style></defs><g id="Layer_1-2" data-name="Layer 1"><g><g><rect width="680.97" height="200.97" x=".51" y=".51" class="cls-1" rx="40.48" ry="40.48"/><rect width="680.97" height="200.97" x=".51" y=".51" class="cls-4" rx="40.48" ry="40.48"/></g><g><path d="M236.13,43.11v26.53h-5.29v-14.5l-5.68,14.5h-5.08l-5.61-14.4v14.4h-5.12v-26.53h5.12l8.16,20.27,8.21-20.27h5.29Z" class="cls-3"/><path d="M259.54,63.84h-9.04l-2.02,5.8h-5.59l9.51-26.53h5.33l9.47,26.53h-5.7l-1.97-5.8ZM258,59.34l-2.95-8.7-2.99,8.7h5.94Z" class="cls-3"/><path d="M296.93,56.42c.02,1.89-.3,3.66-.98,5.31s-1.61,3.06-2.8,4.22-2.61,2.07-4.26,2.73-3.4.98-5.26.96h-9.65v-26.53h9.65c2.43-.04,4.66.52,6.71,1.68s3.67,2.76,4.86,4.82,1.78,4.33,1.74,6.81ZM291.25,56.42c0-2.46-.71-4.46-2.12-5.99s-3.27-2.29-5.58-2.29h-4.22v16.52h4.22c2.34,0,4.21-.75,5.61-2.26s2.09-3.5,2.09-5.99Z" class="cls-3"/><path d="M321.38,64.59v5.04h-16.7v-26.53h16.7v5.1h-11.34v5.66h10.07v4.75h-10.07v5.98h11.34Z" class="cls-3"/><path d="M348.35,48.17v6.1h9.58l-.04,4.82h-9.54v10.55h-5.36v-26.53h16.73l-.04,5.06h-11.34Z" class="cls-3"/><path d="M378.62,42.64c1.9,0,3.66.34,5.27,1.03s2.99,1.63,4.11,2.84,2,2.66,2.63,4.35.94,3.52.94,5.49c.02,1.95-.28,3.77-.91,5.48s-1.52,3.17-2.65,4.39-2.52,2.17-4.15,2.86-3.39,1.03-5.27,1c-2.5.04-4.74-.54-6.72-1.72s-3.52-2.82-4.6-4.92-1.6-4.45-1.56-7.05c-.02-1.95.28-3.77.91-5.48s1.51-3.18,2.65-4.39,2.51-2.17,4.13-2.87,3.37-1.03,5.23-1ZM378.65,64.82c2.24,0,4.01-.76,5.32-2.28s1.96-3.58,1.96-6.2-.65-4.71-1.95-6.22-3.08-2.27-5.33-2.27-4.03.76-5.33,2.27-1.95,3.59-1.95,6.22.65,4.71,1.95,6.21,3.08,2.26,5.33,2.26Z" class="cls-3"/><path d="M419.5,69.64h-6.01l-4.08-9.58h-4.89v9.58h-5.36v-26.53h10.21c3.28.01,5.76.74,7.44,2.19s2.51,3.55,2.51,6.3c0,1.79-.39,3.34-1.17,4.65s-1.89,2.28-3.33,2.91l4.68,10.48ZM413.63,51.6c0-1.15-.36-2.05-1.07-2.72s-1.78-.99-3.18-.99h-4.85v7.49h4.85c1.41,0,2.47-.34,3.18-1.01s1.07-1.6,1.07-2.77Z" class="cls-3"/></g><g><path d="M251.92,146.83v11.25h-43.5v-70.08h43.5v11.34h-31.59v18.14h28.22v10.55h-28.22v18.8h31.59Z" class="cls-2"/><path d="M285,86.74c6.41,0,11.73,1.56,15.98,4.69,4.25,3.12,6.86,7.3,7.83,12.52l-11.2,3.05c-.62-3-2.09-5.31-4.38-6.94-2.3-1.62-5.13-2.44-8.51-2.44-3.62,0-6.5.87-8.62,2.6s-3.19,4.04-3.19,6.91c0,4.53,2.81,7.47,8.44,8.81l11.44,2.86c5.91,1.53,10.33,3.96,13.27,7.29s4.41,7.43,4.41,12.3c0,6.25-2.27,11.3-6.8,15.16-4.53,3.86-10.59,5.79-18.19,5.79-6.84,0-12.62-1.61-17.34-4.83-4.66-3.34-7.41-7.75-8.25-13.22l11.2-2.91c.41,3.09,1.95,5.52,4.62,7.27,2.67,1.75,6.07,2.62,10.2,2.62s7.09-.84,9.35-2.51c2.27-1.67,3.4-3.94,3.4-6.82,0-4.5-2.81-7.47-8.44-8.91l-11.44-2.77c-5.88-1.34-10.29-3.73-13.24-7.15s-4.43-7.6-4.43-12.54c0-6.19,2.2-11.21,6.59-15.07,4.39-3.86,10.16-5.79,17.32-5.79Z" class="cls-2"/><path d="M371.55,110.83c0,6.88-2.09,12.44-6.26,16.69-4.17,4.25-9.79,6.38-16.85,6.38h-15.75v24.19h-11.91v-70.08h27.75c7.12,0,12.74,2.06,16.85,6.16,4.11,4.11,6.16,9.66,6.16,16.66ZM359.08,110.55c0-3.53-1.1-6.4-3.3-8.6s-5.32-3.3-9.35-3.3h-13.73v24.8h13.55c4.12,0,7.3-1.17,9.52-3.52s3.33-5.47,3.33-9.38Z" class="cls-2"/><path d="M381.23,88h11.91v29.3l30.89.09v-29.39h12v70.08h-12v-30.14l-30.89-.09v30.23h-11.91v-70.08Z" class="cls-2"/><path d="M470.88,109.43c7.25,0,13.16,2.33,17.72,6.98,4.56,4.66,6.84,10.64,6.84,17.95s-2.28,13.25-6.84,17.91c-4.56,4.66-10.47,6.98-17.72,6.98s-13.25-2.33-17.81-6.98c-4.56-4.66-6.84-10.62-6.84-17.91s2.28-13.34,6.84-17.98,10.5-6.96,17.81-6.96ZM470.88,149.04c3.88,0,7.02-1.37,9.45-4.1,2.42-2.73,3.63-6.29,3.63-10.66s-1.21-7.91-3.63-10.62c-2.42-2.7-5.57-4.05-9.45-4.05s-7.17,1.35-9.61,4.05c-2.44,2.7-3.66,6.24-3.66,10.62s1.22,7.93,3.66,10.66c2.44,2.73,5.64,4.1,9.61,4.1Z" class="cls-2"/><path d="M576.42,127.94v30.14h-11.02v-28.41c0-3.28-.84-5.84-2.53-7.69-1.69-1.84-3.98-2.77-6.89-2.77-3.09,0-5.56,1.01-7.41,3.02s-2.77,4.84-2.77,8.46v27.38h-11.16v-28.41c0-3.28-.82-5.84-2.46-7.69-1.64-1.84-3.91-2.77-6.82-2.77-3.09,0-5.58,1.01-7.45,3.02s-2.81,4.84-2.81,8.46v27.38h-11.34v-47.34h10.55l.38,4.55c2.75-4.03,7.23-6.05,13.45-6.05,3.62,0,6.77.75,9.42,2.25s4.67,3.69,6.05,6.56c1.16-2.75,3.07-4.91,5.74-6.47,2.67-1.56,5.85-2.34,9.54-2.34,5.38,0,9.64,1.66,12.8,4.97,3.16,3.31,4.73,7.89,4.73,13.73Z" class="cls-2"/><path d="M631.43,138.58h-36.28c.38,3.56,1.72,6.28,4.03,8.16s5.25,2.81,8.81,2.81c5.78,0,9.83-2.41,12.14-7.22l9.47,3.75c-1.78,4.16-4.59,7.41-8.41,9.75s-8.23,3.52-13.2,3.52c-7,0-12.68-2.29-17.04-6.87-4.36-4.58-6.54-10.59-6.54-18.02s2.2-13.48,6.59-18.14c4.39-4.66,10.13-6.98,17.23-6.98s12.58,2.3,16.83,6.89,6.38,10.64,6.38,18.14v4.22ZM595.34,130.61h24.56c-.22-3.84-1.38-6.77-3.49-8.79s-4.95-3.02-8.51-3.02-6.41,1.02-8.62,3.07c-2.22,2.05-3.53,4.96-3.94,8.74Z" class="cls-2"/></g><g><path d="M170.55,153.49c0,4.12-3.38,7.5-7.5,7.5H58.05c-4.12,0-7.5-3.38-7.5-7.5v-45c0-4.12,2.39-9.89,5.3-12Line truncated
|
||||
|
After Width: | Height: | Size: 5.3 KiB |
@@ -0,0 +1,176 @@
|
||||
#pragma once
|
||||
// Hourly weather forecast for mcu-m5paper: parsed from the weather.get_forecasts
|
||||
// response and kept in the on-board FM24C02 EEPROM (256 B, I2C 0x50), so it
|
||||
// survives the power-off between RTC wake-ups.
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
#include "esphome/components/i2c/i2c_bus.h"
|
||||
#include "esphome/components/json/json_util.h"
|
||||
#include "esphome/core/hal.h"
|
||||
|
||||
namespace m5paper_weather {
|
||||
|
||||
static constexpr uint8_t EEPROM_ADDR = 0x50;
|
||||
static constexpr uint8_t EEPROM_PAGE = 8;
|
||||
static constexpr uint8_t MAGIC = 0xA5;
|
||||
static constexpr uint8_t VERSION = 1;
|
||||
static constexpr uint8_t MAX_HOURS = 48;
|
||||
// magic, version, fetched_at (u32), first_ts (u32), count
|
||||
static constexpr size_t HEADER = 11;
|
||||
static constexpr size_t MAX_SIZE = HEADER + MAX_HOURS * 4;
|
||||
static_assert(MAX_SIZE <= 256, "forecast does not fit into the EEPROM");
|
||||
|
||||
static constexpr uint8_t NO_VALUE = 0xFF;
|
||||
static constexpr int8_t NO_TEMP = INT8_MIN;
|
||||
|
||||
struct Hour {
|
||||
uint8_t cond; // index into CONDITIONS, NO_VALUE if unknown
|
||||
int8_t temp2; // temperature in 0.5 °C, NO_TEMP if unknown
|
||||
uint8_t wind; // km/h, NO_VALUE if unknown
|
||||
uint8_t pop; // precipitation probability %, NO_VALUE if unknown
|
||||
};
|
||||
static_assert(sizeof(Hour) == 4, "Hour must stay 4 bytes");
|
||||
|
||||
struct Forecast {
|
||||
uint32_t fetched_at{0}; // UTC epoch of the download
|
||||
uint32_t first_ts{0}; // UTC epoch of hours[0]; entries are 1 h apart
|
||||
uint8_t count{0};
|
||||
Hour hours[MAX_HOURS]{};
|
||||
|
||||
bool valid() const { return this->count > 0; }
|
||||
// hour covering the given UTC epoch, nullptr if outside the forecast
|
||||
const Hour *at(uint32_t ts) const {
|
||||
if (!this->valid() || ts < this->first_ts)
|
||||
return nullptr;
|
||||
uint32_t idx = (ts - this->first_ts) / 3600;
|
||||
return idx < this->count ? &this->hours[idx] : nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
// the forecast in RAM, loaded from the EEPROM at boot
|
||||
inline Forecast &forecast() {
|
||||
static Forecast fc;
|
||||
return fc;
|
||||
}
|
||||
|
||||
// Home Assistant weather conditions; the index is the stored condition code
|
||||
static const char *const CONDITIONS[] = {
|
||||
"clear-night", "cloudy", "exceptional", "fog", "hail", "lightning", "lightning-rainy", "partlycloudy",
|
||||
"pouring", "rainy", "snowy", "snowy-rainy", "sunny", "windy", "windy-variant",
|
||||
};
|
||||
static const char *const CONDITION_NAMES[] = {
|
||||
"Jasno", "Oblačno", "Extrémne počasie", "Hmla", "Krupobitie", "Búrka", "Búrka s dažďom", "Polooblačno",
|
||||
"Lejak", "Dážď", "Sneženie", "Dážď so snehom", "Slnečno", "Veterno", "Veterno, oblačno",
|
||||
};
|
||||
static constexpr uint8_t NUM_CONDITIONS = sizeof(CONDITIONS) / sizeof(CONDITIONS[0]);
|
||||
|
||||
inline uint8_t condition_code(const char *name) {
|
||||
for (uint8_t i = 0; i < NUM_CONDITIONS; i++) {
|
||||
if (strcmp(name, CONDITIONS[i]) == 0)
|
||||
return i;
|
||||
}
|
||||
return NO_VALUE;
|
||||
}
|
||||
|
||||
inline const char *condition_name(uint8_t code) { return code < NUM_CONDITIONS ? CONDITION_NAMES[code] : "?"; }
|
||||
|
||||
// days since 1970-01-01 for a proleptic Gregorian date (H. Hinnant's algorithm)
|
||||
inline int32_t days_from_civil(int y, int m, int d) {
|
||||
y -= m <= 2;
|
||||
const int era = (y >= 0 ? y : y - 399) / 400;
|
||||
const int yoe = y - era * 400;
|
||||
const int doy = (153 * (m + (m > 2 ? -3 : 9)) + 2) / 5 + d - 1;
|
||||
const int doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
|
||||
return era * 146097 + doe - 719468;
|
||||
}
|
||||
|
||||
// "2026-09-27T16:00:00+00:00" -> UTC epoch, 0 on error
|
||||
inline uint32_t parse_iso8601(const char *s) {
|
||||
int y, mo, d, h, mi, sec = 0, oh = 0, om = 0;
|
||||
char sign = '+';
|
||||
int n = sscanf(s, "%d-%d-%dT%d:%d:%d%c%d:%d", &y, &mo, &d, &h, &mi, &sec, &sign, &oh, &om);
|
||||
if (n < 5)
|
||||
return 0;
|
||||
int64_t ts = int64_t(days_from_civil(y, mo, d)) * 86400 + h * 3600 + mi * 60 + sec;
|
||||
if (n >= 8 && (sign == '+' || sign == '-')) {
|
||||
int64_t offset = oh * 3600 + om * 60;
|
||||
ts += sign == '+' ? -offset : offset;
|
||||
}
|
||||
return ts > 0 ? uint32_t(ts) : 0;
|
||||
}
|
||||
|
||||
// Parse the weather.get_forecasts response for the given entity;
|
||||
// Home Assistant wraps the action response as {"response": {...}}
|
||||
inline bool parse(ArduinoJson::JsonObjectConst response, const char *entity, uint32_t now, Forecast &fc) {
|
||||
ArduinoJson::JsonObjectConst root = response["response"].is<ArduinoJson::JsonObjectConst>()
|
||||
? response["response"].as<ArduinoJson::JsonObjectConst>()
|
||||
: response;
|
||||
ArduinoJson::JsonArrayConst items = root[entity]["forecast"];
|
||||
if (items.isNull() || items.size() == 0)
|
||||
return false;
|
||||
|
||||
Forecast out;
|
||||
for (ArduinoJson::JsonObjectConst item : items) {
|
||||
if (out.count >= MAX_HOURS)
|
||||
break;
|
||||
if (out.count == 0) {
|
||||
out.first_ts = parse_iso8601(item["datetime"] | "");
|
||||
if (out.first_ts == 0)
|
||||
return false;
|
||||
}
|
||||
Hour &hour = out.hours[out.count++];
|
||||
hour.cond = condition_code(item["condition"] | "");
|
||||
float temp = item["temperature"] | NAN;
|
||||
hour.temp2 = std::isnan(temp) ? NO_TEMP : int8_t(std::max(-127L, std::min(127L, lroundf(temp * 2))));
|
||||
float wind = item["wind_speed"] | NAN;
|
||||
hour.wind = std::isnan(wind) ? NO_VALUE : uint8_t(std::max(0L, std::min(254L, lroundf(wind))));
|
||||
float pop = item["precipitation_probability"] | NAN;
|
||||
hour.pop = std::isnan(pop) ? NO_VALUE : uint8_t(std::max(0L, std::min(100L, lroundf(pop))));
|
||||
}
|
||||
out.fetched_at = now;
|
||||
fc = out;
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool load(esphome::i2c::I2CBus *bus, Forecast &fc) {
|
||||
uint8_t buf[MAX_SIZE];
|
||||
const uint8_t start = 0;
|
||||
fc.count = 0;
|
||||
if (bus->write_readv(EEPROM_ADDR, &start, 1, buf, sizeof(buf)) != esphome::i2c::ERROR_OK)
|
||||
return false;
|
||||
if (buf[0] != MAGIC || buf[1] != VERSION)
|
||||
return false;
|
||||
memcpy(&fc.fetched_at, buf + 2, 4);
|
||||
memcpy(&fc.first_ts, buf + 6, 4);
|
||||
fc.count = std::min(buf[10], MAX_HOURS);
|
||||
memcpy(fc.hours, buf + HEADER, fc.count * sizeof(Hour));
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool save(esphome::i2c::I2CBus *bus, const Forecast &fc) {
|
||||
uint8_t buf[MAX_SIZE];
|
||||
buf[0] = MAGIC;
|
||||
buf[1] = VERSION;
|
||||
memcpy(buf + 2, &fc.fetched_at, 4);
|
||||
memcpy(buf + 6, &fc.first_ts, 4);
|
||||
buf[10] = fc.count;
|
||||
memcpy(buf + HEADER, fc.hours, fc.count * sizeof(Hour));
|
||||
const size_t len = HEADER + fc.count * sizeof(Hour);
|
||||
|
||||
// page writes must not cross an 8-byte page boundary; 5 ms write cycle per page
|
||||
for (size_t off = 0; off < len; off += EEPROM_PAGE) {
|
||||
uint8_t page[EEPROM_PAGE + 1];
|
||||
const size_t n = std::min<size_t>(EEPROM_PAGE, len - off);
|
||||
page[0] = uint8_t(off);
|
||||
memcpy(page + 1, buf + off, n);
|
||||
if (bus->write(EEPROM_ADDR, page, n + 1) != esphome::i2c::ERROR_OK)
|
||||
return false;
|
||||
esphome::delay(6);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace m5paper_weather
|
||||
+564
-75
@@ -4,92 +4,370 @@ substitutions:
|
||||
name: M5Paper
|
||||
area: DEV
|
||||
comment: M5Paper
|
||||
weather_entity: weather.openweathermap
|
||||
|
||||
# Power modes
|
||||
# - USB: always on, WiFi + API, forecast refreshed every hour and redrawn every 15 min
|
||||
# - battery: RTC wakes the board every 15 min (off 1:00-5:00), it draws the forecast
|
||||
# from EEPROM and powers itself off; WiFi only to refresh the forecast every 6 h
|
||||
# Screens: status (after power-on, until Home Assistant connects) and forecast
|
||||
# The mode is detected by trying to power off: on USB the board keeps running.
|
||||
|
||||
esphome:
|
||||
name: mcu-${device}
|
||||
friendly_name: ${name}
|
||||
area: ${area}
|
||||
comment: ${comment}
|
||||
includes:
|
||||
- include/m5paper_weather.h
|
||||
on_boot:
|
||||
- priority: 600
|
||||
then:
|
||||
- lambda: |-
|
||||
// BM8563 CONTROL_STATUS_2: TF (bit 2) is set when the timer woke us up
|
||||
const uint8_t reg = 0x01;
|
||||
uint8_t status = 0;
|
||||
if (id(i2c_main)->write_readv(0x51, ®, 1, &status, 1) == i2c::ERROR_OK) {
|
||||
id(rtc_wake) = status & 0x04;
|
||||
// clear TF and TIE so the RTC releases the power latch
|
||||
const uint8_t clear[2] = {reg, uint8_t(status & ~0x05)};
|
||||
id(i2c_main)->write(0x51, clear, 2);
|
||||
}
|
||||
m5paper_weather::load(id(i2c_main), m5paper_weather::forecast());
|
||||
ESP_LOGI("m5paper", "Wake-up: %s, forecast: %u h", id(rtc_wake) ? "RTC" : "power-on", m5paper_weather::forecast().count);
|
||||
- priority: -100
|
||||
then:
|
||||
- if:
|
||||
condition:
|
||||
lambda: return id(rtc_wake);
|
||||
then:
|
||||
- script.execute: battery_cycle
|
||||
else:
|
||||
# power button / USB: stay awake for a while (logs, OTA), then decide the mode
|
||||
- wifi.enable:
|
||||
# IT8951 init runs asynchronously after setup
|
||||
- delay: 3s
|
||||
- component.update: m5paper_display
|
||||
- delay: 2min
|
||||
- script.execute: battery_cycle
|
||||
|
||||
esp32:
|
||||
variant: esp32
|
||||
toolchain: esp-idf
|
||||
framework:
|
||||
type: esp-idf
|
||||
advanced:
|
||||
flash_chip: generic
|
||||
minimum_chip_revision: 3.0
|
||||
sram1_as_iram: true
|
||||
|
||||
psram:
|
||||
|
||||
external_components:
|
||||
- source: github://Passific/m5paper_esphome
|
||||
|
||||
packages:
|
||||
common: !include common/common.yaml
|
||||
|
||||
time:
|
||||
- id: !extend ha_time
|
||||
on_time_sync:
|
||||
- bm8563.write_time
|
||||
- platform: bm8563
|
||||
id: rtc_time
|
||||
sleep_duration: 3600000ms
|
||||
globals:
|
||||
- id: show_status
|
||||
type: bool
|
||||
initial_value: 'true'
|
||||
- id: usb_mode
|
||||
type: bool
|
||||
initial_value: 'false'
|
||||
- id: rtc_wake
|
||||
type: bool
|
||||
initial_value: 'false'
|
||||
# battery cycle running: forecast screen regardless of the connection state
|
||||
- id: weather_page
|
||||
type: bool
|
||||
initial_value: 'false'
|
||||
- id: fetch_done
|
||||
type: bool
|
||||
initial_value: 'false'
|
||||
- id: sleep_s
|
||||
type: int
|
||||
initial_value: '900'
|
||||
|
||||
m5paper:
|
||||
battery_power_pin: GPIO5
|
||||
main_power_pin: GPIO2
|
||||
# update_interval: 10s
|
||||
# battery_voltage:
|
||||
# name: "${name} Battery"
|
||||
wifi:
|
||||
enable_on_boot: false
|
||||
on_connect:
|
||||
- if:
|
||||
condition:
|
||||
lambda: return id(show_status) && !id(weather_page);
|
||||
then:
|
||||
- component.update: m5paper_display
|
||||
|
||||
api:
|
||||
# status screen until Home Assistant connects (log viewers don't subscribe to states)
|
||||
on_client_connected:
|
||||
- delay: 2s
|
||||
- if:
|
||||
condition:
|
||||
api.connected:
|
||||
state_subscription_only: true
|
||||
then:
|
||||
- lambda: id(show_status) = false;
|
||||
- if:
|
||||
condition:
|
||||
lambda: return !id(weather_page);
|
||||
then:
|
||||
- component.update: m5paper_display
|
||||
# outside the battery cycle: refresh a forecast older than an hour
|
||||
- if:
|
||||
condition:
|
||||
and:
|
||||
- lambda: |-
|
||||
const auto &fc = m5paper_weather::forecast();
|
||||
const auto now = id(rtc_time).now();
|
||||
return !id(weather_page) && (!fc.valid() || !now.is_valid() || now.timestamp - fc.fetched_at >= 3600);
|
||||
- api.connected:
|
||||
state_subscription_only: true
|
||||
then:
|
||||
- script.execute: fetch_forecast
|
||||
on_client_disconnected:
|
||||
- if:
|
||||
condition:
|
||||
not:
|
||||
api.connected:
|
||||
state_subscription_only: true
|
||||
then:
|
||||
- lambda: id(show_status) = true;
|
||||
- if:
|
||||
condition:
|
||||
lambda: return !id(weather_page);
|
||||
then:
|
||||
- component.update: m5paper_display
|
||||
|
||||
# I2C devices (i2c_main)
|
||||
# 0x44 SHT30
|
||||
# 0x50
|
||||
# 0x51
|
||||
# 0x5D GT911
|
||||
# 0x50 FM24C02 EEPROM (forecast storage, see include/m5paper_weather.h)
|
||||
# 0x51 BM8563 RTC
|
||||
# 0x5D GT911 touch (docs say 0x14; the driver tries 0x5D, then 0x14)
|
||||
|
||||
i2c:
|
||||
- id: i2c_main
|
||||
sda: 21
|
||||
scl: 22
|
||||
scan: true #false
|
||||
scan: true
|
||||
# Port A
|
||||
# - id: i2c_a
|
||||
# sda: 25
|
||||
# scl: 32
|
||||
# scan: true #false
|
||||
# Port B
|
||||
# - id: i2c_b
|
||||
# sda: 26
|
||||
# scl: 33
|
||||
# scan: true #false
|
||||
# Port C
|
||||
# - id: i2c_c
|
||||
# sda: 18
|
||||
# scl: 19
|
||||
# scan: true #false
|
||||
|
||||
spi:
|
||||
clk_pin: 14
|
||||
miso_pin: 13
|
||||
mosi_pin: 12
|
||||
|
||||
switch:
|
||||
# Power control MOS (no PMU on M5Paper); on battery the device powers off when this goes low
|
||||
- platform: gpio
|
||||
id: main_power
|
||||
pin:
|
||||
number: GPIO2
|
||||
ignore_strapping_warning: true
|
||||
restore_mode: ALWAYS_ON
|
||||
internal: true
|
||||
# External power (Grove ports)
|
||||
- platform: gpio
|
||||
id: ext_power
|
||||
pin:
|
||||
number: GPIO5
|
||||
ignore_strapping_warning: true
|
||||
restore_mode: ALWAYS_ON
|
||||
internal: true
|
||||
|
||||
script:
|
||||
# One wake-up on battery: refresh the forecast if it is old, draw it, power off
|
||||
- id: battery_cycle
|
||||
mode: single
|
||||
then:
|
||||
- lambda: id(weather_page) = true;
|
||||
- wait_until:
|
||||
condition:
|
||||
lambda: return id(rtc_time).now().is_valid();
|
||||
timeout: 10s
|
||||
- if:
|
||||
condition:
|
||||
lambda: |-
|
||||
const auto &fc = m5paper_weather::forecast();
|
||||
const auto now = id(rtc_time).now();
|
||||
return !fc.valid() || !now.is_valid() || now.timestamp - fc.fetched_at >= 6 * 3600 - 600;
|
||||
then:
|
||||
- wifi.enable:
|
||||
- wait_until:
|
||||
condition:
|
||||
api.connected:
|
||||
state_subscription_only: true
|
||||
timeout: 45s
|
||||
- if:
|
||||
condition:
|
||||
api.connected:
|
||||
state_subscription_only: true
|
||||
then:
|
||||
- script.execute: fetch_forecast
|
||||
- wait_until:
|
||||
condition:
|
||||
lambda: return id(fetch_done);
|
||||
timeout: 15s
|
||||
- wait_until:
|
||||
condition:
|
||||
lambda: return !std::isnan(id(current_temperature).state);
|
||||
timeout: 5s
|
||||
- component.update: m5paper_display
|
||||
# let the e-paper refresh finish before cutting power
|
||||
- delay: 4s
|
||||
- script.execute: try_sleep
|
||||
|
||||
- id: fetch_forecast
|
||||
mode: single
|
||||
then:
|
||||
- lambda: id(fetch_done) = false;
|
||||
- homeassistant.action:
|
||||
action: weather.get_forecasts
|
||||
data:
|
||||
entity_id: ${weather_entity}
|
||||
type: hourly
|
||||
capture_response: true
|
||||
on_success:
|
||||
- lambda: |-
|
||||
auto &fc = m5paper_weather::forecast();
|
||||
if (m5paper_weather::parse(response, "${weather_entity}", id(rtc_time).now().timestamp, fc)) {
|
||||
const bool saved = m5paper_weather::save(id(i2c_main), fc);
|
||||
ESP_LOGI("m5paper", "Forecast: %u h%s", fc.count, saved ? "" : " (EEPROM write failed)");
|
||||
} else {
|
||||
ESP_LOGW("m5paper", "Forecast: unexpected response");
|
||||
}
|
||||
id(fetch_done) = true;
|
||||
# the battery cycle draws on its own
|
||||
- if:
|
||||
condition:
|
||||
lambda: return !id(weather_page);
|
||||
then:
|
||||
- component.update: m5paper_display
|
||||
on_error:
|
||||
- lambda: |-
|
||||
ESP_LOGW("m5paper", "Forecast: %s", error.c_str());
|
||||
id(fetch_done) = true;
|
||||
|
||||
# Arm the RTC and cut power; if we are still running afterwards, we are on USB
|
||||
- id: try_sleep
|
||||
mode: single
|
||||
then:
|
||||
- lambda: |-
|
||||
// next quarter hour, 1:00-5:00 in one go
|
||||
int duration = 900;
|
||||
const auto now = id(rtc_time).now();
|
||||
if (now.is_valid()) {
|
||||
const int sod = now.hour * 3600 + now.minute * 60 + now.second;
|
||||
int next = (sod / 900 + 1) * 900;
|
||||
if (next - sod < 90) // the 1/60 Hz RTC timer may wake us up to a minute early
|
||||
next += 900;
|
||||
const int next_hour = (next / 3600) % 24;
|
||||
if (next_hour >= 1 && next_hour < 5)
|
||||
next = (next / 86400) * 86400 + 5 * 3600;
|
||||
duration = next - sod;
|
||||
}
|
||||
if (duration > 255) // longer timers count whole minutes
|
||||
duration = (duration + 59) / 60 * 60;
|
||||
id(sleep_s) = std::min(duration, 255 * 60);
|
||||
ESP_LOGI("m5paper", "Powering off for %d s", id(sleep_s));
|
||||
- bm8563.start_timer:
|
||||
duration: !lambda return id(sleep_s);
|
||||
# a battery wake-up is shorter than boot_is_good_after: confirm the OTA image
|
||||
# before cutting power, otherwise the bootloader rolls back
|
||||
- safe_mode.mark_successful:
|
||||
- delay: 200ms
|
||||
- switch.turn_off: main_power
|
||||
- delay: 2s
|
||||
# still alive -> USB power
|
||||
- switch.turn_on: main_power
|
||||
- lambda: |-
|
||||
ESP_LOGI("m5paper", "Still running, USB power");
|
||||
id(usb_mode) = true;
|
||||
id(weather_page) = false;
|
||||
id(show_status) = !api::global_api_server->is_connected_with_state_subscription();
|
||||
- wifi.enable:
|
||||
- component.update: m5paper_display
|
||||
|
||||
interval:
|
||||
# USB mode: the battery voltage drops once USB is unplugged -> go to battery mode
|
||||
- interval: 5min
|
||||
then:
|
||||
- if:
|
||||
condition:
|
||||
lambda: |-
|
||||
static float last = NAN;
|
||||
const float v = id(battery_voltage).state;
|
||||
const bool dropped = id(usb_mode) && !std::isnan(last) && last - v > 0.03f;
|
||||
last = v;
|
||||
return dropped;
|
||||
then:
|
||||
- lambda: |-
|
||||
ESP_LOGI("m5paper", "Battery voltage dropping, checking USB");
|
||||
id(usb_mode) = false;
|
||||
- script.execute: battery_cycle
|
||||
|
||||
time:
|
||||
- id: !extend ha_time
|
||||
on_time_sync:
|
||||
- bm8563.write_time
|
||||
- if:
|
||||
condition:
|
||||
lambda: return !id(weather_page);
|
||||
then:
|
||||
- component.update: m5paper_display
|
||||
on_time:
|
||||
- seconds: 0
|
||||
minutes: /15
|
||||
then:
|
||||
- if:
|
||||
condition:
|
||||
lambda: return !id(show_status) && !id(weather_page);
|
||||
then:
|
||||
- component.update: m5paper_display
|
||||
# USB mode: keep the stored forecast fresh for battery mode
|
||||
- seconds: 0
|
||||
minutes: 5
|
||||
then:
|
||||
- if:
|
||||
condition:
|
||||
and:
|
||||
- lambda: return id(usb_mode);
|
||||
- api.connected:
|
||||
state_subscription_only: true
|
||||
then:
|
||||
- script.execute: fetch_forecast
|
||||
- platform: bm8563
|
||||
id: rtc_time
|
||||
|
||||
binary_sensor:
|
||||
- platform: gpio
|
||||
name: ${name} Button UP
|
||||
id: button_up
|
||||
pin:
|
||||
number: 37
|
||||
number: GPIO37
|
||||
inverted: true
|
||||
on_press:
|
||||
- component.update: m5paper_display
|
||||
on_release:
|
||||
- component.update: m5paper_display
|
||||
- platform: gpio
|
||||
name: ${name} Button Press
|
||||
id: button_press
|
||||
pin:
|
||||
number: 38
|
||||
number: GPIO38
|
||||
inverted: true
|
||||
- platform: gpio
|
||||
name: ${name} Button Down
|
||||
id: button_down
|
||||
pin:
|
||||
number: 39
|
||||
number: GPIO39
|
||||
inverted: true
|
||||
- platform: touchscreen
|
||||
name: ${name} Top Left Touch Button
|
||||
@@ -101,51 +379,19 @@ binary_sensor:
|
||||
y_min: 0
|
||||
y_max: 100
|
||||
on_press:
|
||||
# Example of applying the sleep duration and shutting down to the lowest power mode
|
||||
- bm8563.apply_sleep_duration
|
||||
- delay: 1s
|
||||
- m5paper.shutdown_main_power
|
||||
- script.execute: battery_cycle
|
||||
|
||||
font:
|
||||
- file: "gfonts://Roboto"
|
||||
id: font_default
|
||||
size: 20
|
||||
- file: "gfonts://Roboto"
|
||||
id: roboto_32
|
||||
size: 32
|
||||
- file: "gfonts://Roboto"
|
||||
id: roboto_24
|
||||
size: 24
|
||||
- file: "gfonts://Roboto"
|
||||
id: roboto_12
|
||||
size: 12
|
||||
|
||||
display:
|
||||
- platform: it8951e
|
||||
id: m5paper_display
|
||||
cs_pin: GPIO15
|
||||
reset_pin: GPIO23
|
||||
busy_pin: GPIO27
|
||||
rotation: 90
|
||||
reversed: False
|
||||
update_interval: "never"
|
||||
lambda: |-
|
||||
it.printf(25, 25, id(roboto_32), "%.1f°", id(current_temperature).state);
|
||||
it.strftime(350, 25, id(roboto_32), "%H:%M:%S", id(rtc_time).now());
|
||||
|
||||
touchscreen:
|
||||
- platform: gt911
|
||||
display: m5paper_display
|
||||
id: gt911_touchscreen
|
||||
interrupt_pin: GPIO36
|
||||
|
||||
# display:
|
||||
# - platform: waveshare_epaper
|
||||
# cs_pin: 15
|
||||
# model: 2.90in
|
||||
# full_update_every: 30
|
||||
# lambda: |-
|
||||
# it.print(0, 0, id(font1), "Hello World!");
|
||||
button:
|
||||
- platform: template
|
||||
name: Refresh Display
|
||||
entity_category: config
|
||||
on_press:
|
||||
- component.update: m5paper_display
|
||||
- platform: template
|
||||
name: Refresh Forecast
|
||||
entity_category: config
|
||||
on_press:
|
||||
- script.execute: fetch_forecast
|
||||
|
||||
sensor:
|
||||
- platform: sht3xd
|
||||
@@ -154,9 +400,252 @@ sensor:
|
||||
id: current_temperature
|
||||
humidity:
|
||||
name: Humidity
|
||||
id: current_humidity
|
||||
address: 0x44
|
||||
update_interval: 60s
|
||||
- platform: adc
|
||||
id: battery_voltage
|
||||
name: Battery Voltage
|
||||
pin: GPIO35
|
||||
attenuation: 12db
|
||||
samples: 16
|
||||
update_interval: 60s
|
||||
entity_category: diagnostic
|
||||
filters:
|
||||
- multiply: 2.0 # 1:2 voltage divider
|
||||
- platform: copy
|
||||
source_id: battery_voltage
|
||||
id: battery_level
|
||||
name: Battery
|
||||
unit_of_measurement: "%"
|
||||
device_class: battery
|
||||
accuracy_decimals: 0
|
||||
filters:
|
||||
- calibrate_linear:
|
||||
- 3.3 -> 0.0
|
||||
- 4.15 -> 100.0
|
||||
- clamp:
|
||||
min_value: 0
|
||||
max_value: 100
|
||||
|
||||
# touchscreen:
|
||||
# - platform: gt911
|
||||
# interrupt_pin: 36
|
||||
.image_defaults:
|
||||
icon_large: &icon_large {platform: file, type: BINARY, transparency: chroma_key, resize: 150x150}
|
||||
icon_small: &icon_small {platform: file, type: BINARY, transparency: chroma_key, resize: 44x44}
|
||||
|
||||
image:
|
||||
- platform: file
|
||||
file: assets/images/made-for-esphome-white-on-black.svg
|
||||
id: esphome_logo
|
||||
resize: 460x136
|
||||
type: GRAYSCALE
|
||||
transparency: alpha_channel
|
||||
# weather icons, same order as m5paper_weather::CONDITIONS
|
||||
- {<<: *icon_large, file: "mdi:weather-night", id: wl_clear_night}
|
||||
- {<<: *icon_large, file: "mdi:weather-cloudy", id: wl_cloudy}
|
||||
- {<<: *icon_large, file: "mdi:alert-circle-outline", id: wl_exceptional}
|
||||
- {<<: *icon_large, file: "mdi:weather-fog", id: wl_fog}
|
||||
- {<<: *icon_large, file: "mdi:weather-hail", id: wl_hail}
|
||||
- {<<: *icon_large, file: "mdi:weather-lightning", id: wl_lightning}
|
||||
- {<<: *icon_large, file: "mdi:weather-lightning-rainy", id: wl_lightning_rainy}
|
||||
- {<<: *icon_large, file: "mdi:weather-partly-cloudy", id: wl_partlycloudy}
|
||||
- {<<: *icon_large, file: "mdi:weather-pouring", id: wl_pouring}
|
||||
- {<<: *icon_large, file: "mdi:weather-rainy", id: wl_rainy}
|
||||
- {<<: *icon_large, file: "mdi:weather-snowy", id: wl_snowy}
|
||||
- {<<: *icon_large, file: "mdi:weather-snowy-rainy", id: wl_snowy_rainy}
|
||||
- {<<: *icon_large, file: "mdi:weather-sunny", id: wl_sunny}
|
||||
- {<<: *icon_large, file: "mdi:weather-windy", id: wl_windy}
|
||||
- {<<: *icon_large, file: "mdi:weather-windy-variant", id: wl_windy_variant}
|
||||
- {<<: *icon_small, file: "mdi:weather-night", id: ws_clear_night}
|
||||
- {<<: *icon_small, file: "mdi:weather-cloudy", id: ws_cloudy}
|
||||
- {<<: *icon_small, file: "mdi:alert-circle-outline", id: ws_exceptional}
|
||||
- {<<: *icon_small, file: "mdi:weather-fog", id: ws_fog}
|
||||
- {<<: *icon_small, file: "mdi:weather-hail", id: ws_hail}
|
||||
- {<<: *icon_small, file: "mdi:weather-lightning", id: ws_lightning}
|
||||
- {<<: *icon_small, file: "mdi:weather-lightning-rainy", id: ws_lightning_rainy}
|
||||
- {<<: *icon_small, file: "mdi:weather-partly-cloudy", id: ws_partlycloudy}
|
||||
- {<<: *icon_small, file: "mdi:weather-pouring", id: ws_pouring}
|
||||
- {<<: *icon_small, file: "mdi:weather-rainy", id: ws_rainy}
|
||||
- {<<: *icon_small, file: "mdi:weather-snowy", id: ws_snowy}
|
||||
- {<<: *icon_small, file: "mdi:weather-snowy-rainy", id: ws_snowy_rainy}
|
||||
- {<<: *icon_small, file: "mdi:weather-sunny", id: ws_sunny}
|
||||
- {<<: *icon_small, file: "mdi:weather-windy", id: ws_windy}
|
||||
- {<<: *icon_small, file: "mdi:weather-windy-variant", id: ws_windy_variant}
|
||||
|
||||
font:
|
||||
- file: "gfonts://Roboto"
|
||||
id: font_title
|
||||
size: 48
|
||||
- file: "gfonts://Roboto"
|
||||
id: font_status
|
||||
size: 28
|
||||
glyphs: &sk_glyphs "ÁÄČĎÉÍĹĽŇÓÔŔŠŤÚÝŽáäčďéíĺľňóôŕšťúýž°·"
|
||||
glyphsets: [GF_Latin_Kernel]
|
||||
- file: "gfonts://Roboto"
|
||||
id: font_small
|
||||
size: 24
|
||||
glyphs: *sk_glyphs
|
||||
glyphsets: [GF_Latin_Kernel]
|
||||
- file: "gfonts://Roboto"
|
||||
id: font_tiny
|
||||
size: 18
|
||||
glyphs: "0123456789% "
|
||||
- file: "gfonts://Roboto@500"
|
||||
id: font_date
|
||||
size: 34
|
||||
glyphs: *sk_glyphs
|
||||
glyphsets: [GF_Latin_Kernel]
|
||||
- file: "gfonts://Roboto@500"
|
||||
id: font_hour
|
||||
size: 28
|
||||
glyphs: "0123456789-°"
|
||||
- file: "gfonts://Roboto"
|
||||
id: font_temp
|
||||
size: 110
|
||||
glyphs: "0123456789.,-°C "
|
||||
|
||||
color:
|
||||
- id: ink
|
||||
red: 0%
|
||||
green: 0%
|
||||
blue: 0%
|
||||
- id: paper
|
||||
red: 100%
|
||||
green: 100%
|
||||
blue: 100%
|
||||
|
||||
display:
|
||||
- platform: it8951
|
||||
id: m5paper_display
|
||||
model: m5stack-m5paper
|
||||
rotation: 270
|
||||
update_interval: never
|
||||
# 1bpp: updates use the fast DU waveform, full GC16 refresh every 30 updates
|
||||
grayscale: false
|
||||
dithering: false
|
||||
lambda: |-
|
||||
// 540x960 portrait
|
||||
it.fill(id(paper));
|
||||
const int w = it.get_width();
|
||||
const int cx = w / 2;
|
||||
const float batt = id(battery_level).state;
|
||||
const float temp = id(current_temperature).state;
|
||||
const float hum = id(current_humidity).state;
|
||||
|
||||
// status screen after power-on, until Home Assistant connects
|
||||
if (id(show_status) && !id(weather_page)) {
|
||||
it.image(cx, 200, id(esphome_logo), ImageAlign::CENTER);
|
||||
it.print(cx, 320, id(font_title), id(ink), TextAlign::CENTER, "${name}");
|
||||
|
||||
const bool wifi_ok = wifi::global_wifi_component->is_connected();
|
||||
if (wifi_ok) {
|
||||
it.printf(40, 440, id(font_status), id(ink), "WiFi: connected (%d dBm)", wifi::global_wifi_component->wifi_rssi());
|
||||
// IPv6 first (skip link-local), then IPv4
|
||||
int y = 484;
|
||||
char buf[network::IP_ADDRESS_BUFFER_SIZE];
|
||||
for (bool v6 : {true, false}) {
|
||||
for (auto &ip : wifi::global_wifi_component->get_ip_addresses()) {
|
||||
if (!ip.is_set() || ip.is_ip6() != v6 || y > 590)
|
||||
continue;
|
||||
ip.str_to(buf);
|
||||
if (v6 && strncmp(buf, "fe80", 4) == 0)
|
||||
continue;
|
||||
it.print(60, y, id(font_small), id(ink), buf);
|
||||
y += 34;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
it.print(40, 440, id(font_status), id(ink), "WiFi: connecting...");
|
||||
}
|
||||
const bool api_ok = api::global_api_server->is_connected_with_state_subscription();
|
||||
it.print(40, 620, id(font_status), id(ink),
|
||||
api_ok ? "Home Assistant: connected" : "Home Assistant: waiting...");
|
||||
if (!std::isnan(batt))
|
||||
it.printf(40, 670, id(font_status), id(ink), "Battery: %.0f%% (%.2f V)", batt, id(battery_voltage).state);
|
||||
it.printf(40, 720, id(font_status), id(ink), "Forecast: %u h stored", m5paper_weather::forecast().count);
|
||||
it.printf(40, 900, id(font_small), id(ink), "ESPHome %s", ESPHOME_VERSION);
|
||||
return;
|
||||
}
|
||||
|
||||
// forecast screen
|
||||
using namespace m5paper_weather;
|
||||
image::Image *large[] = {id(wl_clear_night), id(wl_cloudy), id(wl_exceptional), id(wl_fog),
|
||||
id(wl_hail), id(wl_lightning), id(wl_lightning_rainy), id(wl_partlycloudy),
|
||||
id(wl_pouring), id(wl_rainy), id(wl_snowy), id(wl_snowy_rainy),
|
||||
id(wl_sunny), id(wl_windy), id(wl_windy_variant)};
|
||||
image::Image *small[] = {id(ws_clear_night), id(ws_cloudy), id(ws_exceptional), id(ws_fog),
|
||||
id(ws_hail), id(ws_lightning), id(ws_lightning_rainy), id(ws_partlycloudy),
|
||||
id(ws_pouring), id(ws_rainy), id(ws_snowy), id(ws_snowy_rainy),
|
||||
id(ws_sunny), id(ws_windy), id(ws_windy_variant)};
|
||||
static const char *const DAYS[] = {"Nedeľa", "Pondelok", "Utorok", "Streda", "Štvrtok", "Piatok", "Sobota"};
|
||||
|
||||
const auto &fc = m5paper_weather::forecast();
|
||||
const auto now = id(rtc_time).now();
|
||||
|
||||
// header: date, battery
|
||||
if (now.is_valid())
|
||||
it.printf(24, 16, id(font_date), id(ink), "%s %d. %d.", DAYS[(now.day_of_week + 6) % 7], now.day_of_month,
|
||||
now.month);
|
||||
if (!std::isnan(batt))
|
||||
it.printf(w - 24, 22, id(font_small), id(ink), TextAlign::TOP_RIGHT, "%.0f %%", batt);
|
||||
|
||||
const Hour *cur = now.is_valid() ? fc.at(now.timestamp) : nullptr;
|
||||
if (cur == nullptr) {
|
||||
it.print(cx, 230, id(font_status), id(ink), TextAlign::CENTER, "Predpoveď nie je k dispozícii");
|
||||
} else {
|
||||
// current hour
|
||||
if (cur->cond < NUM_CONDITIONS)
|
||||
it.image(24, 80, large[cur->cond], id(ink));
|
||||
if (cur->temp2 != NO_TEMP)
|
||||
it.printf(200, 70, id(font_temp), id(ink), "%.0f°", cur->temp2 / 2.0f);
|
||||
it.print(204, 200, id(font_status), id(ink), condition_name(cur->cond));
|
||||
if (cur->wind != NO_VALUE)
|
||||
it.printf(204, 245, id(font_small), id(ink), "vietor %u km/h", cur->wind);
|
||||
if (cur->pop != NO_VALUE)
|
||||
it.printf(204, 280, id(font_small), id(ink), "zrážky %u %%", cur->pop);
|
||||
|
||||
// next 24 hours: 3 columns x 8 rows
|
||||
const uint32_t first = (now.timestamp / 3600 + 1) * 3600;
|
||||
const int top = 440, row_h = 60, col_w = w / 3;
|
||||
for (int i = 0; i < 24; i++) {
|
||||
const Hour *h = fc.at(first + i * 3600);
|
||||
if (h == nullptr)
|
||||
break;
|
||||
const int x = (i / 8) * col_w + 12;
|
||||
const int y = top + (i % 8) * row_h;
|
||||
const auto t = ESPTime::from_epoch_local(first + i * 3600);
|
||||
it.printf(x, y + 14, id(font_hour), id(ink), "%02d", t.hour);
|
||||
if (h->cond < NUM_CONDITIONS)
|
||||
it.image(x + 42, y + 8, small[h->cond], id(ink));
|
||||
if (h->temp2 != NO_TEMP)
|
||||
it.printf(x + 94, y + 6, id(font_hour), id(ink), "%.0f°", h->temp2 / 2.0f);
|
||||
if (h->pop != NO_VALUE && h->pop >= 10)
|
||||
it.printf(x + 94, y + 38, id(font_tiny), id(ink), "%u %%", h->pop);
|
||||
}
|
||||
for (int c = 1; c < 3; c++)
|
||||
it.vertical_line(c * col_w, top, 8 * row_h, id(ink));
|
||||
}
|
||||
|
||||
// indoor
|
||||
it.horizontal_line(24, 340, w - 48, id(ink));
|
||||
it.printf(cx, 380, id(font_status), id(ink), TextAlign::CENTER, "Doma %.1f °C · %.0f %%", temp, hum);
|
||||
it.horizontal_line(24, 420, w - 48, id(ink));
|
||||
|
||||
// footer
|
||||
if (fc.valid()) {
|
||||
const auto f = ESPTime::from_epoch_local(fc.fetched_at);
|
||||
it.printf(24, 944, id(font_small), id(ink), TextAlign::BOTTOM_LEFT, "predpoveď %d. %d. %02d:%02d",
|
||||
f.day_of_month, f.month, f.hour, f.minute);
|
||||
}
|
||||
if (now.is_valid())
|
||||
it.printf(w - 24, 944, id(font_small), id(ink), TextAlign::BOTTOM_RIGHT, "%02d:%02d", now.hour, now.minute);
|
||||
|
||||
touchscreen:
|
||||
- platform: gt911
|
||||
id: gt911_touchscreen
|
||||
display: m5paper_display
|
||||
# GPIO36 is input-only; INT is only driven as output when reset_pin is set
|
||||
interrupt_pin:
|
||||
number: GPIO36
|
||||
mode:
|
||||
input: true
|
||||
output: false
|
||||
Reference in new issue
Block a user