Compare commits

...
5 Commits
Author SHA1 Message Date
randy 369578a63a mcu-nspanel-office-j: dim the display at night like the bedroom panel
CI / ESPHome config validation (push) Failing after 8s
CI / YAML lint (push) Failing after 38s
Inactive display dim follows the sun (5 % by day, off after dusk at
-6 degrees) and stays at 5 % while light.office_j_lights is on.
Re-applied on time sync, at dawn/dusk and when the lights change.
2026-09-27 20:36:59 +02:00
randy bdf49db1c2 mcu-lvgl-trainer: six pages for the pain cave, fix touch rotation
- touch: drop calibration/transform, the driver defaults to the native
  320x480 panel and LVGL rotates touch points itself (taps were rotated
  by 90 degrees)
- pages: clock (default), basement lights with brightness and color
  temperature for the exercise room (sliders hidden while it is off),
  training start/stop, fan off/50-100 %, stopwatch, status screen with
  "Made for ESPHome" logo and IPv6 first
- light attributes read as text, 'None' while off is skipped
- unused fonts, colors and leftovers removed; ext_uart kept for a
  future ANT+ bridge
2026-09-27 20:31:37 +02:00
randy 349f699572 mcu-stackchan: "Made for ESPHome" logo, IPv6 on status screen
- status screen logo replaced by the official "Made for ESPHome" badge,
  the plain ESPHome logo is not meant for third-party devices
- status screen shows one global IPv6 and one IPv4 address (link-local
  skipped) instead of the IPv4 sensor value
2026-09-27 19:49:57 +02:00
randy 329830e553 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
2026-09-27 19:49:57 +02:00
randy d813717d2c common: enable ha_time in common packages, devices extend it
Move the homeassistant time platform (id ha_time) into all common
packages. Devices with custom triggers now use '!extend ha_time';
plain duplicate time blocks are removed. mcu-lvgl's unused time_comp
is unified to ha_time.
2026-09-27 16:43:35 +02:00
37 changed files with 1376 additions and 320 deletions

No files matched your search

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1194" height="240" fill="none" viewBox="0 0 1194 240"><g clip-path="url(#clip0_2_63)"><path fill="#18BCF2" d="M240 219C240 227.24 233.24 234 225 234H15C6.76 234 0 227.24 0 219V129C0 120.76 4.78 109.22 10.6 103.4L109.4 4.61999C115.24 -1.22001 124.78 -1.22001 130.62 4.61999L229.4 103.4C235.24 109.24 240 120.76 240 129V219Z"/><path fill="#F2F4F9" d="M160 78.66H80C76.68 78.66 74 81.34 74 84.66V234H86V90.66H154V102.66H104C100.68 102.66 98 105.34 98 108.66V132.66C98 135.98 100.68 138.66 104 138.66H154V150.66H104C100.68 150.66 98 153.34 98 156.66V180.66C98 183.98 100.68 186.66 104 186.66H154V198.66H104C100.68 198.66 98 201.34 98 204.66C98 207.98 100.68 210.66 104 210.66H160C163.32 210.66 166 207.98 166 204.66V180.66C166 177.34 163.32 174.66 160 174.66H110V162.66H160C163.32 162.66 166 159.98 166 156.66V132.66C166 129.34 163.32 126.66 160 126.66H110V114.66H160C163.32 114.66 166 111.98 166 108.66V84.66C166 81.34 163.32 78.66 160 78.66Z"/><path fill="#F2F4F9" d="M423 167.61V190.11H336V49.95H423V72.63H359.82V108.91H416.26V130.01H359.82V167.61H423Z"/><path fill="#F2F4F9" d="M491.08 47.41C503.9 47.41 514.54 50.53 523.04 56.79C531.54 63.03 536.76 71.39 538.7 81.83L516.3 87.93C515.04 81.93 512.12 77.31 507.54 74.05C502.94 70.81 497.28 69.17 490.52 69.17C483.26 69.17 477.52 70.91 473.28 74.37C469.02 77.83 466.9 82.45 466.9 88.19C466.9 97.25 472.52 103.13 483.78 105.81L506.66 111.53C518.48 114.59 527.32 119.45 533.2 126.11C539.08 132.77 542.02 140.97 542.02 150.71C542.02 163.21 537.48 173.31 528.42 181.03C519.36 188.75 507.24 192.61 492.04 192.61C478.36 192.61 466.78 189.39 457.36 182.95C448.04 176.27 442.54 167.45 440.86 156.51L463.26 150.69C464.08 156.87 467.16 161.73 472.5 165.23C477.84 168.73 484.64 170.47 492.9 170.47C501.16 170.47 507.08 168.79 511.6 165.45C516.14 162.11 518.4 157.57 518.4 151.81C518.4 142.81 512.78 136.87 501.52 133.99L478.64 128.45C466.88 125.77 458.06 120.99 452.16 114.15C446.26 107.31 443.3 98.95 443.3 89.07C443.3 76.69 447.7 66.65 456.48 58.93C465.26 51.21 476.8 47.35 491.12 47.35L491.08 47.41Z"/><path fill="#F2F4F9" d="M666.1 95.61C666.1 109.37 661.92 120.49 653.58 128.99C645.24 137.49 634 141.75 619.88 141.75H588.38V190.13H564.56V49.95H620.06C634.3 49.95 645.54 54.07 653.76 62.27C661.98 70.49 666.08 81.59 666.08 95.59L666.1 95.61ZM641.16 95.05C641.16 87.99 638.96 82.25 634.56 77.85C630.16 73.45 623.92 71.25 615.86 71.25H588.4V120.85H615.5C623.74 120.85 630.1 118.51 634.54 113.81C638.98 109.13 641.2 102.87 641.2 95.05H641.16Z"/><path fill="#F2F4F9" d="M687.38 49.95H711.2V108.55L772.98 108.73V49.95H796.98V190.11H772.98V129.83L711.2 129.65V190.11H687.38V49.95Z"/><path fill="#F2F4F9" d="M868.6 92.79C883.1 92.79 894.92 97.45 904.04 106.75C913.16 116.07 917.72 128.03 917.72 142.65C917.72 157.27 913.16 169.15 904.04 178.47C894.92 187.79 883.1 192.43 868.6 192.43C854.1 192.43 842.1 187.77 832.98 178.47C823.86 169.15 819.3 157.23 819.3 142.65C819.3 128.07 823.86 115.97 832.98 106.69C842.1 97.41 853.98 92.77 868.6 92.77V92.79ZM868.6 172.01C876.34 172.01 882.64 169.27 887.5 163.81C892.34 158.35 894.76 151.23 894.76 142.49C894.76 133.75 892.34 126.67 887.5 121.25C882.66 115.85 876.36 113.15 868.6 113.15C860.84 113.15 854.26 115.85 849.38 121.25C844.5 126.65 842.06 133.73 842.06 142.49C842.06 151.25 844.5 158.35 849.38 163.81C854.26 169.27 860.66 172.01 868.6 172.01Z"/><path fill="#F2F4F9" d="M1081.6 129.83V190.11H1059.56V133.29C1059.56 126.73 1057.88 121.61 1054.5 117.91C1051.12 114.23 1046.54 112.37 1040.72 112.37C1034.54 112.37 1029.6 114.39 1025.9 118.41C1022.22 122.45 1020.36 128.09 1020.36 135.33V190.09H998.04V133.27C998.04 126.71 996.4 121.59 993.12 117.89C989.84 114.21 985.3 112.35 979.48 112.35C973.3 112.35 968.32 114.37 964.58 118.39C960.84 122.41 958.96 128.07 958.96 135.31V190.07H936.28V95.39H957.38L958.14 104.49C963.64 96.43 972.6 92.39 985.04 92.39C992.28 92.39 998.58 93.89 1003.88 96.89C1009.2 99.89 1013.22 104.27 1015.98 110.01C1018.3 104.51 1022.12 100.19 1027.46 97.07C1032.8 93.95 1039.16 92.39 1046.54 92.39C1057.28 92.39 1065.82 95.71 1072.14 102.33C1078.46 108.95 1081.6 118.11 1081.6 129.79V129.83Z"/><path fill="#F2F4F9" d="M1193.54 151.11H1120.98C1121.74 158.23 1124.42 163.67 1129.04 167.43C1133.66 171.19 1139.54 173.05 1146.66 173.05C1158.22 173.05 1166.32 168.23 1170.94 158.61L1189.88 166.11C1186.32 174.43 1180.7 180.93 1173.06 185.61C1165.4 190.29 1156.6 192.65 1146.66 192.65C1132.66 192.65 1121.3 188.07 1112.58 178.91C1103.86 169.75 1099.5 157.73 1099.5 142.87C1099.5 128.01 1103.9 115.91 1112.68 106.59C1121.46 97.27 1132.94 92.63 1147.14 92.63C1161.34 92.63 1172.3 97.23 1180.8 106.41C1189.3 115.59 1193.56 127.69 1193.56 142.69V151.13L1193.54 151.11ZM1121.36 135.17H1170.48C1170.04 127.49 1167.72 121.63 1163.5 117.59C1159.28 113.55 1153.6 111.55 1146.48 111.55C1139.36 111.55 1133.66 113.59 1129.24 117.69C1124.8 121.79 1122.18 127.61 1121.36 135.17Z"/></g><defs><clipPath id="clip0_2_63"><rect width="1193.56" height="240" fill="#fff"/></cliLine truncated

Before

Width:  |  Height:  |  Size: 4.9 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: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

+2 -2
View File
@@ -24,8 +24,8 @@ text_sensor:
# - <<: !include text_sensor/uptime.yaml
- <<: !include text_sensor/wifi_info.yaml
# time:
#. - <<: !include time/homeassistant.yaml
time:
- <<: !include time/homeassistant.yaml
network:
enable_ipv6: true
+3
View File
@@ -24,5 +24,8 @@ text_sensor:
- <<: !include text_sensor/version.yaml
- <<: !include text_sensor/wifi_info.yaml
time:
- <<: !include time/homeassistant.yaml
network:
enable_ipv6: true
+3
View File
@@ -23,5 +23,8 @@ text_sensor:
- <<: !include text_sensor/version.yaml
- <<: !include text_sensor/wifi_info.yaml
time:
- <<: !include time/homeassistant.yaml
network:
enable_ipv6: true
+3
View File
@@ -21,5 +21,8 @@ text_sensor:
- <<: !include text_sensor/version.yaml
- <<: !include text_sensor/ethernet_info.yaml
time:
- <<: !include time/homeassistant.yaml
network:
enable_ipv6: true
+3
View File
@@ -27,3 +27,6 @@ text_sensor:
- <<: !include text_sensor/git_ref.yaml
- <<: !include text_sensor/version.yaml
- <<: !include text_sensor/wifi_info.yaml
time:
- <<: !include time/homeassistant.yaml
+176
View File
@@ -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
+1 -2
View File
@@ -491,8 +491,7 @@ sensor:
# name: Dallas Temp 4
# one_wire_id: hub_1
time:
- !include common/time/homeassistant.yaml
# time:
# - platform: sntp
# id: sntp_time
# timezone: Europe/Bratislava
+1 -2
View File
@@ -139,8 +139,7 @@ switch:
id: relay_2
time:
- platform: homeassistant
timezone: Europe/Bratislava
- id: !extend ha_time
on_time:
- seconds: 0
minutes: /15
-5
View File
@@ -31,11 +31,6 @@ packages:
debug:
time:
- platform: homeassistant
id: ha_time
timezone: Europe/Bratislava
i2c:
# - id: i2c0
sda: 32
-5
View File
@@ -109,11 +109,6 @@ globals:
restore_value: no
initial_value: "false"
time:
- platform: homeassistant
id: ha_time
timezone: Europe/Bratislava
display:
- platform: tm1637
id: tm1637_display
-5
View File
@@ -51,11 +51,6 @@ globals:
restore_value: no
initial_value: "false"
time:
- platform: homeassistant
id: ha_time
timezone: Europe/Bratislava
binary_sensor:
- platform: gpio
name: Button
-5
View File
@@ -242,11 +242,6 @@ binary_sensor:
# on_clockwise:
# - display_menu.down:
time:
- platform: homeassistant
id: ha_time
timezone: Europe/Bratislava
# display:
# - platform: tm1637
# id: tm1637_display
-5
View File
@@ -44,11 +44,6 @@ packages:
# Relay1 G22
# Relay2 G19
time:
- platform: homeassistant
id: ha_time
timezone: Europe/Bratislava
i2c:
# - id: i2c0
# sda: 26
+1 -3
View File
@@ -129,9 +129,7 @@ globals:
initial_value: '20.5'
time:
- platform: homeassistant
id: ha_time
timezone: Europe/Bratislava
- id: !extend ha_time
on_time:
- seconds: 0
minutes: /1
-5
View File
@@ -55,11 +55,6 @@ api:
# Relay1 G22
# Relay2 G19
time:
- platform: homeassistant
id: ha_time
timezone: Europe/Bratislava
i2c:
# - id: i2c0
# sda: 26
+556 -140
View File
@@ -4,6 +4,13 @@ substitutions:
name: Trainer Display
area: Basement
comment: "${area} | Pain Cave"
light_basement: light.mcu_basement_2_light
light_workbench: light.mcu_basement_2_workbench_light
light_exercise: light.exercise_room
fan: light.mcu_dimmer_exercise_room_fan
# Pages (480x320, navigation bar at the bottom):
# clock, lights, training, fan, stopwatch, status
esphome:
name: mcu-${device}
@@ -17,7 +24,6 @@ esp32:
framework:
type: esp-idf
packages:
common: !include common/common.yaml
@@ -36,71 +42,17 @@ spi:
clk_pin: 47
data_pins: [21, 48, 40, 39]
color:
- id: my_red
red: 100%
green: 0%
blue: 0%
- id: my_orange
red: 100%
green: 50%
blue: 0%
- id: my_yellow
red: 100%
green: 100%
blue: 0%
- id: my_green
red: 0%
green: 100%
blue: 0%
- id: my_blue
red: 0%
green: 0%
blue: 100%
- id: my_teal
red: 0%
green: 100%
blue: 100%
- id: my_gray
red: 70%
green: 70%
blue: 70%
- id: my_white
red: 100%
green: 100%
blue: 100%
font:
- file: "gfonts://Roboto"
id: Roboto_48
size: 48
- file: "gfonts://Roboto"
id: Roboto_36
size: 36
- file: "gfonts://Roboto"
id: Roboto_24
size: 24
- file: "gfonts://Roboto"
id: Roboto_12
size: 12
- file: "gfonts://Roboto"
id: font_std
size: 40
glyphs: "!\"%()+=,-_.:°0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz/\\[]|&@#'"
- file: "gfonts://Roboto@700"
id: font_title
size: 40
glyphs: "!\"%()+=,-_.:°0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz/\\[]|&@#'"
globals:
- id: bgcolor
type: Color
initial_value: "Color::random_color()"
# power_supply:
# id: main_backlight
# pin: 1
# enable_on_boot: true
# stopwatch: accumulated time of finished runs + start of the current run
- id: sw_running
type: bool
initial_value: 'false'
- id: sw_start
type: uint32_t
initial_value: '0'
- id: sw_accum
type: uint32_t
initial_value: '0'
output:
- platform: ledc
@@ -119,7 +71,6 @@ display:
- platform: mipi_spi
model: AXS15231
data_rate: 40MHz #20mhz is default
#spi_mode: MODE2
id: main_display
spi_id: display_qspi
dimensions:
@@ -127,11 +78,6 @@ display:
width: 320
cs_pin: 45
auto_clear_enabled: false
# show_test_card: true
# lambda: |-
# it.fill(id(bgcolor));
# it.print(it.get_width()/2, it.get_height()/2-20, id(font_title), TextAlign::CENTER, "ESPHome");
# it.print(it.get_width()/2, it.get_height()/2+20, id(font_std), TextAlign::CENTER, "@UTBDK");
touchscreen:
- platform: axs15231
@@ -142,27 +88,8 @@ touchscreen:
# interrupt_pin:
# number: 3
# ignore_strapping_warning: true
calibration:
x_min: 0
x_max: 479
y_min: 0
y_max: 319
transform:
swap_xy: true
mirror_x: false
mirror_y: true
# on_touch:
# - lambda: |-
# Color newColor;
# do { newColor = Color::random_color(); } while (newColor == id(bgcolor));
# id(bgcolor) = newColor;
# ESP_LOGI("cal", "x=%d, y=%d, x_raw=%d, y_raw=%0d",
# touch.x,
# touch.y,
# touch.x_raw,
# touch.y_raw
# );
# no calibration/transform: the driver defaults to the native 320x480 panel and
# LVGL applies its own rotation to touch points
on_release:
- if:
condition: lvgl.is_paused
@@ -183,107 +110,596 @@ uart:
binary_sensor:
- platform: homeassistant
id: exercise_room_light
entity_id: light.exercise_room
id: basement_light
entity_id: ${light_basement}
trigger_on_initial_state: true
on_state:
then:
lvgl.widget.update:
id: exercise_room_light_btn
- lvgl.widget.update:
id: basement_light_btn
state:
checked: !lambda return x;
- platform: homeassistant
id: exercise_room_fan
entity_id: light.mcu_dimmer_exercise_room_fan
id: workbench_light
entity_id: ${light_workbench}
trigger_on_initial_state: true
on_state:
then:
lvgl.widget.update:
id: exercise_room_fan_btn
- lvgl.widget.update:
id: workbench_light_btn
state:
checked: !lambda return x;
- platform: homeassistant
id: exercise_light
entity_id: ${light_exercise}
trigger_on_initial_state: true
on_state:
- lvgl.widget.update:
id: exercise_light_btn
state:
checked: !lambda return x;
# brightness and color temperature only make sense while the light is on
- if:
condition:
lambda: return x;
then:
- lvgl.widget.show: exercise_controls
else:
- lvgl.widget.hide: exercise_controls
- platform: homeassistant
id: fan_on
entity_id: ${fan}
trigger_on_initial_state: true
on_state:
- script.execute: update_fan_buttons
sensor:
- platform: homeassistant
id: fan_brightness
entity_id: ${fan}
attribute: brightness
on_value:
- script.execute: update_fan_buttons
text_sensor:
# light attributes read as text: they are 'None' while the light is off
- platform: homeassistant
id: exercise_brightness
entity_id: ${light_exercise}
attribute: brightness
on_value:
- if:
condition:
lambda: return parse_number<float>(x).has_value();
then:
- lvgl.slider.update:
id: exercise_brightness_slider
value: !lambda return roundf(*parse_number<float>(x) * 100.0f / 255.0f);
- platform: homeassistant
id: exercise_color_temp
entity_id: ${light_exercise}
attribute: color_temp_kelvin
on_value:
- if:
condition:
lambda: return parse_number<float>(x).has_value();
then:
- lvgl.slider.update:
id: exercise_color_temp_slider
value: !lambda return *parse_number<float>(x);
# a running automation (e.g. its delay) has current > 0
- platform: homeassistant
id: training_start_running
entity_id: automation.zwift_enable_fan
attribute: current
on_value:
- script.execute: update_training_status
- platform: homeassistant
id: training_stop_running
entity_id: automation.zwift_disable_fan
attribute: current
on_value:
- script.execute: update_training_status
script:
- id: update_fan_buttons
then:
- lambda: |-
// fan level rounded to the 10 % steps of the buttons, 0 = off
int pct = 0;
if (id(fan_on).state && !std::isnan(id(fan_brightness).state))
pct = (int) roundf(id(fan_brightness).state * 10.0f / 255.0f) * 10;
const int levels[] = {0, 50, 60, 70, 80, 90, 100};
lv_obj_t *matrix = id(fan_matrix)->obj;
for (int i = 0; i < 7; i++) {
if (levels[i] == pct)
lv_buttonmatrix_set_button_ctrl(matrix, i, LV_BUTTONMATRIX_CTRL_CHECKED);
else
lv_buttonmatrix_clear_button_ctrl(matrix, i, LV_BUTTONMATRIX_CTRL_CHECKED);
}
- lvgl.label.update:
id: fan_status_label
text: !lambda |-
if (!id(fan_on).state)
return std::string("Ventilátor vypnutý");
if (std::isnan(id(fan_brightness).state))
return std::string("Ventilátor zapnutý");
return str_sprintf("Ventilátor %.0f %%", id(fan_brightness).state * 100.0f / 255.0f);
- id: update_training_status
then:
- lvgl.label.update:
id: training_status_label
text: !lambda |-
if (id(training_start_running).state != "0" && !id(training_start_running).state.empty())
return std::string("Tréning sa spúšťa, ventilátor sa zapne o 10 min");
if (id(training_stop_running).state != "0" && !id(training_stop_running).state.empty())
return std::string("Tréning sa ukončuje, ventilátor sa vypne o 2 min");
return std::string("");
- id: update_stopwatch
then:
- lvgl.label.update:
id: stopwatch_label
text: !lambda |-
const uint32_t ms = id(sw_accum) + (id(sw_running) ? millis() - id(sw_start) : 0);
const uint32_t s = ms / 1000;
if (s >= 3600)
return str_sprintf("%u:%02u:%02u", s / 3600, (s / 60) % 60, s % 60);
return str_sprintf("%02u:%02u.%u", s / 60, s % 60, (ms / 100) % 10);
- lvgl.label.update:
id: stopwatch_start_label
text: !lambda |-
return std::string(id(sw_running) ? "Stop" : "Štart");
- id: update_status
then:
- lvgl.label.update:
id: status_label
text: !lambda |-
std::string out;
if (wifi::global_wifi_component->is_connected()) {
out = str_sprintf("WiFi: connected (%d dBm)", wifi::global_wifi_component->wifi_rssi());
// IPv6 first (skip link-local), then IPv4
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)
continue;
ip.str_to(buf);
if (v6 && strncmp(buf, "fe80", 4) == 0)
continue;
out += "\n ";
out += buf;
}
}
} else {
out = "WiFi: connecting...";
}
out += api::global_api_server->is_connected_with_state_subscription() ? "\nHome Assistant: connected"
: "\nHome Assistant: waiting...";
const uint32_t up = millis() / 1000;
out += str_sprintf("\nESPHome %s · up %ud %02u:%02u", ESPHOME_VERSION, up / 86400, (up / 3600) % 24,
(up / 60) % 60);
return out;
interval:
- interval: 1s
then:
- lvgl.label.update:
id: clock_label
text: !lambda |-
auto now = id(ha_time).now();
if (!now.is_valid())
return std::string("--:--:--");
return now.strftime("%H:%M:%S");
- interval: 100ms
then:
- if:
condition:
and:
- lambda: return id(sw_running);
- lvgl.page.is_showing: stopwatch_page
then:
- script.execute: update_stopwatch
- interval: 2s
then:
- if:
condition:
lvgl.page.is_showing: status_page
then:
- script.execute: update_status
image:
- platform: file
file: assets/images/made-for-esphome-white-on-black.svg
id: esphome_logo
resize: 280x83
type: RGB565
transparency: alpha_channel
font:
- file: "gfonts://Roboto@500"
id: font_digits
size: 110
glyphs: "0123456789:.-"
- file: "gfonts://Roboto"
id: font_ui
size: 24
glyphs: &sk_glyphs "ÁÄČĎÉÍĹĽŇÓÔŔŠŤÚÝŽáäčďéíĺľňóôŕšťúýž°·"
glyphsets: [GF_Latin_Kernel]
- file: "gfonts://Roboto@500"
id: font_button
size: 34
glyphs: *sk_glyphs
glyphsets: [GF_Latin_Kernel]
- file: "gfonts://Roboto"
id: font_small
size: 18
glyphs: *sk_glyphs
glyphsets: [GF_Latin_Kernel]
lvgl:
rotation: 90
buffer_size: 100%
log_level: WARN
color_depth: 16
default_font: montserrat_18
# on_idle:
# - timeout: !lambda "return (id(display_timeout).state * 1000);"
# then:
# - logger.log: "LVGL is idle"
# - light.turn_off:
# id: display_backlight
# transition_length: 2500ms
# - lvgl.pause:
default_font: font_ui
bg_color: 0x000000
text_color: 0xFFFFFF
theme:
button:
bg_color: 0x2B2F36
radius: 12
checked:
bg_color: 0xE08A00
buttonmatrix:
bg_opa: TRANSP
border_width: 0
items:
bg_color: 0x2B2F36
text_color: 0xFFFFFF
radius: 12
checked:
bg_color: 0xE08A00
slider:
bg_color: 0x2B2F36
indicator:
bg_color: 0xE08A00
knob:
bg_color: 0xFFFFFF
top_layer:
widgets:
- buttonmatrix:
id: nav_bar
align: bottom_mid
# styles: header_footer
pad_all: 0
outline_width: 0
id: top_layer
# items:
# styles: header_footer
width: 100%
height: 56
pad_all: 4
pad_column: 6
items:
text_font: montserrat_28
rows:
- buttons:
- id: page_prev
text: "\uF053"
text: ""
on_press:
then:
lvgl.page.previous:
- id: page_home
text: "\uF015"
text: ""
on_press:
then:
lvgl.page.show: main_page
lvgl.page.show: clock_page
- id: page_next
text: "\uF054"
text: ""
on_press:
then:
lvgl.page.next:
pages:
- id: main_page
# 1: clock (default after boot)
- id: clock_page
widgets:
- label:
text: "ESPHome LVGL Display"
id: clock_label
align: CENTER
text_align: CENTER
text_color: 0xFFFFFF
- id: excercise_page
y: -28
text_font: font_digits
text: "--:--:--"
# 2: basement lights
- id: lights_page
pad_all: 10
widgets:
- label:
align: CENTER
text: 'Hello World!'
- button:
id: exercise_room_light_btn
id: basement_light_btn
align: TOP_LEFT
width: 100
height: 70
width: 148
height: 80
checkable: true
widgets:
- label:
align: CENTER
text: 'Light'
text: "Pivnica"
on_click:
- homeassistant.action:
action: light.toggle
data:
entity_id: light.exercise_room
entity_id: ${light_basement}
- button:
id: exercise_room_fan_btn
align: TOP_RIGHT
width: 100
height: 70
id: workbench_light_btn
align: TOP_MID
width: 148
height: 80
checkable: true
widgets:
- label:
align: CENTER
text: 'Fan'
text: "Pracovný stôl"
on_click:
- homeassistant.action:
action: light.toggle
data:
entity_id: light.mcu_dimmer_exercise_room_fan
entity_id: ${light_workbench}
- button:
id: exercise_light_btn
align: TOP_RIGHT
width: 148
height: 80
checkable: true
widgets:
- label:
align: CENTER
text: "Posilka"
on_click:
- homeassistant.action:
action: light.toggle
data:
entity_id: ${light_exercise}
- obj:
id: exercise_controls
hidden: true
y: 110
width: 460
height: 120
pad_all: 0
bg_opa: TRANSP
border_width: 0
scrollbar_mode: "OFF"
widgets:
- label:
x: 4
y: 0
text_font: font_small
text: "Posilka jas"
- slider:
id: exercise_brightness_slider
x: 14
y: 32
width: 430
height: 16
min_value: 1
max_value: 100
value: 100
on_release:
- homeassistant.action:
action: light.turn_on
data:
entity_id: ${light_exercise}
brightness_pct: !lambda return (int) x;
- label:
x: 4
y: 62
text_font: font_small
text: "Posilka teplota (2700 – 6500 K)"
- slider:
id: exercise_color_temp_slider
x: 14
y: 94
width: 430
height: 16
min_value: 2700
max_value: 6500
value: 2700
on_release:
- homeassistant.action:
action: light.turn_on
data:
entity_id: ${light_exercise}
color_temp_kelvin: !lambda return (int) x;
# 3: training start / stop
- id: training_page
pad_all: 10
widgets:
- button:
align: TOP_LEFT
y: 10
width: 222
height: 150
bg_color: 0x1E7D32
widgets:
- label:
align: CENTER
text_font: font_button
text: "Štart"
on_click:
- homeassistant.action:
action: automation.trigger
data:
entity_id: automation.zwift_enable_fan
- button:
align: TOP_RIGHT
y: 10
width: 222
height: 150
bg_color: 0xB71C1C
widgets:
- label:
align: CENTER
text_font: font_button
text: "Stop"
on_click:
- homeassistant.action:
action: automation.trigger
data:
entity_id: automation.zwift_disable_fan
- label:
id: training_status_label
align: TOP_MID
y: 185
width: 440
text_align: CENTER
text_font: font_small
text: ""
# 4: fan
- id: fan_page
pad_all: 10
widgets:
- buttonmatrix:
id: fan_matrix
align: TOP_MID
width: 460
height: 180
pad_row: 8
pad_column: 8
one_checked: true
items:
text_font: font_button
rows:
- buttons:
- text: "Vyp"
control:
checkable: true
on_press:
- homeassistant.action:
action: light.turn_off
data:
entity_id: ${fan}
- text: "50"
control:
checkable: true
on_press:
- homeassistant.action:
action: light.turn_on
data:
entity_id: ${fan}
brightness_pct: "50"
- text: "60"
control:
checkable: true
on_press:
- homeassistant.action:
action: light.turn_on
data:
entity_id: ${fan}
brightness_pct: "60"
- text: "70"
control:
checkable: true
on_press:
- homeassistant.action:
action: light.turn_on
data:
entity_id: ${fan}
brightness_pct: "70"
- buttons:
- text: "80"
control:
checkable: true
on_press:
- homeassistant.action:
action: light.turn_on
data:
entity_id: ${fan}
brightness_pct: "80"
- text: "90"
control:
checkable: true
on_press:
- homeassistant.action:
action: light.turn_on
data:
entity_id: ${fan}
brightness_pct: "90"
- text: "100"
control:
checkable: true
on_press:
- homeassistant.action:
action: light.turn_on
data:
entity_id: ${fan}
brightness_pct: "100"
- label:
id: fan_status_label
align: TOP_MID
y: 200
text_font: font_small
text: ""
# 5: stopwatch
- id: stopwatch_page
pad_all: 10
widgets:
- label:
id: stopwatch_label
align: TOP_MID
y: 10
text_font: font_digits
text: "00:00.0"
- button:
align: TOP_LEFT
y: 150
width: 222
height: 90
widgets:
- label:
id: stopwatch_start_label
align: CENTER
text_font: font_button
text: "Štart"
on_click:
- lambda: |-
if (id(sw_running)) {
id(sw_accum) += millis() - id(sw_start);
id(sw_running) = false;
} else {
id(sw_start) = millis();
id(sw_running) = true;
}
- script.execute: update_stopwatch
- button:
align: TOP_RIGHT
y: 150
width: 222
height: 90
widgets:
- label:
align: CENTER
text_font: font_button
text: "Reset"
on_click:
- lambda: |-
id(sw_accum) = 0;
id(sw_start) = millis();
- script.execute: update_stopwatch
# 6: status
- id: status_page
on_load:
- script.execute: update_status
widgets:
- image:
src: esphome_logo
align: TOP_MID
y: 8
- label:
align: TOP_MID
y: 96
text: "${name}"
- label:
id: status_label
x: 20
y: 130
text_font: font_small
text: ""
-4
View File
@@ -158,10 +158,6 @@ text_sensor:
icon: mdi:clock-start
internal: True
time:
- platform: homeassistant
id: time_comp
number:
- platform: template
initial_value: 0
+564 -77
View File
@@ -4,94 +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, &reg, 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:
- platform: homeassistant
id: ha_time
timezone: Europe/Bratislava
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
@@ -103,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
@@ -156,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
+1 -3
View File
@@ -228,9 +228,7 @@ button:
- pzemac.reset_energy: pzemac_l3
time:
- platform: homeassistant
id: ha_time
timezone: Europe/Bratislava
- id: !extend ha_time
on_time:
- cron: '0 0 0 * * *'
then:
+1 -3
View File
@@ -136,9 +136,7 @@ nspanel_lovelace:
thermo_entity_id: climate.bt_trv_bedroom
time:
- platform: homeassistant
id: ha_time
timezone: Europe/Bratislava
- id: !extend ha_time
on_time_sync:
- script.execute: apply_sleep_dim
-5
View File
@@ -540,11 +540,6 @@ sensor:
- platform: template
id: room_temperature_id
time:
platform: homeassistant
id: ha_time
timezone: Europe/Bratislava
text_sensor:
- platform: homeassistant
id: name_day
-5
View File
@@ -185,11 +185,6 @@ nspanel_lovelace:
- name: Auto-zatváranie
entity_id: switch.gate_auto_close
time:
- platform: homeassistant
id: ha_time
timezone: Europe/Bratislava
binary_sensor:
- platform: gpio
name: Left Button
+33 -3
View File
@@ -38,6 +38,16 @@ external_components:
packages:
common: !include common/common.yaml
script:
- id: apply_sleep_dim
then:
- lambda: |-
uint8_t dim;
if (id(bedroom_lights).state) dim = 5; // sleepOverride
else if (id(sun_id).is_above_horizon(-6)) dim = 5; // day
else dim = 0; // night
id(nspanel_id).set_display_inactive_dim(dim);
api:
homeassistant_services: true
homeassistant_states: true
@@ -135,9 +145,9 @@ nspanel_lovelace:
entity_id: switch.gate_auto_close
time:
- platform: homeassistant
id: ha_time
timezone: Europe/Bratislava
- id: !extend ha_time
on_time_sync:
- script.execute: apply_sleep_dim
binary_sensor:
- platform: gpio
@@ -161,6 +171,12 @@ binary_sensor:
entity_id: light.bedside_left
- lambda: 'id(nspanel_id).render_default_page();'
- platform: homeassistant # sleepOverride
id: bedroom_lights
entity_id: light.office_j_lights
on_state:
- script.execute: apply_sleep_dim
output:
- platform: ledc
id: buzzer_out
@@ -186,6 +202,20 @@ light:
# # name: Relay 2
# # output: relay_2
sun:
id: sun_id
time_id: ha_time
latitude: 48.15°
longitude: 17.11°
on_sunrise:
- elevation: -6°
then:
- script.execute: apply_sleep_dim
on_sunset:
- elevation: -6°
then:
- script.execute: apply_sleep_dim
switch:
- platform: gpio
name: Screen Power
-5
View File
@@ -158,11 +158,6 @@ nspanel_lovelace:
- name: Auto-zatváranie
entity_id: switch.gate_auto_close
time:
- platform: homeassistant
id: ha_time
timezone: Europe/Bratislava
binary_sensor:
- platform: gpio
name: Left Button
-5
View File
@@ -214,11 +214,6 @@ nspanel_lovelace:
- name: Rast trávy
entity_id: automation.irrigation_grow_grass
time:
- platform: homeassistant
id: ha_time
timezone: Europe/Bratislava
binary_sensor:
- platform: gpio
name: Left Button
-5
View File
@@ -54,11 +54,6 @@ packages:
# Relay1 G22
# Relay2 G19
time:
- platform: homeassistant
id: ha_time
timezone: Europe/Bratislava
i2c:
# - id: i2c0
# sda: 26
-2
View File
@@ -187,5 +187,3 @@ switch:
- switch.turn_off: relay_off # bi-stable relay so no need to keep on
interlock: [relay_on]
time:
- !include common/time/homeassistant.yaml
+1 -4
View File
@@ -2,7 +2,7 @@
substitutions:
device: pow-washer
name: Washer POW
comment:
comment: "spare"
area: Washroom
update_interval: 10s
@@ -186,6 +186,3 @@ switch:
- delay: 500ms
- switch.turn_off: relay_off # bi-stable relay so no need to keep on
interlock: [relay_on]
time:
- !include common/time/homeassistant.yaml
-2
View File
@@ -109,5 +109,3 @@ switch:
number: 19
mode: OUTPUT
time:
- !include common/time/homeassistant.yaml
+1 -2
View File
@@ -121,8 +121,7 @@ switch:
# number: GPIO00
# inverted: True
time:
- !include common/time/homeassistant.yaml
# time:
# - platform: homeassistant
# timezone: Europe/Bratislava
# on_time:
+1 -2
View File
@@ -122,8 +122,7 @@ switch:
on_turn_off:
- light.turn_off: light_red
time:
- !include common/time/homeassistant.yaml
# time:
# - platform: homeassistant
# timezone: Europe/Bratislava
# on_time:
+1 -2
View File
@@ -144,8 +144,7 @@ switch:
# number: GPIO00
# inverted: True
time:
- !include common/time/homeassistant.yaml
# time:
# - platform: homeassistant
# timezone: Europe/Bratislava
# on_time:
+22 -9
View File
@@ -464,9 +464,9 @@ spi:
image:
- platform: file
file: assets/images/esphome-logo-text-on-dark.svg
file: assets/images/made-for-esphome-white-on-black.svg
id: esphome_logo
resize: 280x56
resize: 230x68
type: RGB565
transparency: alpha_channel
- platform: file
@@ -545,22 +545,36 @@ display:
if (id(show_status)) {
first_draw = true;
it.fill(id(face_bg));
it.image(160, 45, id(esphome_logo), ImageAlign::CENTER);
it.image(160, 46, id(esphome_logo), ImageAlign::CENTER);
it.print(160, 95, id(font_title), id(face_fg), TextAlign::CENTER, "${name}");
if (wifi_ok) {
it.printf(20, 135, id(font_status), id(status_ok), "WiFi: %s (%d dBm)",
id(ip_address_sensor).state.c_str(), rssi);
it.printf(20, 114, id(font_status), id(status_ok), "WiFi: connected (%d dBm)", rssi);
// one global IPv6 (skip link-local) and one IPv4 address
int y = 133;
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)
continue;
ip.str_to(buf);
if (v6 && strncmp(buf, "fe80", 4) == 0)
continue;
it.print(30, y, id(font_status), id(status_dim), buf);
y += 19;
break;
}
}
} else {
it.print(20, 135, id(font_status), id(status_wait), "WiFi: connecting...");
it.print(20, 114, id(font_status), id(status_wait), "WiFi: connecting...");
}
const bool api_ok = api::global_api_server->is_connected();
it.print(20, 162, id(font_status), api_ok ? id(status_ok) : id(status_wait),
it.print(20, 172, id(font_status), api_ok ? id(status_ok) : id(status_wait),
api_ok ? "Home Assistant: connected" : "Home Assistant: waiting...");
if (batt >= 0) {
it.printf(20, 189, id(font_status), batt <= 20 && !charging ? id(status_low) : id(status_dim),
it.printf(20, 191, id(font_status), batt <= 20 && !charging ? id(status_low) : id(status_dim),
"Battery: %d%%%s", batt, charging ? " (charging)" : "");
}
it.printf(20, 212, id(font_status), id(status_dim), "ESPHome %s", ESPHOME_VERSION);
@@ -883,4 +897,3 @@ time:
id: rtc_time
i2c_id: i2c_bsp
update_interval: never
- !include common/time/homeassistant.yaml
-2
View File
@@ -358,5 +358,3 @@ valve:
close_action:
- switch.turn_off: irrigation_relay_8
time:
- !include common/time/homeassistant.yaml