mirror of
https://github.com/randybb/esphome-configs.git
synced 2026-10-14 08:35:12 +02:00
667 lines
22 KiB
YAML
667 lines
22 KiB
YAML
---
|
|
substitutions:
|
|
device: m5paper
|
|
name: M5Paper
|
|
area: DEV
|
|
comment: M5Paper
|
|
waste_calendar: calendar.https_www_hviezdoslavov_sk_kalendar_icalfeed_category_0_ongoing_1
|
|
# contacts + manual
|
|
birthday_calendars: calendar.birthdays, calendar.narodeniny
|
|
|
|
# Power modes
|
|
# - USB: always on, WiFi + API, events refreshed and redrawn after midnight
|
|
# - battery: RTC wakes the board at 00:05, it refreshes the events, draws them and powers
|
|
# itself off; the RTC timer lasts at most 255 min, so the wake-ups on the way to
|
|
# midnight only re-arm it
|
|
# The power button (and USB) also connects to WiFi: the events are refreshed on every
|
|
# Home Assistant connection outside the battery cycle.
|
|
# Screens: status (after power-on, until Home Assistant connects) and calendar
|
|
# 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_calendar.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_calendar::load(m5paper_calendar::events());
|
|
ESP_LOGI("m5paper", "Wake-up: %s, events: %u", id(rtc_wake) ? "RTC" : "power-on", m5paper_calendar::events().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:
|
|
|
|
packages:
|
|
common: !include common/common.yaml
|
|
ipv6_only: !include common/ipv6_only.yaml
|
|
|
|
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: calendar screen regardless of the connection state
|
|
- id: calendar_page
|
|
type: bool
|
|
initial_value: 'false'
|
|
- id: fetch_done
|
|
type: bool
|
|
initial_value: 'false'
|
|
- id: sleep_s
|
|
type: int
|
|
initial_value: '900'
|
|
|
|
wifi:
|
|
enable_on_boot: false
|
|
on_connect:
|
|
- if:
|
|
condition:
|
|
lambda: return id(show_status) && !id(calendar_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(calendar_page);
|
|
then:
|
|
- component.update: m5paper_display
|
|
# outside the battery cycle (power button, USB) WiFi is up anyway: always refresh
|
|
- if:
|
|
condition:
|
|
and:
|
|
- lambda: return !id(calendar_page);
|
|
- api.connected:
|
|
state_subscription_only: true
|
|
then:
|
|
- script.execute: fetch_events
|
|
on_client_disconnected:
|
|
- if:
|
|
condition:
|
|
not:
|
|
api.connected:
|
|
state_subscription_only: true
|
|
then:
|
|
- lambda: id(show_status) = true;
|
|
- if:
|
|
condition:
|
|
lambda: return !id(calendar_page);
|
|
then:
|
|
- component.update: m5paper_display
|
|
|
|
# I2C devices (i2c_main)
|
|
# 0x44 SHT30
|
|
# 0x50 FM24C02 EEPROM (unused)
|
|
# 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
|
|
# Port A
|
|
# - id: i2c_a
|
|
# sda: 25
|
|
# scl: 32
|
|
# Port B
|
|
# - id: i2c_b
|
|
# sda: 26
|
|
# scl: 33
|
|
# Port C
|
|
# - id: i2c_c
|
|
# sda: 18
|
|
# scl: 19
|
|
|
|
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 events if they are old, draw them, power off
|
|
- id: battery_cycle
|
|
mode: single
|
|
then:
|
|
- lambda: id(calendar_page) = true;
|
|
- wait_until:
|
|
condition:
|
|
lambda: return id(rtc_time).now().is_valid();
|
|
timeout: 10s
|
|
- if:
|
|
# RTC wake-up on the way to midnight: nothing to draw
|
|
condition:
|
|
lambda: |-
|
|
const auto now = id(rtc_time).now();
|
|
return id(rtc_wake) && now.is_valid() && now.hour != 0;
|
|
then:
|
|
- script.execute: try_sleep
|
|
else:
|
|
- if:
|
|
condition:
|
|
lambda: return m5paper_calendar::stale(id(rtc_time).now());
|
|
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_events
|
|
- wait_until:
|
|
condition:
|
|
lambda: return id(fetch_done);
|
|
timeout: 15s
|
|
- component.update: m5paper_display
|
|
# let the e-paper refresh finish before cutting power
|
|
- delay: 4s
|
|
- script.execute: try_sleep
|
|
|
|
# today and the next 31 days (the month grid and the list)
|
|
- id: fetch_events
|
|
mode: single
|
|
then:
|
|
- lambda: id(fetch_done) = false;
|
|
- homeassistant.action:
|
|
action: calendar.get_events
|
|
data:
|
|
entity_id: ${waste_calendar}, ${birthday_calendars}
|
|
start_date_time: !lambda return id(rtc_time).now().strftime("%Y-%m-%d 00:00:00");
|
|
duration: "744:00:00"
|
|
capture_response: true
|
|
on_success:
|
|
- lambda: |-
|
|
auto &ev = m5paper_calendar::events();
|
|
if (m5paper_calendar::parse(response, "${waste_calendar}", id(rtc_time).now().timestamp, ev)) {
|
|
const bool saved = m5paper_calendar::save(ev);
|
|
ESP_LOGI("m5paper", "Events: %u%s", ev.count, saved ? "" : " (flash write failed)");
|
|
} else {
|
|
ESP_LOGW("m5paper", "Events: unexpected response");
|
|
}
|
|
id(fetch_done) = true;
|
|
# the battery cycle draws on its own
|
|
- if:
|
|
condition:
|
|
lambda: return !id(calendar_page);
|
|
then:
|
|
- component.update: m5paper_display
|
|
on_error:
|
|
- lambda: |-
|
|
ESP_LOGW("m5paper", "Events: %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 00:05; the timer lasts at most 255 min, so get there in steps
|
|
const int MAX = 255 * 60;
|
|
int duration = MAX;
|
|
const auto now = id(rtc_time).now();
|
|
if (now.is_valid()) {
|
|
const int sod = now.hour * 3600 + now.minute * 60 + now.second;
|
|
int left = (86400 + 300 - sod) % 86400;
|
|
if (left < 600) // just drawn: the 1/60 Hz RTC timer may wake us up to a minute early
|
|
left += 86400;
|
|
// leave at least 20 min for the last step, so it cannot fall into the case above
|
|
duration = left > MAX ? std::min(MAX, left - 1200) : left;
|
|
}
|
|
if (duration > 255) // longer timers count whole minutes
|
|
duration = (duration + 59) / 60 * 60;
|
|
id(sleep_s) = std::min(duration, MAX);
|
|
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(rtc_wake) = false;
|
|
id(calendar_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(calendar_page);
|
|
then:
|
|
- component.update: m5paper_display
|
|
on_time:
|
|
# new day
|
|
- seconds: 0
|
|
minutes: 0
|
|
hours: 0
|
|
then:
|
|
- if:
|
|
condition:
|
|
lambda: return !id(show_status) && !id(calendar_page);
|
|
then:
|
|
- component.update: m5paper_display
|
|
# USB mode: keep the stored events fresh for battery mode
|
|
- seconds: 0
|
|
minutes: 5
|
|
hours: 0
|
|
then:
|
|
- if:
|
|
condition:
|
|
and:
|
|
- lambda: return id(usb_mode);
|
|
- api.connected:
|
|
state_subscription_only: true
|
|
then:
|
|
- script.execute: fetch_events
|
|
- platform: bm8563
|
|
id: rtc_time
|
|
|
|
binary_sensor:
|
|
- platform: gpio
|
|
name: ${name} Button UP
|
|
id: button_up
|
|
pin:
|
|
number: GPIO37
|
|
inverted: true
|
|
on_press:
|
|
- component.update: m5paper_display
|
|
- platform: gpio
|
|
name: ${name} Button Press
|
|
id: button_press
|
|
pin:
|
|
number: GPIO38
|
|
inverted: true
|
|
- platform: gpio
|
|
name: ${name} Button Down
|
|
id: button_down
|
|
pin:
|
|
number: GPIO39
|
|
inverted: true
|
|
- platform: touchscreen
|
|
name: ${name} Top Left Touch Button
|
|
id: top_left_touch_button
|
|
touchscreen_id: gt911_touchscreen
|
|
internal: true
|
|
x_min: 0
|
|
x_max: 100
|
|
y_min: 0
|
|
y_max: 100
|
|
on_press:
|
|
- script.execute: battery_cycle
|
|
|
|
button:
|
|
- platform: template
|
|
name: Refresh Display
|
|
entity_category: config
|
|
on_press:
|
|
- component.update: m5paper_display
|
|
- platform: template
|
|
name: Refresh Calendar
|
|
entity_category: config
|
|
on_press:
|
|
- script.execute: fetch_events
|
|
|
|
sensor:
|
|
- platform: sht3xd
|
|
temperature:
|
|
name: Temperature
|
|
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
|
|
|
|
.image_defaults:
|
|
icon: &icon {platform: file, type: BINARY, transparency: chroma_key}
|
|
|
|
image:
|
|
- platform: file
|
|
file: assets/images/made-for-esphome-white-on-black.svg
|
|
id: esphome_logo
|
|
resize: 460x136
|
|
type: GRAYSCALE
|
|
transparency: alpha_channel
|
|
# same order as m5paper_calendar::Kind
|
|
- {<<: *icon, file: "mdi:cake-variant", id: icon_birthday, resize: 32x32}
|
|
- {<<: *icon, file: "mdi:trash-can", id: icon_waste, resize: 32x32}
|
|
- {<<: *icon, file: "mdi:cake-variant", id: mark_birthday, resize: 18x18}
|
|
- {<<: *icon, file: "mdi:trash-can", id: mark_waste, resize: 18x18}
|
|
|
|
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_footer
|
|
size: 18
|
|
glyphs: "0123456789abcdef:. "
|
|
- file: "gfonts://Roboto@500"
|
|
id: font_date
|
|
size: 34
|
|
glyphs: *sk_glyphs
|
|
glyphsets: [GF_Latin_Kernel]
|
|
|
|
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;
|
|
|
|
// status screen after power-on, until Home Assistant connects
|
|
if (id(show_status) && !id(calendar_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), "Calendar: %u events stored", m5paper_calendar::events().count);
|
|
it.printf(40, 900, id(font_small), id(ink), "ESPHome %s", ESPHOME_VERSION);
|
|
return;
|
|
}
|
|
|
|
// calendar screen
|
|
using namespace m5paper_calendar;
|
|
image::Image *icons[] = {id(icon_birthday), id(icon_waste)};
|
|
image::Image *marks[] = {id(mark_birthday), id(mark_waste)};
|
|
static const char *const DAYS[] = {"Pondelok", "Utorok", "Streda", "Štvrtok", "Piatok", "Sobota", "Nedeľa"};
|
|
static const char *const DAYS_SHORT[] = {"Po", "Ut", "St", "Št", "Pi", "So", "Ne"};
|
|
static const char *const MONTHS[] = {"Január", "Február", "Marec", "Apríl", "Máj", "Jún",
|
|
"Júl", "August", "September", "Október", "November", "December"};
|
|
|
|
const auto &ev = m5paper_calendar::events();
|
|
const auto now = id(rtc_time).now();
|
|
if (!now.is_valid()) {
|
|
it.print(cx, 400, id(font_status), id(ink), TextAlign::CENTER, "Čas nie je k dispozícii");
|
|
return;
|
|
}
|
|
const int32_t today = local_day(now);
|
|
|
|
// header: date, battery
|
|
it.printf(24, 16, id(font_date), id(ink), "%s %d. %d.", DAYS[weekday(today)], 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);
|
|
|
|
// month grid; days with events get the icons of their kinds
|
|
const int32_t first = days_from_civil(now.year, now.month, 1);
|
|
const int32_t next = now.month == 12 ? days_from_civil(now.year + 1, 1, 1) : days_from_civil(now.year, now.month + 1, 1);
|
|
const int dim = next - first, lead = weekday(first);
|
|
uint8_t kinds[32] = {};
|
|
for (int i = 0; i < ev.count; i++) {
|
|
if (ev.items[i].day >= first && ev.items[i].day < next)
|
|
kinds[ev.items[i].day - first + 1] |= 1 << ev.items[i].kind;
|
|
}
|
|
const int col_w = 70, row_h = 54, gx = (w - 7 * col_w) / 2, gy = 150;
|
|
it.printf(cx, 70, id(font_status), id(ink), TextAlign::TOP_CENTER, "%s %d", MONTHS[now.month - 1], now.year);
|
|
for (int c = 0; c < 7; c++)
|
|
it.print(gx + c * col_w + col_w / 2, 116, id(font_small), id(ink), TextAlign::TOP_CENTER, DAYS_SHORT[c]);
|
|
for (int d = 1; d <= dim; d++) {
|
|
const int cell = lead + d - 1;
|
|
const int x = gx + (cell % 7) * col_w, y = gy + (cell / 7) * row_h;
|
|
const bool is_today = first + d - 1 == today;
|
|
if (is_today)
|
|
it.filled_rectangle(x + 3, y + 1, col_w - 6, row_h - 2, id(ink));
|
|
const Color fg = is_today ? id(paper) : id(ink);
|
|
it.printf(x + col_w / 2, y + 4, id(font_small), fg, TextAlign::TOP_CENTER, "%d", d);
|
|
const int n = __builtin_popcount(kinds[d]);
|
|
int mx = x + (col_w - n * 20) / 2;
|
|
for (int k = 0; k < 2; k++) {
|
|
if (kinds[d] & (1 << k)) {
|
|
it.image(mx, y + 32, marks[k], fg);
|
|
mx += 20;
|
|
}
|
|
}
|
|
}
|
|
|
|
// next 14 days; waste collection today or tomorrow is inverted: put the bin out
|
|
const int top = 490, list_h = 50, max_rows = 8;
|
|
it.horizontal_line(24, top - 12, w - 48, id(ink));
|
|
int rows = 0, more = 0;
|
|
for (int i = 0; i < ev.count; i++) {
|
|
const Event &e = ev.items[i];
|
|
if (e.day < today || e.day >= today + 14)
|
|
continue;
|
|
if (rows == max_rows) {
|
|
more++;
|
|
continue;
|
|
}
|
|
const int y = top + rows++ * list_h;
|
|
const bool urgent = e.kind == WASTE && e.day <= today + 1;
|
|
if (urgent)
|
|
it.filled_rectangle(16, y, w - 32, list_h - 4, id(ink));
|
|
const Color fg = urgent ? id(paper) : id(ink);
|
|
const int ty = y + (list_h - 4) / 2;
|
|
if (e.day == today)
|
|
it.print(28, ty, id(font_small), fg, TextAlign::CENTER_LEFT, "Dnes");
|
|
else if (e.day == today + 1)
|
|
it.print(28, ty, id(font_small), fg, TextAlign::CENTER_LEFT, "Zajtra");
|
|
else {
|
|
const auto t = ESPTime::from_epoch_local(uint32_t(e.day) * 86400 + 43200);
|
|
it.printf(28, ty, id(font_small), fg, TextAlign::CENTER_LEFT, "%s %d. %d.", DAYS_SHORT[weekday(e.day)],
|
|
t.day_of_month, t.month);
|
|
}
|
|
if (e.kind < 2)
|
|
it.image(150, ty - 16, icons[e.kind], fg);
|
|
it.start_clipping(194, y, w - 28, y + list_h);
|
|
if (e.minute == ALL_DAY)
|
|
it.print(194, ty, id(font_small), fg, TextAlign::CENTER_LEFT, e.summary);
|
|
else
|
|
it.printf(194, ty, id(font_small), fg, TextAlign::CENTER_LEFT, "%02d:%02d %s", e.minute / 60, e.minute % 60,
|
|
e.summary);
|
|
it.end_clipping();
|
|
}
|
|
if (!ev.valid())
|
|
it.print(cx, top + 20, id(font_small), id(ink), TextAlign::TOP_CENTER, "Kalendár nie je k dispozícii");
|
|
else if (rows == 0)
|
|
it.print(cx, top + 20, id(font_small), id(ink), TextAlign::TOP_CENTER, "Žiadne udalosti v najbližších 14 dňoch");
|
|
else if (more > 0)
|
|
it.printf(w - 28, top + rows * list_h, id(font_small), id(ink), TextAlign::TOP_RIGHT, "+%d ďalšie", more);
|
|
|
|
// footer: IPv6 address (only while WiFi is up), last download
|
|
it.horizontal_line(24, 910, w - 48, id(ink));
|
|
char buf[network::IP_ADDRESS_BUFFER_SIZE];
|
|
for (auto &ip : wifi::global_wifi_component->get_ip_addresses()) {
|
|
if (!ip.is_set() || !ip.is_ip6())
|
|
continue;
|
|
ip.str_to(buf);
|
|
if (strncmp(buf, "fe80", 4) != 0) {
|
|
it.print(24, 944, id(font_footer), id(ink), TextAlign::BOTTOM_LEFT, buf);
|
|
break;
|
|
}
|
|
}
|
|
if (ev.valid()) {
|
|
const auto f = ESPTime::from_epoch_local(ev.fetched_at);
|
|
it.printf(w - 24, 944, id(font_footer), id(ink), TextAlign::BOTTOM_RIGHT, "%d. %d. %02d:%02d", f.day_of_month,
|
|
f.month, f.hour, f.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
|