mirror of
https://github.com/randybb/esphome-configs.git
synced 2026-10-14 08:35:12 +02:00
Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
369578a63a | ||
|
|
bdf49db1c2 | ||
|
|
349f699572 | ||
|
|
329830e553 | ||
|
|
d813717d2c | ||
|
|
eea872b930 | ||
|
|
39e744900d |
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 |
+2
-2
@@ -24,8 +24,8 @@ text_sensor:
|
|||||||
# - <<: !include text_sensor/uptime.yaml
|
# - <<: !include text_sensor/uptime.yaml
|
||||||
- <<: !include text_sensor/wifi_info.yaml
|
- <<: !include text_sensor/wifi_info.yaml
|
||||||
|
|
||||||
# time:
|
time:
|
||||||
#. - <<: !include time/homeassistant.yaml
|
- <<: !include time/homeassistant.yaml
|
||||||
|
|
||||||
network:
|
network:
|
||||||
enable_ipv6: true
|
enable_ipv6: true
|
||||||
@@ -24,5 +24,8 @@ text_sensor:
|
|||||||
- <<: !include text_sensor/version.yaml
|
- <<: !include text_sensor/version.yaml
|
||||||
- <<: !include text_sensor/wifi_info.yaml
|
- <<: !include text_sensor/wifi_info.yaml
|
||||||
|
|
||||||
|
time:
|
||||||
|
- <<: !include time/homeassistant.yaml
|
||||||
|
|
||||||
network:
|
network:
|
||||||
enable_ipv6: true
|
enable_ipv6: true
|
||||||
@@ -23,5 +23,8 @@ text_sensor:
|
|||||||
- <<: !include text_sensor/version.yaml
|
- <<: !include text_sensor/version.yaml
|
||||||
- <<: !include text_sensor/wifi_info.yaml
|
- <<: !include text_sensor/wifi_info.yaml
|
||||||
|
|
||||||
|
time:
|
||||||
|
- <<: !include time/homeassistant.yaml
|
||||||
|
|
||||||
network:
|
network:
|
||||||
enable_ipv6: true
|
enable_ipv6: true
|
||||||
@@ -21,5 +21,8 @@ text_sensor:
|
|||||||
- <<: !include text_sensor/version.yaml
|
- <<: !include text_sensor/version.yaml
|
||||||
- <<: !include text_sensor/ethernet_info.yaml
|
- <<: !include text_sensor/ethernet_info.yaml
|
||||||
|
|
||||||
|
time:
|
||||||
|
- <<: !include time/homeassistant.yaml
|
||||||
|
|
||||||
network:
|
network:
|
||||||
enable_ipv6: true
|
enable_ipv6: true
|
||||||
@@ -27,3 +27,6 @@ text_sensor:
|
|||||||
- <<: !include text_sensor/git_ref.yaml
|
- <<: !include text_sensor/git_ref.yaml
|
||||||
- <<: !include text_sensor/version.yaml
|
- <<: !include text_sensor/version.yaml
|
||||||
- <<: !include text_sensor/wifi_info.yaml
|
- <<: !include text_sensor/wifi_info.yaml
|
||||||
|
|
||||||
|
time:
|
||||||
|
- <<: !include time/homeassistant.yaml
|
||||||
@@ -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
@@ -491,8 +491,7 @@ sensor:
|
|||||||
# name: Dallas Temp 4
|
# name: Dallas Temp 4
|
||||||
# one_wire_id: hub_1
|
# one_wire_id: hub_1
|
||||||
|
|
||||||
time:
|
# time:
|
||||||
- !include common/time/homeassistant.yaml
|
|
||||||
# - platform: sntp
|
# - platform: sntp
|
||||||
# id: sntp_time
|
# id: sntp_time
|
||||||
# timezone: Europe/Bratislava
|
# timezone: Europe/Bratislava
|
||||||
|
|||||||
+1
-2
@@ -139,8 +139,7 @@ switch:
|
|||||||
id: relay_2
|
id: relay_2
|
||||||
|
|
||||||
time:
|
time:
|
||||||
- platform: homeassistant
|
- id: !extend ha_time
|
||||||
timezone: Europe/Bratislava
|
|
||||||
on_time:
|
on_time:
|
||||||
- seconds: 0
|
- seconds: 0
|
||||||
minutes: /15
|
minutes: /15
|
||||||
|
|||||||
@@ -31,11 +31,6 @@ packages:
|
|||||||
|
|
||||||
debug:
|
debug:
|
||||||
|
|
||||||
time:
|
|
||||||
- platform: homeassistant
|
|
||||||
id: ha_time
|
|
||||||
timezone: Europe/Bratislava
|
|
||||||
|
|
||||||
i2c:
|
i2c:
|
||||||
# - id: i2c0
|
# - id: i2c0
|
||||||
sda: 32
|
sda: 32
|
||||||
|
|||||||
@@ -109,11 +109,6 @@ globals:
|
|||||||
restore_value: no
|
restore_value: no
|
||||||
initial_value: "false"
|
initial_value: "false"
|
||||||
|
|
||||||
time:
|
|
||||||
- platform: homeassistant
|
|
||||||
id: ha_time
|
|
||||||
timezone: Europe/Bratislava
|
|
||||||
|
|
||||||
display:
|
display:
|
||||||
- platform: tm1637
|
- platform: tm1637
|
||||||
id: tm1637_display
|
id: tm1637_display
|
||||||
|
|||||||
@@ -51,11 +51,6 @@ globals:
|
|||||||
restore_value: no
|
restore_value: no
|
||||||
initial_value: "false"
|
initial_value: "false"
|
||||||
|
|
||||||
time:
|
|
||||||
- platform: homeassistant
|
|
||||||
id: ha_time
|
|
||||||
timezone: Europe/Bratislava
|
|
||||||
|
|
||||||
binary_sensor:
|
binary_sensor:
|
||||||
- platform: gpio
|
- platform: gpio
|
||||||
name: Button
|
name: Button
|
||||||
|
|||||||
@@ -242,11 +242,6 @@ binary_sensor:
|
|||||||
# on_clockwise:
|
# on_clockwise:
|
||||||
# - display_menu.down:
|
# - display_menu.down:
|
||||||
|
|
||||||
time:
|
|
||||||
- platform: homeassistant
|
|
||||||
id: ha_time
|
|
||||||
timezone: Europe/Bratislava
|
|
||||||
|
|
||||||
# display:
|
# display:
|
||||||
# - platform: tm1637
|
# - platform: tm1637
|
||||||
# id: tm1637_display
|
# id: tm1637_display
|
||||||
|
|||||||
@@ -44,11 +44,6 @@ packages:
|
|||||||
# Relay1 G22
|
# Relay1 G22
|
||||||
# Relay2 G19
|
# Relay2 G19
|
||||||
|
|
||||||
time:
|
|
||||||
- platform: homeassistant
|
|
||||||
id: ha_time
|
|
||||||
timezone: Europe/Bratislava
|
|
||||||
|
|
||||||
i2c:
|
i2c:
|
||||||
# - id: i2c0
|
# - id: i2c0
|
||||||
# sda: 26
|
# sda: 26
|
||||||
|
|||||||
+1
-3
@@ -129,9 +129,7 @@ globals:
|
|||||||
initial_value: '20.5'
|
initial_value: '20.5'
|
||||||
|
|
||||||
time:
|
time:
|
||||||
- platform: homeassistant
|
- id: !extend ha_time
|
||||||
id: ha_time
|
|
||||||
timezone: Europe/Bratislava
|
|
||||||
on_time:
|
on_time:
|
||||||
- seconds: 0
|
- seconds: 0
|
||||||
minutes: /1
|
minutes: /1
|
||||||
|
|||||||
@@ -55,11 +55,6 @@ api:
|
|||||||
# Relay1 G22
|
# Relay1 G22
|
||||||
# Relay2 G19
|
# Relay2 G19
|
||||||
|
|
||||||
time:
|
|
||||||
- platform: homeassistant
|
|
||||||
id: ha_time
|
|
||||||
timezone: Europe/Bratislava
|
|
||||||
|
|
||||||
i2c:
|
i2c:
|
||||||
# - id: i2c0
|
# - id: i2c0
|
||||||
# sda: 26
|
# sda: 26
|
||||||
|
|||||||
+556
-140
@@ -4,6 +4,13 @@ substitutions:
|
|||||||
name: Trainer Display
|
name: Trainer Display
|
||||||
area: Basement
|
area: Basement
|
||||||
comment: "${area} | Pain Cave"
|
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:
|
esphome:
|
||||||
name: mcu-${device}
|
name: mcu-${device}
|
||||||
@@ -17,7 +24,6 @@ esp32:
|
|||||||
framework:
|
framework:
|
||||||
type: esp-idf
|
type: esp-idf
|
||||||
|
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
common: !include common/common.yaml
|
common: !include common/common.yaml
|
||||||
|
|
||||||
@@ -36,71 +42,17 @@ spi:
|
|||||||
clk_pin: 47
|
clk_pin: 47
|
||||||
data_pins: [21, 48, 40, 39]
|
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:
|
globals:
|
||||||
- id: bgcolor
|
# stopwatch: accumulated time of finished runs + start of the current run
|
||||||
type: Color
|
- id: sw_running
|
||||||
initial_value: "Color::random_color()"
|
type: bool
|
||||||
|
initial_value: 'false'
|
||||||
# power_supply:
|
- id: sw_start
|
||||||
# id: main_backlight
|
type: uint32_t
|
||||||
# pin: 1
|
initial_value: '0'
|
||||||
# enable_on_boot: true
|
- id: sw_accum
|
||||||
|
type: uint32_t
|
||||||
|
initial_value: '0'
|
||||||
|
|
||||||
output:
|
output:
|
||||||
- platform: ledc
|
- platform: ledc
|
||||||
@@ -119,7 +71,6 @@ display:
|
|||||||
- platform: mipi_spi
|
- platform: mipi_spi
|
||||||
model: AXS15231
|
model: AXS15231
|
||||||
data_rate: 40MHz #20mhz is default
|
data_rate: 40MHz #20mhz is default
|
||||||
#spi_mode: MODE2
|
|
||||||
id: main_display
|
id: main_display
|
||||||
spi_id: display_qspi
|
spi_id: display_qspi
|
||||||
dimensions:
|
dimensions:
|
||||||
@@ -127,11 +78,6 @@ display:
|
|||||||
width: 320
|
width: 320
|
||||||
cs_pin: 45
|
cs_pin: 45
|
||||||
auto_clear_enabled: false
|
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:
|
touchscreen:
|
||||||
- platform: axs15231
|
- platform: axs15231
|
||||||
@@ -142,27 +88,8 @@ touchscreen:
|
|||||||
# interrupt_pin:
|
# interrupt_pin:
|
||||||
# number: 3
|
# number: 3
|
||||||
# ignore_strapping_warning: true
|
# ignore_strapping_warning: true
|
||||||
calibration:
|
# no calibration/transform: the driver defaults to the native 320x480 panel and
|
||||||
x_min: 0
|
# LVGL applies its own rotation to touch points
|
||||||
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
|
|
||||||
# );
|
|
||||||
on_release:
|
on_release:
|
||||||
- if:
|
- if:
|
||||||
condition: lvgl.is_paused
|
condition: lvgl.is_paused
|
||||||
@@ -183,107 +110,596 @@ uart:
|
|||||||
|
|
||||||
binary_sensor:
|
binary_sensor:
|
||||||
- platform: homeassistant
|
- platform: homeassistant
|
||||||
id: exercise_room_light
|
id: basement_light
|
||||||
entity_id: light.exercise_room
|
entity_id: ${light_basement}
|
||||||
trigger_on_initial_state: true
|
trigger_on_initial_state: true
|
||||||
on_state:
|
on_state:
|
||||||
then:
|
- lvgl.widget.update:
|
||||||
lvgl.widget.update:
|
id: basement_light_btn
|
||||||
id: exercise_room_light_btn
|
|
||||||
state:
|
state:
|
||||||
checked: !lambda return x;
|
checked: !lambda return x;
|
||||||
- platform: homeassistant
|
- platform: homeassistant
|
||||||
id: exercise_room_fan
|
id: workbench_light
|
||||||
entity_id: light.mcu_dimmer_exercise_room_fan
|
entity_id: ${light_workbench}
|
||||||
trigger_on_initial_state: true
|
trigger_on_initial_state: true
|
||||||
on_state:
|
on_state:
|
||||||
then:
|
- lvgl.widget.update:
|
||||||
lvgl.widget.update:
|
id: workbench_light_btn
|
||||||
id: exercise_room_fan_btn
|
|
||||||
state:
|
state:
|
||||||
checked: !lambda return x;
|
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:
|
lvgl:
|
||||||
rotation: 90
|
rotation: 90
|
||||||
buffer_size: 100%
|
buffer_size: 100%
|
||||||
log_level: WARN
|
log_level: WARN
|
||||||
color_depth: 16
|
color_depth: 16
|
||||||
default_font: montserrat_18
|
default_font: font_ui
|
||||||
# on_idle:
|
bg_color: 0x000000
|
||||||
# - timeout: !lambda "return (id(display_timeout).state * 1000);"
|
text_color: 0xFFFFFF
|
||||||
# then:
|
theme:
|
||||||
# - logger.log: "LVGL is idle"
|
button:
|
||||||
# - light.turn_off:
|
bg_color: 0x2B2F36
|
||||||
# id: display_backlight
|
radius: 12
|
||||||
# transition_length: 2500ms
|
checked:
|
||||||
# - lvgl.pause:
|
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:
|
top_layer:
|
||||||
widgets:
|
widgets:
|
||||||
- buttonmatrix:
|
- buttonmatrix:
|
||||||
|
id: nav_bar
|
||||||
align: bottom_mid
|
align: bottom_mid
|
||||||
# styles: header_footer
|
width: 100%
|
||||||
pad_all: 0
|
height: 56
|
||||||
outline_width: 0
|
pad_all: 4
|
||||||
id: top_layer
|
pad_column: 6
|
||||||
# items:
|
items:
|
||||||
# styles: header_footer
|
text_font: montserrat_28
|
||||||
rows:
|
rows:
|
||||||
- buttons:
|
- buttons:
|
||||||
- id: page_prev
|
- id: page_prev
|
||||||
text: "\uF053"
|
text: ""
|
||||||
on_press:
|
on_press:
|
||||||
then:
|
then:
|
||||||
lvgl.page.previous:
|
lvgl.page.previous:
|
||||||
- id: page_home
|
- id: page_home
|
||||||
text: "\uF015"
|
text: ""
|
||||||
on_press:
|
on_press:
|
||||||
then:
|
then:
|
||||||
lvgl.page.show: main_page
|
lvgl.page.show: clock_page
|
||||||
- id: page_next
|
- id: page_next
|
||||||
text: "\uF054"
|
text: ""
|
||||||
on_press:
|
on_press:
|
||||||
then:
|
then:
|
||||||
lvgl.page.next:
|
lvgl.page.next:
|
||||||
pages:
|
pages:
|
||||||
- id: main_page
|
# 1: clock (default after boot)
|
||||||
|
- id: clock_page
|
||||||
widgets:
|
widgets:
|
||||||
- label:
|
- label:
|
||||||
text: "ESPHome LVGL Display"
|
id: clock_label
|
||||||
align: CENTER
|
align: CENTER
|
||||||
text_align: CENTER
|
y: -28
|
||||||
text_color: 0xFFFFFF
|
text_font: font_digits
|
||||||
- id: excercise_page
|
text: "--:--:--"
|
||||||
|
|
||||||
|
# 2: basement lights
|
||||||
|
- id: lights_page
|
||||||
|
pad_all: 10
|
||||||
widgets:
|
widgets:
|
||||||
- label:
|
|
||||||
align: CENTER
|
|
||||||
text: 'Hello World!'
|
|
||||||
- button:
|
- button:
|
||||||
id: exercise_room_light_btn
|
id: basement_light_btn
|
||||||
align: TOP_LEFT
|
align: TOP_LEFT
|
||||||
width: 100
|
width: 148
|
||||||
height: 70
|
height: 80
|
||||||
checkable: true
|
checkable: true
|
||||||
widgets:
|
widgets:
|
||||||
- label:
|
- label:
|
||||||
align: CENTER
|
align: CENTER
|
||||||
text: 'Light'
|
text: "Pivnica"
|
||||||
on_click:
|
on_click:
|
||||||
- homeassistant.action:
|
- homeassistant.action:
|
||||||
action: light.toggle
|
action: light.toggle
|
||||||
data:
|
data:
|
||||||
entity_id: light.exercise_room
|
entity_id: ${light_basement}
|
||||||
- button:
|
- button:
|
||||||
id: exercise_room_fan_btn
|
id: workbench_light_btn
|
||||||
align: TOP_RIGHT
|
align: TOP_MID
|
||||||
width: 100
|
width: 148
|
||||||
height: 70
|
height: 80
|
||||||
checkable: true
|
checkable: true
|
||||||
widgets:
|
widgets:
|
||||||
- label:
|
- label:
|
||||||
align: CENTER
|
align: CENTER
|
||||||
text: 'Fan'
|
text: "Pracovný stôl"
|
||||||
on_click:
|
on_click:
|
||||||
- homeassistant.action:
|
- homeassistant.action:
|
||||||
action: light.toggle
|
action: light.toggle
|
||||||
data:
|
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: ""
|
||||||
@@ -158,10 +158,6 @@ text_sensor:
|
|||||||
icon: mdi:clock-start
|
icon: mdi:clock-start
|
||||||
internal: True
|
internal: True
|
||||||
|
|
||||||
time:
|
|
||||||
- platform: homeassistant
|
|
||||||
id: time_comp
|
|
||||||
|
|
||||||
number:
|
number:
|
||||||
- platform: template
|
- platform: template
|
||||||
initial_value: 0
|
initial_value: 0
|
||||||
|
|||||||
+564
-77
@@ -4,94 +4,370 @@ substitutions:
|
|||||||
name: M5Paper
|
name: M5Paper
|
||||||
area: DEV
|
area: DEV
|
||||||
comment: M5Paper
|
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:
|
esphome:
|
||||||
name: mcu-${device}
|
name: mcu-${device}
|
||||||
friendly_name: ${name}
|
friendly_name: ${name}
|
||||||
area: ${area}
|
area: ${area}
|
||||||
comment: ${comment}
|
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:
|
esp32:
|
||||||
variant: esp32
|
variant: esp32
|
||||||
toolchain: esp-idf
|
toolchain: esp-idf
|
||||||
framework:
|
framework:
|
||||||
type: esp-idf
|
type: esp-idf
|
||||||
|
advanced:
|
||||||
|
flash_chip: generic
|
||||||
|
minimum_chip_revision: 3.0
|
||||||
|
sram1_as_iram: true
|
||||||
|
|
||||||
psram:
|
psram:
|
||||||
|
|
||||||
external_components:
|
|
||||||
- source: github://Passific/m5paper_esphome
|
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
common: !include common/common.yaml
|
common: !include common/common.yaml
|
||||||
|
|
||||||
time:
|
globals:
|
||||||
- platform: homeassistant
|
- id: show_status
|
||||||
id: ha_time
|
type: bool
|
||||||
timezone: Europe/Bratislava
|
initial_value: 'true'
|
||||||
on_time_sync:
|
- id: usb_mode
|
||||||
- bm8563.write_time
|
type: bool
|
||||||
- platform: bm8563
|
initial_value: 'false'
|
||||||
id: rtc_time
|
- id: rtc_wake
|
||||||
sleep_duration: 3600000ms
|
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:
|
wifi:
|
||||||
battery_power_pin: GPIO5
|
enable_on_boot: false
|
||||||
main_power_pin: GPIO2
|
on_connect:
|
||||||
# update_interval: 10s
|
- if:
|
||||||
# battery_voltage:
|
condition:
|
||||||
# name: "${name} Battery"
|
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
|
# 0x44 SHT30
|
||||||
# 0x50
|
# 0x50 FM24C02 EEPROM (forecast storage, see include/m5paper_weather.h)
|
||||||
# 0x51
|
# 0x51 BM8563 RTC
|
||||||
# 0x5D GT911
|
# 0x5D GT911 touch (docs say 0x14; the driver tries 0x5D, then 0x14)
|
||||||
|
|
||||||
i2c:
|
i2c:
|
||||||
- id: i2c_main
|
- id: i2c_main
|
||||||
sda: 21
|
sda: 21
|
||||||
scl: 22
|
scl: 22
|
||||||
scan: true #false
|
scan: true
|
||||||
|
# Port A
|
||||||
# - id: i2c_a
|
# - id: i2c_a
|
||||||
# sda: 25
|
# sda: 25
|
||||||
# scl: 32
|
# scl: 32
|
||||||
# scan: true #false
|
# Port B
|
||||||
# - id: i2c_b
|
# - id: i2c_b
|
||||||
# sda: 26
|
# sda: 26
|
||||||
# scl: 33
|
# scl: 33
|
||||||
# scan: true #false
|
# Port C
|
||||||
# - id: i2c_c
|
# - id: i2c_c
|
||||||
# sda: 18
|
# sda: 18
|
||||||
# scl: 19
|
# scl: 19
|
||||||
# scan: true #false
|
|
||||||
|
|
||||||
spi:
|
spi:
|
||||||
clk_pin: 14
|
clk_pin: 14
|
||||||
miso_pin: 13
|
miso_pin: 13
|
||||||
mosi_pin: 12
|
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:
|
binary_sensor:
|
||||||
- platform: gpio
|
- platform: gpio
|
||||||
name: ${name} Button UP
|
name: ${name} Button UP
|
||||||
id: button_up
|
id: button_up
|
||||||
pin:
|
pin:
|
||||||
number: 37
|
number: GPIO37
|
||||||
inverted: true
|
inverted: true
|
||||||
on_press:
|
on_press:
|
||||||
- component.update: m5paper_display
|
- component.update: m5paper_display
|
||||||
on_release:
|
|
||||||
- component.update: m5paper_display
|
|
||||||
- platform: gpio
|
- platform: gpio
|
||||||
name: ${name} Button Press
|
name: ${name} Button Press
|
||||||
id: button_press
|
id: button_press
|
||||||
pin:
|
pin:
|
||||||
number: 38
|
number: GPIO38
|
||||||
inverted: true
|
inverted: true
|
||||||
- platform: gpio
|
- platform: gpio
|
||||||
name: ${name} Button Down
|
name: ${name} Button Down
|
||||||
id: button_down
|
id: button_down
|
||||||
pin:
|
pin:
|
||||||
number: 39
|
number: GPIO39
|
||||||
inverted: true
|
inverted: true
|
||||||
- platform: touchscreen
|
- platform: touchscreen
|
||||||
name: ${name} Top Left Touch Button
|
name: ${name} Top Left Touch Button
|
||||||
@@ -103,51 +379,19 @@ binary_sensor:
|
|||||||
y_min: 0
|
y_min: 0
|
||||||
y_max: 100
|
y_max: 100
|
||||||
on_press:
|
on_press:
|
||||||
# Example of applying the sleep duration and shutting down to the lowest power mode
|
- script.execute: battery_cycle
|
||||||
- bm8563.apply_sleep_duration
|
|
||||||
- delay: 1s
|
|
||||||
- m5paper.shutdown_main_power
|
|
||||||
|
|
||||||
font:
|
button:
|
||||||
- file: "gfonts://Roboto"
|
- platform: template
|
||||||
id: font_default
|
name: Refresh Display
|
||||||
size: 20
|
entity_category: config
|
||||||
- file: "gfonts://Roboto"
|
on_press:
|
||||||
id: roboto_32
|
- component.update: m5paper_display
|
||||||
size: 32
|
- platform: template
|
||||||
- file: "gfonts://Roboto"
|
name: Refresh Forecast
|
||||||
id: roboto_24
|
entity_category: config
|
||||||
size: 24
|
on_press:
|
||||||
- file: "gfonts://Roboto"
|
- script.execute: fetch_forecast
|
||||||
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!");
|
|
||||||
|
|
||||||
sensor:
|
sensor:
|
||||||
- platform: sht3xd
|
- platform: sht3xd
|
||||||
@@ -156,9 +400,252 @@ sensor:
|
|||||||
id: current_temperature
|
id: current_temperature
|
||||||
humidity:
|
humidity:
|
||||||
name: Humidity
|
name: Humidity
|
||||||
|
id: current_humidity
|
||||||
address: 0x44
|
address: 0x44
|
||||||
update_interval: 60s
|
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:
|
.image_defaults:
|
||||||
# - platform: gt911
|
icon_large: &icon_large {platform: file, type: BINARY, transparency: chroma_key, resize: 150x150}
|
||||||
# interrupt_pin: 36
|
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
|
||||||
@@ -228,9 +228,7 @@ button:
|
|||||||
- pzemac.reset_energy: pzemac_l3
|
- pzemac.reset_energy: pzemac_l3
|
||||||
|
|
||||||
time:
|
time:
|
||||||
- platform: homeassistant
|
- id: !extend ha_time
|
||||||
id: ha_time
|
|
||||||
timezone: Europe/Bratislava
|
|
||||||
on_time:
|
on_time:
|
||||||
- cron: '0 0 0 * * *'
|
- cron: '0 0 0 * * *'
|
||||||
then:
|
then:
|
||||||
|
|||||||
@@ -136,9 +136,7 @@ nspanel_lovelace:
|
|||||||
thermo_entity_id: climate.bt_trv_bedroom
|
thermo_entity_id: climate.bt_trv_bedroom
|
||||||
|
|
||||||
time:
|
time:
|
||||||
- platform: homeassistant
|
- id: !extend ha_time
|
||||||
id: ha_time
|
|
||||||
timezone: Europe/Bratislava
|
|
||||||
on_time_sync:
|
on_time_sync:
|
||||||
- script.execute: apply_sleep_dim
|
- script.execute: apply_sleep_dim
|
||||||
|
|
||||||
|
|||||||
@@ -540,11 +540,6 @@ sensor:
|
|||||||
- platform: template
|
- platform: template
|
||||||
id: room_temperature_id
|
id: room_temperature_id
|
||||||
|
|
||||||
time:
|
|
||||||
platform: homeassistant
|
|
||||||
id: ha_time
|
|
||||||
timezone: Europe/Bratislava
|
|
||||||
|
|
||||||
text_sensor:
|
text_sensor:
|
||||||
- platform: homeassistant
|
- platform: homeassistant
|
||||||
id: name_day
|
id: name_day
|
||||||
|
|||||||
@@ -185,11 +185,6 @@ nspanel_lovelace:
|
|||||||
- name: Auto-zatváranie
|
- name: Auto-zatváranie
|
||||||
entity_id: switch.gate_auto_close
|
entity_id: switch.gate_auto_close
|
||||||
|
|
||||||
time:
|
|
||||||
- platform: homeassistant
|
|
||||||
id: ha_time
|
|
||||||
timezone: Europe/Bratislava
|
|
||||||
|
|
||||||
binary_sensor:
|
binary_sensor:
|
||||||
- platform: gpio
|
- platform: gpio
|
||||||
name: Left Button
|
name: Left Button
|
||||||
|
|||||||
@@ -38,6 +38,16 @@ external_components:
|
|||||||
packages:
|
packages:
|
||||||
common: !include common/common.yaml
|
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:
|
api:
|
||||||
homeassistant_services: true
|
homeassistant_services: true
|
||||||
homeassistant_states: true
|
homeassistant_states: true
|
||||||
@@ -135,9 +145,9 @@ nspanel_lovelace:
|
|||||||
entity_id: switch.gate_auto_close
|
entity_id: switch.gate_auto_close
|
||||||
|
|
||||||
time:
|
time:
|
||||||
- platform: homeassistant
|
- id: !extend ha_time
|
||||||
id: ha_time
|
on_time_sync:
|
||||||
timezone: Europe/Bratislava
|
- script.execute: apply_sleep_dim
|
||||||
|
|
||||||
binary_sensor:
|
binary_sensor:
|
||||||
- platform: gpio
|
- platform: gpio
|
||||||
@@ -161,6 +171,12 @@ binary_sensor:
|
|||||||
entity_id: light.bedside_left
|
entity_id: light.bedside_left
|
||||||
- lambda: 'id(nspanel_id).render_default_page();'
|
- 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:
|
output:
|
||||||
- platform: ledc
|
- platform: ledc
|
||||||
id: buzzer_out
|
id: buzzer_out
|
||||||
@@ -186,6 +202,20 @@ light:
|
|||||||
# # name: Relay 2
|
# # name: Relay 2
|
||||||
# # output: 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:
|
switch:
|
||||||
- platform: gpio
|
- platform: gpio
|
||||||
name: Screen Power
|
name: Screen Power
|
||||||
|
|||||||
@@ -158,11 +158,6 @@ nspanel_lovelace:
|
|||||||
- name: Auto-zatváranie
|
- name: Auto-zatváranie
|
||||||
entity_id: switch.gate_auto_close
|
entity_id: switch.gate_auto_close
|
||||||
|
|
||||||
time:
|
|
||||||
- platform: homeassistant
|
|
||||||
id: ha_time
|
|
||||||
timezone: Europe/Bratislava
|
|
||||||
|
|
||||||
binary_sensor:
|
binary_sensor:
|
||||||
- platform: gpio
|
- platform: gpio
|
||||||
name: Left Button
|
name: Left Button
|
||||||
|
|||||||
@@ -214,11 +214,6 @@ nspanel_lovelace:
|
|||||||
- name: Rast trávy
|
- name: Rast trávy
|
||||||
entity_id: automation.irrigation_grow_grass
|
entity_id: automation.irrigation_grow_grass
|
||||||
|
|
||||||
time:
|
|
||||||
- platform: homeassistant
|
|
||||||
id: ha_time
|
|
||||||
timezone: Europe/Bratislava
|
|
||||||
|
|
||||||
binary_sensor:
|
binary_sensor:
|
||||||
- platform: gpio
|
- platform: gpio
|
||||||
name: Left Button
|
name: Left Button
|
||||||
|
|||||||
@@ -54,11 +54,6 @@ packages:
|
|||||||
# Relay1 G22
|
# Relay1 G22
|
||||||
# Relay2 G19
|
# Relay2 G19
|
||||||
|
|
||||||
time:
|
|
||||||
- platform: homeassistant
|
|
||||||
id: ha_time
|
|
||||||
timezone: Europe/Bratislava
|
|
||||||
|
|
||||||
i2c:
|
i2c:
|
||||||
# - id: i2c0
|
# - id: i2c0
|
||||||
# sda: 26
|
# sda: 26
|
||||||
|
|||||||
@@ -187,5 +187,3 @@ switch:
|
|||||||
- switch.turn_off: relay_off # bi-stable relay so no need to keep on
|
- switch.turn_off: relay_off # bi-stable relay so no need to keep on
|
||||||
interlock: [relay_on]
|
interlock: [relay_on]
|
||||||
|
|
||||||
time:
|
|
||||||
- !include common/time/homeassistant.yaml
|
|
||||||
+1
-4
@@ -2,7 +2,7 @@
|
|||||||
substitutions:
|
substitutions:
|
||||||
device: pow-washer
|
device: pow-washer
|
||||||
name: Washer POW
|
name: Washer POW
|
||||||
comment:
|
comment: "spare"
|
||||||
area: Washroom
|
area: Washroom
|
||||||
update_interval: 10s
|
update_interval: 10s
|
||||||
|
|
||||||
@@ -186,6 +186,3 @@ switch:
|
|||||||
- delay: 500ms
|
- delay: 500ms
|
||||||
- switch.turn_off: relay_off # bi-stable relay so no need to keep on
|
- switch.turn_off: relay_off # bi-stable relay so no need to keep on
|
||||||
interlock: [relay_on]
|
interlock: [relay_on]
|
||||||
|
|
||||||
time:
|
|
||||||
- !include common/time/homeassistant.yaml
|
|
||||||
@@ -109,5 +109,3 @@ switch:
|
|||||||
number: 19
|
number: 19
|
||||||
mode: OUTPUT
|
mode: OUTPUT
|
||||||
|
|
||||||
time:
|
|
||||||
- !include common/time/homeassistant.yaml
|
|
||||||
+1
-2
@@ -121,8 +121,7 @@ switch:
|
|||||||
# number: GPIO00
|
# number: GPIO00
|
||||||
# inverted: True
|
# inverted: True
|
||||||
|
|
||||||
time:
|
# time:
|
||||||
- !include common/time/homeassistant.yaml
|
|
||||||
# - platform: homeassistant
|
# - platform: homeassistant
|
||||||
# timezone: Europe/Bratislava
|
# timezone: Europe/Bratislava
|
||||||
# on_time:
|
# on_time:
|
||||||
|
|||||||
+1
-2
@@ -122,8 +122,7 @@ switch:
|
|||||||
on_turn_off:
|
on_turn_off:
|
||||||
- light.turn_off: light_red
|
- light.turn_off: light_red
|
||||||
|
|
||||||
time:
|
# time:
|
||||||
- !include common/time/homeassistant.yaml
|
|
||||||
# - platform: homeassistant
|
# - platform: homeassistant
|
||||||
# timezone: Europe/Bratislava
|
# timezone: Europe/Bratislava
|
||||||
# on_time:
|
# on_time:
|
||||||
|
|||||||
+1
-2
@@ -144,8 +144,7 @@ switch:
|
|||||||
# number: GPIO00
|
# number: GPIO00
|
||||||
# inverted: True
|
# inverted: True
|
||||||
|
|
||||||
time:
|
# time:
|
||||||
- !include common/time/homeassistant.yaml
|
|
||||||
# - platform: homeassistant
|
# - platform: homeassistant
|
||||||
# timezone: Europe/Bratislava
|
# timezone: Europe/Bratislava
|
||||||
# on_time:
|
# on_time:
|
||||||
|
|||||||
@@ -0,0 +1,899 @@
|
|||||||
|
substitutions:
|
||||||
|
device: stackchan
|
||||||
|
name: StackChan
|
||||||
|
area: DEV
|
||||||
|
comment: StackChan
|
||||||
|
|
||||||
|
esphome:
|
||||||
|
name: mcu-${device}
|
||||||
|
friendly_name: ${name}
|
||||||
|
area: ${area}
|
||||||
|
comment: ${comment}
|
||||||
|
on_boot:
|
||||||
|
- priority: -100
|
||||||
|
then:
|
||||||
|
- light.turn_on:
|
||||||
|
id: light_bar
|
||||||
|
brightness: 20%
|
||||||
|
red: 0%
|
||||||
|
green: 0%
|
||||||
|
blue: 100%
|
||||||
|
- component.update: display_tft
|
||||||
|
|
||||||
|
esp32:
|
||||||
|
variant: esp32s3
|
||||||
|
flash_size: 16MB
|
||||||
|
toolchain: esp-idf
|
||||||
|
framework:
|
||||||
|
type: esp-idf
|
||||||
|
|
||||||
|
psram:
|
||||||
|
mode: quad
|
||||||
|
speed: 80MHz
|
||||||
|
|
||||||
|
packages:
|
||||||
|
common: !include common/common.yaml
|
||||||
|
|
||||||
|
external_components:
|
||||||
|
- source: github://m5stack/esphome-yaml/components
|
||||||
|
components: [axp2101, aw88298, aw9523b, si12t, m5ioe1, ftservo, scs9009]
|
||||||
|
refresh: 0s
|
||||||
|
- source: github://randybb/esphome-components
|
||||||
|
components: [audio_spectrum]
|
||||||
|
refresh: 1d
|
||||||
|
|
||||||
|
globals:
|
||||||
|
- id: in_love
|
||||||
|
type: bool
|
||||||
|
initial_value: 'false'
|
||||||
|
- id: show_status
|
||||||
|
type: bool
|
||||||
|
initial_value: 'true'
|
||||||
|
- id: mouth_open
|
||||||
|
type: float
|
||||||
|
initial_value: '0.0'
|
||||||
|
- id: eyes_open
|
||||||
|
type: bool
|
||||||
|
initial_value: 'true'
|
||||||
|
- id: speaking
|
||||||
|
type: bool
|
||||||
|
initial_value: 'false'
|
||||||
|
- id: listening
|
||||||
|
type: bool
|
||||||
|
initial_value: 'false'
|
||||||
|
- id: gaze_x
|
||||||
|
type: int
|
||||||
|
initial_value: '0'
|
||||||
|
- id: gaze_y
|
||||||
|
type: int
|
||||||
|
initial_value: '0'
|
||||||
|
|
||||||
|
i2c:
|
||||||
|
- id: i2c_bsp
|
||||||
|
sda: 12
|
||||||
|
scl: 11
|
||||||
|
frequency: 100kHz
|
||||||
|
|
||||||
|
axp2101:
|
||||||
|
id: axp2101_pmu
|
||||||
|
i2c_id: i2c_bsp
|
||||||
|
|
||||||
|
aw9523b:
|
||||||
|
id: aw9523b_hub
|
||||||
|
i2c_id: i2c_bsp
|
||||||
|
p0_drive_mode: PUSH_PULL
|
||||||
|
|
||||||
|
m5ioe1:
|
||||||
|
id: m5ioe1_hub
|
||||||
|
i2c_id: i2c_bsp
|
||||||
|
reset: true
|
||||||
|
|
||||||
|
output:
|
||||||
|
- platform: axp2101
|
||||||
|
id: lcd_backlight_output
|
||||||
|
type: range
|
||||||
|
channel: DLDO1
|
||||||
|
min_voltage: 2600
|
||||||
|
max_voltage: 3300
|
||||||
|
- platform: axp2101
|
||||||
|
channel: ALDO1
|
||||||
|
voltage: 1800
|
||||||
|
- platform: axp2101
|
||||||
|
channel: ALDO2
|
||||||
|
voltage: 3300
|
||||||
|
- platform: axp2101
|
||||||
|
channel: ALDO3
|
||||||
|
voltage: 3300
|
||||||
|
- platform: axp2101
|
||||||
|
channel: ALDO4
|
||||||
|
voltage: 3300
|
||||||
|
- platform: axp2101
|
||||||
|
channel: BLDO1
|
||||||
|
voltage: 2800
|
||||||
|
- platform: axp2101
|
||||||
|
channel: BLDO2
|
||||||
|
voltage: 1500
|
||||||
|
- platform: axp2101
|
||||||
|
channel: DCDC1
|
||||||
|
voltage: 3300
|
||||||
|
- platform: axp2101
|
||||||
|
channel: DCDC4
|
||||||
|
voltage: 1800
|
||||||
|
- platform: axp2101
|
||||||
|
channel: DCDC3
|
||||||
|
voltage: 3300
|
||||||
|
- platform: axp2101
|
||||||
|
channel: DCDC5
|
||||||
|
voltage: 3300
|
||||||
|
|
||||||
|
switch:
|
||||||
|
- platform: gpio
|
||||||
|
id: boost_en
|
||||||
|
name: Boost
|
||||||
|
entity_category: config
|
||||||
|
pin:
|
||||||
|
aw9523b_id: aw9523b_hub
|
||||||
|
number: 15
|
||||||
|
restore_mode: RESTORE_DEFAULT_ON
|
||||||
|
- platform: gpio
|
||||||
|
id: usb_otg_en
|
||||||
|
name: USB OTG
|
||||||
|
entity_category: config
|
||||||
|
pin:
|
||||||
|
aw9523b_id: aw9523b_hub
|
||||||
|
number: 5
|
||||||
|
restore_mode: RESTORE_DEFAULT_OFF
|
||||||
|
- platform: gpio
|
||||||
|
id: bus_out_en
|
||||||
|
name: BUS Out
|
||||||
|
entity_category: config
|
||||||
|
pin:
|
||||||
|
aw9523b_id: aw9523b_hub
|
||||||
|
number: 1
|
||||||
|
restore_mode: RESTORE_DEFAULT_ON
|
||||||
|
- platform: gpio
|
||||||
|
id: light_bar_en
|
||||||
|
internal: true
|
||||||
|
pin:
|
||||||
|
m5ioe1_id: m5ioe1_hub
|
||||||
|
number: 13
|
||||||
|
mode:
|
||||||
|
output: true
|
||||||
|
pullup: true
|
||||||
|
restore_mode: ALWAYS_ON
|
||||||
|
- platform: gpio
|
||||||
|
id: aw_rst
|
||||||
|
internal: true
|
||||||
|
pin:
|
||||||
|
aw9523b_id: aw9523b_hub
|
||||||
|
number: 2
|
||||||
|
restore_mode: ALWAYS_ON
|
||||||
|
- platform: gpio
|
||||||
|
id: cam_rst
|
||||||
|
internal: true
|
||||||
|
pin:
|
||||||
|
aw9523b_id: aw9523b_hub
|
||||||
|
number: 8
|
||||||
|
restore_mode: ALWAYS_ON
|
||||||
|
- platform: gpio
|
||||||
|
id: servo_power
|
||||||
|
internal: true
|
||||||
|
pin:
|
||||||
|
m5ioe1_id: m5ioe1_hub
|
||||||
|
number: 0
|
||||||
|
mode:
|
||||||
|
output: true
|
||||||
|
pullup: true
|
||||||
|
restore_mode: ALWAYS_ON
|
||||||
|
|
||||||
|
i2s_audio:
|
||||||
|
- id: i2s_bus
|
||||||
|
i2s_lrclk_pin: 33
|
||||||
|
i2s_bclk_pin: 34
|
||||||
|
i2s_mclk_pin:
|
||||||
|
number: 0
|
||||||
|
ignore_strapping_warning: true
|
||||||
|
|
||||||
|
audio_adc:
|
||||||
|
- platform: es7210
|
||||||
|
id: es7210_adc
|
||||||
|
i2c_id: i2c_bsp
|
||||||
|
bits_per_sample: 16bit
|
||||||
|
sample_rate: 16000
|
||||||
|
mic_gain: 36
|
||||||
|
|
||||||
|
audio_dac:
|
||||||
|
- platform: aw88298
|
||||||
|
id: aw88298_dac
|
||||||
|
i2c_id: i2c_bsp
|
||||||
|
sample_rate: 48000
|
||||||
|
|
||||||
|
microphone:
|
||||||
|
- platform: i2s_audio
|
||||||
|
id: stackchan_microphone
|
||||||
|
i2s_audio_id: i2s_bus
|
||||||
|
i2s_din_pin: 14
|
||||||
|
adc_type: external
|
||||||
|
sample_rate: 16000
|
||||||
|
bits_per_sample: 16bit
|
||||||
|
|
||||||
|
speaker:
|
||||||
|
- platform: i2s_audio
|
||||||
|
id: speaker_id
|
||||||
|
i2s_audio_id: i2s_bus
|
||||||
|
dac_type: external
|
||||||
|
i2s_dout_pin: 13
|
||||||
|
audio_dac: aw88298_dac
|
||||||
|
sample_rate: 48000
|
||||||
|
buffer_duration: 100ms
|
||||||
|
# pass-through that computes the spectrum shown on the display while music plays
|
||||||
|
- platform: audio_spectrum
|
||||||
|
id: spectrum
|
||||||
|
output_speaker: speaker_id
|
||||||
|
bands: 20
|
||||||
|
- platform: mixer
|
||||||
|
id: mixer_speaker_id
|
||||||
|
output_speaker: spectrum
|
||||||
|
num_channels: 1
|
||||||
|
source_speakers:
|
||||||
|
- id: announcement_spk_mixer_input
|
||||||
|
- id: media_spk_mixer_input
|
||||||
|
- platform: resampler
|
||||||
|
id: media_spk_resampling_input
|
||||||
|
output_speaker: media_spk_mixer_input
|
||||||
|
- platform: resampler
|
||||||
|
id: announcement_spk_resampling_input
|
||||||
|
output_speaker: announcement_spk_mixer_input
|
||||||
|
|
||||||
|
media_player:
|
||||||
|
- platform: speaker
|
||||||
|
id: media_out
|
||||||
|
name: Player
|
||||||
|
buffer_size: 14000
|
||||||
|
# AW88298 maps volume linearly to -96..0 dB, the lower half is inaudible
|
||||||
|
volume_min: 0.5
|
||||||
|
volume_max: 1.0
|
||||||
|
media_pipeline:
|
||||||
|
speaker: media_spk_resampling_input
|
||||||
|
format: NONE
|
||||||
|
announcement_pipeline:
|
||||||
|
speaker: announcement_spk_resampling_input
|
||||||
|
format: NONE
|
||||||
|
on_announcement:
|
||||||
|
- script.execute: release_i2s_bus
|
||||||
|
on_play:
|
||||||
|
- script.execute: release_i2s_bus
|
||||||
|
on_idle:
|
||||||
|
- if:
|
||||||
|
condition:
|
||||||
|
not:
|
||||||
|
voice_assistant.is_running:
|
||||||
|
then:
|
||||||
|
- micro_wake_word.start:
|
||||||
|
|
||||||
|
micro_wake_word:
|
||||||
|
id: mww
|
||||||
|
microphone: stackchan_microphone
|
||||||
|
models:
|
||||||
|
- model: okay_nabu
|
||||||
|
on_wake_word_detected:
|
||||||
|
- voice_assistant.start:
|
||||||
|
wake_word: !lambda return wake_word;
|
||||||
|
|
||||||
|
voice_assistant:
|
||||||
|
id: va
|
||||||
|
microphone: stackchan_microphone
|
||||||
|
media_player: media_out
|
||||||
|
micro_wake_word: mww
|
||||||
|
noise_suppression_level: 2
|
||||||
|
auto_gain: 31dBFS
|
||||||
|
on_client_connected:
|
||||||
|
- micro_wake_word.start:
|
||||||
|
- delay: 2s
|
||||||
|
- lambda: id(show_status) = false;
|
||||||
|
- component.update: display_tft
|
||||||
|
on_client_disconnected:
|
||||||
|
- micro_wake_word.stop:
|
||||||
|
- lambda: id(show_status) = true;
|
||||||
|
- component.update: display_tft
|
||||||
|
on_listening:
|
||||||
|
- lambda: |-
|
||||||
|
id(listening) = true;
|
||||||
|
id(gaze_x) = 0;
|
||||||
|
id(gaze_y) = 0;
|
||||||
|
- component.update: display_tft
|
||||||
|
- light.turn_on: {id: light_bar, brightness: 60%, red: 0%, green: 60%, blue: 100%}
|
||||||
|
on_stt_end:
|
||||||
|
- lambda: id(listening) = false;
|
||||||
|
- component.update: display_tft
|
||||||
|
- light.turn_on: {id: light_bar, brightness: 40%, red: 80%, green: 50%, blue: 0%}
|
||||||
|
on_tts_start:
|
||||||
|
- lambda: id(speaking) = true;
|
||||||
|
- light.turn_on: {id: light_bar, brightness: 60%, red: 0%, green: 100%, blue: 20%}
|
||||||
|
on_end:
|
||||||
|
- wait_until:
|
||||||
|
not:
|
||||||
|
media_player.is_announcing: media_out
|
||||||
|
- lambda: |-
|
||||||
|
id(speaking) = false;
|
||||||
|
id(listening) = false;
|
||||||
|
id(mouth_open) = 0.0f;
|
||||||
|
- component.update: display_tft
|
||||||
|
- light.turn_on: {id: light_bar, brightness: 20%, red: 0%, green: 0%, blue: 100%}
|
||||||
|
- wait_until:
|
||||||
|
not: voice_assistant.is_running
|
||||||
|
- micro_wake_word.start:
|
||||||
|
on_error:
|
||||||
|
- light.turn_on: {id: light_bar, brightness: 60%, red: 100%, green: 0%, blue: 0%}
|
||||||
|
|
||||||
|
script:
|
||||||
|
# short tap on the head starts the assistant
|
||||||
|
- id: head_tap
|
||||||
|
then:
|
||||||
|
- if:
|
||||||
|
condition:
|
||||||
|
not: voice_assistant.is_running
|
||||||
|
then:
|
||||||
|
- voice_assistant.start:
|
||||||
|
# stroking the head shows the in-love face for a while
|
||||||
|
- id: head_stroke
|
||||||
|
mode: restart
|
||||||
|
then:
|
||||||
|
- lambda: id(in_love) = true;
|
||||||
|
- component.update: display_tft
|
||||||
|
- delay: 5s
|
||||||
|
- lambda: id(in_love) = false;
|
||||||
|
- component.update: display_tft
|
||||||
|
# mic and speaker share one I2S bus - the mic must be released before playback
|
||||||
|
- id: release_i2s_bus
|
||||||
|
mode: single
|
||||||
|
then:
|
||||||
|
- if:
|
||||||
|
condition:
|
||||||
|
microphone.is_capturing:
|
||||||
|
then:
|
||||||
|
- micro_wake_word.stop:
|
||||||
|
# stop VA only while it is listening (e.g. continued conversation), not during its own response
|
||||||
|
- if:
|
||||||
|
condition:
|
||||||
|
lambda: return id(listening);
|
||||||
|
then:
|
||||||
|
- voice_assistant.stop:
|
||||||
|
- wait_until:
|
||||||
|
condition:
|
||||||
|
not:
|
||||||
|
microphone.is_capturing:
|
||||||
|
timeout: 2s
|
||||||
|
|
||||||
|
interval:
|
||||||
|
- interval: 80ms # spectrum analyzer frame rate while music plays
|
||||||
|
then:
|
||||||
|
- if:
|
||||||
|
condition:
|
||||||
|
and:
|
||||||
|
- media_player.is_playing: media_out
|
||||||
|
- not: voice_assistant.is_running
|
||||||
|
then:
|
||||||
|
- component.update: display_tft
|
||||||
|
# head touch gestures: a touch episode ends after 250 ms without contact;
|
||||||
|
# tap = short episode, stroke = longer episode that reaches the pads one after another
|
||||||
|
- interval: 50ms
|
||||||
|
then:
|
||||||
|
- lambda: |-
|
||||||
|
static bool active = false;
|
||||||
|
static uint32_t start = 0, last_on = 0;
|
||||||
|
static uint32_t first_on[3];
|
||||||
|
const bool on[3] = {id(head_touch_1).state != "No touch", id(head_touch_2).state != "No touch",
|
||||||
|
id(head_touch_3).state != "No touch"};
|
||||||
|
const uint32_t now = millis();
|
||||||
|
|
||||||
|
if (on[0] || on[1] || on[2]) {
|
||||||
|
if (!active) {
|
||||||
|
active = true;
|
||||||
|
start = now;
|
||||||
|
first_on[0] = first_on[1] = first_on[2] = 0;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < 3; i++)
|
||||||
|
if (on[i] && first_on[i] == 0)
|
||||||
|
first_on[i] = now;
|
||||||
|
last_on = now;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!active || now - last_on < 250)
|
||||||
|
return;
|
||||||
|
active = false;
|
||||||
|
|
||||||
|
const uint32_t duration = last_on - start;
|
||||||
|
int pads = 0;
|
||||||
|
uint32_t earliest = UINT32_MAX, latest = 0;
|
||||||
|
for (int i = 0; i < 3; i++) {
|
||||||
|
if (first_on[i] == 0)
|
||||||
|
continue;
|
||||||
|
pads++;
|
||||||
|
earliest = std::min(earliest, first_on[i]);
|
||||||
|
latest = std::max(latest, first_on[i]);
|
||||||
|
}
|
||||||
|
const uint32_t spread = latest - earliest;
|
||||||
|
ESP_LOGD("head_touch", "duration %u ms, pads %d, spread %u ms, order 1:%d 2:%d 3:%d", (unsigned) duration,
|
||||||
|
pads, (unsigned) spread, first_on[0] ? (int) (first_on[0] - earliest) : -1,
|
||||||
|
first_on[1] ? (int) (first_on[1] - earliest) : -1, first_on[2] ? (int) (first_on[2] - earliest) : -1);
|
||||||
|
|
||||||
|
if (pads >= 2 && spread >= 150 && duration >= 400) {
|
||||||
|
id(head_stroke).execute();
|
||||||
|
} else if (duration <= 600) {
|
||||||
|
id(head_tap).execute();
|
||||||
|
}
|
||||||
|
- interval: 1s # status screen refresh until Home Assistant connects
|
||||||
|
then:
|
||||||
|
- if:
|
||||||
|
condition:
|
||||||
|
lambda: return id(show_status);
|
||||||
|
then:
|
||||||
|
- component.update: display_tft
|
||||||
|
- interval: 90ms
|
||||||
|
then:
|
||||||
|
- lambda: |-
|
||||||
|
if (id(speaking)) {
|
||||||
|
id(mouth_open) = 0.15f + (esp_random() % 85) / 100.0f;
|
||||||
|
id(display_tft).update();
|
||||||
|
} else if (id(mouth_open) > 0) {
|
||||||
|
id(mouth_open) = 0;
|
||||||
|
id(display_tft).update();
|
||||||
|
}
|
||||||
|
- interval: 3s # blinking
|
||||||
|
then:
|
||||||
|
- delay: !lambda return esp_random() % 3000;
|
||||||
|
- lambda: id(eyes_open) = false;
|
||||||
|
- component.update: display_tft
|
||||||
|
- delay: 150ms
|
||||||
|
- lambda: id(eyes_open) = true;
|
||||||
|
- component.update: display_tft
|
||||||
|
- interval: 7s # looking around while idle
|
||||||
|
then:
|
||||||
|
- lambda: |-
|
||||||
|
if (!id(speaking) && !id(listening)) {
|
||||||
|
id(gaze_x) = (int) (esp_random() % 21) - 10;
|
||||||
|
id(gaze_y) = (int) (esp_random() % 11) - 5;
|
||||||
|
id(display_tft).update();
|
||||||
|
}
|
||||||
|
|
||||||
|
spi:
|
||||||
|
- id: spi_bus
|
||||||
|
clk_pin: 36
|
||||||
|
mosi_pin: 37
|
||||||
|
|
||||||
|
image:
|
||||||
|
- platform: file
|
||||||
|
file: assets/images/made-for-esphome-white-on-black.svg
|
||||||
|
id: esphome_logo
|
||||||
|
resize: 230x68
|
||||||
|
type: RGB565
|
||||||
|
transparency: alpha_channel
|
||||||
|
- platform: file
|
||||||
|
file: mdi:ear-hearing
|
||||||
|
id: icon_listening
|
||||||
|
resize: 28x28
|
||||||
|
type: BINARY
|
||||||
|
|
||||||
|
font:
|
||||||
|
- file: "gfonts://Roboto"
|
||||||
|
id: font_title
|
||||||
|
size: 24
|
||||||
|
- file: "gfonts://Roboto"
|
||||||
|
id: font_status
|
||||||
|
size: 16
|
||||||
|
|
||||||
|
color:
|
||||||
|
- id: status_ok
|
||||||
|
red: 30%
|
||||||
|
green: 85%
|
||||||
|
blue: 40%
|
||||||
|
- id: status_wait
|
||||||
|
red: 100%
|
||||||
|
green: 70%
|
||||||
|
blue: 0%
|
||||||
|
- id: love_red
|
||||||
|
red: 100%
|
||||||
|
green: 25%
|
||||||
|
blue: 45%
|
||||||
|
- id: status_low
|
||||||
|
red: 100%
|
||||||
|
green: 20%
|
||||||
|
blue: 20%
|
||||||
|
- id: status_dim
|
||||||
|
red: 55%
|
||||||
|
green: 55%
|
||||||
|
blue: 55%
|
||||||
|
- id: face_fg
|
||||||
|
red: 100%
|
||||||
|
green: 100%
|
||||||
|
blue: 100%
|
||||||
|
- id: face_bg
|
||||||
|
red: 0%
|
||||||
|
green: 0%
|
||||||
|
blue: 0%
|
||||||
|
|
||||||
|
display:
|
||||||
|
- platform: mipi_spi
|
||||||
|
id: display_tft
|
||||||
|
model: M5CORE
|
||||||
|
dc_pin: 35
|
||||||
|
reset_pin:
|
||||||
|
aw9523b_id: aw9523b_hub
|
||||||
|
number: 9
|
||||||
|
cs_pin:
|
||||||
|
number: 3
|
||||||
|
ignore_strapping_warning: true
|
||||||
|
data_rate: 40MHz
|
||||||
|
invert_colors: true
|
||||||
|
update_interval: never
|
||||||
|
auto_clear_enabled: false
|
||||||
|
lambda: |-
|
||||||
|
static bool first_draw = true;
|
||||||
|
static bool dot_shown = false;
|
||||||
|
static bool bar_drawn = false;
|
||||||
|
static int last_bars = -1, last_batt = -2;
|
||||||
|
static bool last_charging = false;
|
||||||
|
|
||||||
|
const bool wifi_ok = wifi::global_wifi_component->is_connected();
|
||||||
|
const int8_t rssi = wifi_ok ? wifi::global_wifi_component->wifi_rssi() : -127;
|
||||||
|
const int wifi_bars = !wifi_ok ? 0 : rssi > -55 ? 4 : rssi > -65 ? 3 : rssi > -75 ? 2 : 1;
|
||||||
|
const int batt = std::isnan(id(battery_level).state) ? -1 : (int) roundf(id(battery_level).state);
|
||||||
|
const bool charging = id(battery_charging).state;
|
||||||
|
|
||||||
|
// status screen until Home Assistant connects
|
||||||
|
if (id(show_status)) {
|
||||||
|
first_draw = true;
|
||||||
|
it.fill(id(face_bg));
|
||||||
|
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, 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, 114, id(font_status), id(status_wait), "WiFi: connecting...");
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool api_ok = api::global_api_server->is_connected();
|
||||||
|
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, 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);
|
||||||
|
it.printf(300, 212, id(font_status), id(status_dim), TextAlign::TOP_RIGHT, "up %us",
|
||||||
|
(unsigned) (millis() / 1000));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// stack-chan face: 2 eyes + mouth, 320x240
|
||||||
|
// the whole screen is cleared only once, then just the face areas, so that only the
|
||||||
|
// changed region is sent over SPI instead of the whole 150 kB buffer
|
||||||
|
const bool love = id(in_love);
|
||||||
|
const bool music = id(media_out).state == media_player::MEDIA_PLAYER_STATE_PLAYING && !id(va).is_running();
|
||||||
|
// switching between face, in-love face and spectrum repaints everything
|
||||||
|
static int last_mode = -1;
|
||||||
|
const int mode = music ? 2 : love ? 1 : 0;
|
||||||
|
if (mode != last_mode) {
|
||||||
|
last_mode = mode;
|
||||||
|
first_draw = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// the in-love face is drawn outside the partially cleared areas, so it always repaints everything
|
||||||
|
static bool spectrum_reset = true;
|
||||||
|
if (first_draw || love) {
|
||||||
|
it.fill(id(face_bg));
|
||||||
|
first_draw = false;
|
||||||
|
bar_drawn = false;
|
||||||
|
spectrum_reset = true;
|
||||||
|
} else if (!music) {
|
||||||
|
it.filled_rectangle(70, 78, 181, 31, id(face_bg)); // eyes including gaze range
|
||||||
|
it.filled_rectangle(105, 130, 111, 38, id(face_bg)); // mouth
|
||||||
|
if (dot_shown && !id(listening)) {
|
||||||
|
it.filled_rectangle(254, 2, 28, 28, id(face_bg));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dot_shown = id(listening);
|
||||||
|
|
||||||
|
// status bar (battery top-left, wifi top-right), redrawn only when something changes
|
||||||
|
if (!bar_drawn || wifi_bars != last_bars || batt != last_batt || charging != last_charging) {
|
||||||
|
bar_drawn = true;
|
||||||
|
last_bars = wifi_bars;
|
||||||
|
last_batt = batt;
|
||||||
|
last_charging = charging;
|
||||||
|
it.filled_rectangle(4, 4, 90, 22, id(face_bg));
|
||||||
|
it.filled_rectangle(288, 4, 28, 22, id(face_bg));
|
||||||
|
|
||||||
|
// battery: outline, terminal nub and fill proportional to the charge
|
||||||
|
if (batt >= 0) {
|
||||||
|
const Color c = charging ? id(status_ok) : batt <= 20 ? id(status_low) : id(status_dim);
|
||||||
|
it.rectangle(8, 8, 26, 13, c);
|
||||||
|
it.filled_rectangle(34, 12, 2, 5, c);
|
||||||
|
it.filled_rectangle(10, 10, 22 * batt / 100, 9, c);
|
||||||
|
it.printf(40, 6, id(font_status), c, "%d%%", batt);
|
||||||
|
}
|
||||||
|
|
||||||
|
// wifi: 4 bars, unused ones dimmed; red when disconnected
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
const int h = 4 + 3 * i;
|
||||||
|
const Color c = !wifi_ok ? id(status_low) : i < wifi_bars ? id(status_dim) : Color(40, 40, 40);
|
||||||
|
it.filled_rectangle(290 + i * 6, 21 - h, 4, h, c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Winamp style spectrum analyzer instead of the face while music plays.
|
||||||
|
// Only the segments that changed since the last frame are drawn or cleared, which keeps both the
|
||||||
|
// pixel work and the SPI transfer small.
|
||||||
|
if (music) {
|
||||||
|
static float peak[32];
|
||||||
|
static uint32_t peak_time[32];
|
||||||
|
static int prev_segs[32], prev_cap[32];
|
||||||
|
const uint32_t now = millis();
|
||||||
|
const int bands = std::min<int>(id(spectrum).get_bands(), 32);
|
||||||
|
const int x0 = 12, x1 = 308, top = 40, bottom = 228;
|
||||||
|
const int slot = (x1 - x0) / bands, bar_w = slot - 3;
|
||||||
|
const int seg_h = 5; // 4 px LED segment + 1 px gap
|
||||||
|
const int max_segs = (bottom - top) / seg_h;
|
||||||
|
if (spectrum_reset) {
|
||||||
|
spectrum_reset = false;
|
||||||
|
for (int b = 0; b < 32; b++) {
|
||||||
|
peak[b] = 0.0f;
|
||||||
|
prev_segs[b] = 0;
|
||||||
|
prev_cap[b] = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
auto seg_y = [&](int s) { return bottom - (s + 1) * seg_h + 1; };
|
||||||
|
for (int b = 0; b < bands; b++) {
|
||||||
|
const float level = id(spectrum).get_level(b);
|
||||||
|
const int x = x0 + b * slot + 1;
|
||||||
|
const int segs = std::min(max_segs, (int) (level * max_segs));
|
||||||
|
|
||||||
|
// peak caps hold for a moment, then fall slowly
|
||||||
|
if (level >= peak[b]) {
|
||||||
|
peak[b] = level;
|
||||||
|
peak_time[b] = now;
|
||||||
|
} else if (now - peak_time[b] > 400) {
|
||||||
|
peak[b] = std::max(0.0f, peak[b] - 0.03f);
|
||||||
|
}
|
||||||
|
const int cap = peak[b] > 0.0f ? std::min(max_segs - 1, std::max(segs, (int) (peak[b] * max_segs))) : -1;
|
||||||
|
|
||||||
|
// the old cap is cleared first; bar segments drawn below repaint anything it overlapped
|
||||||
|
if (prev_cap[b] >= 0 && prev_cap[b] != cap)
|
||||||
|
it.filled_rectangle(x, seg_y(prev_cap[b]), bar_w, seg_h - 1, id(face_bg));
|
||||||
|
|
||||||
|
if (segs > prev_segs[b]) {
|
||||||
|
for (int s = prev_segs[b]; s < segs; s++) {
|
||||||
|
// green at the bottom, through yellow to red at the top
|
||||||
|
const float f = float(s) / float(max_segs - 1);
|
||||||
|
const Color c = f < 0.5f ? Color((uint8_t) (510 * f), 230, 0) : Color(255, (uint8_t) (460 * (1.0f - f)), 0);
|
||||||
|
it.filled_rectangle(x, seg_y(s), bar_w, seg_h - 1, c);
|
||||||
|
}
|
||||||
|
} else if (segs < prev_segs[b]) {
|
||||||
|
it.filled_rectangle(x, seg_y(prev_segs[b] - 1), bar_w, (prev_segs[b] - segs) * seg_h, id(face_bg));
|
||||||
|
}
|
||||||
|
prev_segs[b] = segs;
|
||||||
|
|
||||||
|
if (cap >= segs && cap >= 0 && (cap != prev_cap[b] || cap == segs)) {
|
||||||
|
const int cy = seg_y(cap) + seg_h - 3;
|
||||||
|
it.filled_rectangle(x, cy, bar_w, 2, id(status_dim));
|
||||||
|
}
|
||||||
|
prev_cap[b] = cap;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int ex = id(gaze_x), ey = id(gaze_y);
|
||||||
|
const int eye_y = 93 + ey, eye_r = 8;
|
||||||
|
const int lx = 90 + ex, rx = 230 + ex;
|
||||||
|
|
||||||
|
if (love) {
|
||||||
|
// heart eyes, blush and a wide smile
|
||||||
|
for (int cx : {lx, rx}) {
|
||||||
|
it.filled_circle(cx - 6, eye_y - 3, 7, id(love_red));
|
||||||
|
it.filled_circle(cx + 6, eye_y - 3, 7, id(love_red));
|
||||||
|
it.filled_triangle(cx - 13, eye_y - 1, cx + 13, eye_y - 1, cx, eye_y + 14, id(love_red));
|
||||||
|
}
|
||||||
|
it.filled_circle(60 + ex, 130 + ey, 12, Color(90, 20, 40));
|
||||||
|
it.filled_circle(260 + ex, 130 + ey, 12, Color(90, 20, 40));
|
||||||
|
if (!id(speaking)) {
|
||||||
|
it.filled_circle(160 + ex / 2, 132 + ey / 2, 30, id(face_fg));
|
||||||
|
it.filled_circle(160 + ex / 2, 122 + ey / 2, 30, id(face_bg));
|
||||||
|
}
|
||||||
|
} else if (id(eyes_open)) {
|
||||||
|
it.filled_circle(lx, eye_y, eye_r, id(face_fg));
|
||||||
|
it.filled_circle(rx, eye_y, eye_r, id(face_fg));
|
||||||
|
} else {
|
||||||
|
it.filled_rectangle(lx - eye_r, eye_y - 2, eye_r * 2, 4, id(face_fg));
|
||||||
|
it.filled_rectangle(rx - eye_r, eye_y - 2, eye_r * 2, 4, id(face_fg));
|
||||||
|
}
|
||||||
|
|
||||||
|
const float m = id(mouth_open);
|
||||||
|
if (!love || id(speaking)) {
|
||||||
|
const int mw = 90 - (int) (40 * m);
|
||||||
|
const int mh = 4 + (int) (26 * m);
|
||||||
|
it.filled_rectangle(160 - mw / 2 + ex / 2, 148 - mh / 2 + ey / 2, mw, mh, id(face_fg));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (id(listening)) {
|
||||||
|
it.image(254, 2, id(icon_listening), id(face_fg), id(face_bg)); // left of the wifi bars
|
||||||
|
}
|
||||||
|
|
||||||
|
touchscreen:
|
||||||
|
- platform: ft63x6
|
||||||
|
id: touch_lcd
|
||||||
|
i2c_id: i2c_bsp
|
||||||
|
reset_pin:
|
||||||
|
aw9523b_id: aw9523b_hub
|
||||||
|
number: 0
|
||||||
|
on_touch:
|
||||||
|
- voice_assistant.start:
|
||||||
|
|
||||||
|
esp32_camera:
|
||||||
|
id: camera_id
|
||||||
|
name: Camera
|
||||||
|
i2c_id: i2c_bsp
|
||||||
|
setup_priority: -200 # after power rails and IO expanders
|
||||||
|
pixel_format: RGB565
|
||||||
|
max_framerate: 15 fps
|
||||||
|
frame_buffer_count: 1
|
||||||
|
# GC0308 has no JPEG, convert RGB565 -> JPEG for HA (as in BSP); runs in the main loop, so no periodic frames
|
||||||
|
jpeg_quality: 12
|
||||||
|
idle_framerate: 0 fps
|
||||||
|
vsync_pin:
|
||||||
|
number: 46
|
||||||
|
ignore_strapping_warning: true
|
||||||
|
href_pin: 38
|
||||||
|
external_clock:
|
||||||
|
pin: 2
|
||||||
|
frequency: 20MHz
|
||||||
|
pixel_clock_pin:
|
||||||
|
number: 45
|
||||||
|
ignore_strapping_warning: true
|
||||||
|
data_pins: [39, 40, 41, 42, 15, 16, 48, 47]
|
||||||
|
resolution: 320x240
|
||||||
|
frame_buffer_location: PSRAM
|
||||||
|
|
||||||
|
sensor:
|
||||||
|
- platform: axp2101
|
||||||
|
id: axp2101_sensor
|
||||||
|
battery_level:
|
||||||
|
id: battery_level
|
||||||
|
name: Battery Level
|
||||||
|
filters:
|
||||||
|
- delta: 1.0
|
||||||
|
on_value:
|
||||||
|
- component.update: display_tft
|
||||||
|
battery_charging:
|
||||||
|
id: battery_charging
|
||||||
|
name: Battery Charging
|
||||||
|
on_state:
|
||||||
|
- component.update: display_tft
|
||||||
|
- platform: ltr_als_ps
|
||||||
|
i2c_id: i2c_bsp
|
||||||
|
address: 0x23
|
||||||
|
type: ALS_PS
|
||||||
|
ambient_light:
|
||||||
|
id: ambient_light
|
||||||
|
name: Ambient Light
|
||||||
|
ps_counts:
|
||||||
|
id: proximity_counts
|
||||||
|
name: Proximity Counts
|
||||||
|
- platform: ina226
|
||||||
|
i2c_id: i2c_bsp
|
||||||
|
address: 0x41
|
||||||
|
shunt_resistance: 0.01 ohm
|
||||||
|
max_current: 3.2A
|
||||||
|
current:
|
||||||
|
id: ina226_current
|
||||||
|
name: Current
|
||||||
|
power:
|
||||||
|
id: ina226_power
|
||||||
|
name: Power
|
||||||
|
bus_voltage:
|
||||||
|
id: ina226_bus_voltage
|
||||||
|
name: Bus Voltage
|
||||||
|
|
||||||
|
si12t:
|
||||||
|
id: touch_hub
|
||||||
|
i2c_id: i2c_bsp
|
||||||
|
# report low-strength touches too, not only medium/high
|
||||||
|
intr_level: OUTPUT_ALL
|
||||||
|
|
||||||
|
text_sensor:
|
||||||
|
- platform: si12t
|
||||||
|
id: head_touch_1
|
||||||
|
name: Head Touch 1
|
||||||
|
channel: CH_1
|
||||||
|
# lower level = lower touch threshold: SENS_LEVEL_1 = 0.7 % capacitance change (default level 3 is 1.2 %),
|
||||||
|
# SENS_LEVEL_0 made neighbouring pads trigger together
|
||||||
|
sensitivity: SENS_LEVEL_1
|
||||||
|
extend_threshold: false
|
||||||
|
update_interval: 100ms
|
||||||
|
internal: true
|
||||||
|
- platform: si12t
|
||||||
|
id: head_touch_2
|
||||||
|
name: Head Touch 2
|
||||||
|
channel: CH_2
|
||||||
|
sensitivity: SENS_LEVEL_1
|
||||||
|
extend_threshold: false
|
||||||
|
update_interval: 100ms
|
||||||
|
internal: true
|
||||||
|
- platform: si12t
|
||||||
|
id: head_touch_3
|
||||||
|
name: Head Touch 3
|
||||||
|
channel: CH_3
|
||||||
|
sensitivity: SENS_LEVEL_1
|
||||||
|
extend_threshold: false
|
||||||
|
update_interval: 100ms
|
||||||
|
internal: true
|
||||||
|
|
||||||
|
uart:
|
||||||
|
- id: uart_servo
|
||||||
|
tx_pin: 6
|
||||||
|
rx_pin: 7
|
||||||
|
baud_rate: 1000000
|
||||||
|
|
||||||
|
scs9009:
|
||||||
|
|
||||||
|
ftservo:
|
||||||
|
- platform: scs9009
|
||||||
|
id: x_servo
|
||||||
|
address: 1
|
||||||
|
- platform: scs9009
|
||||||
|
id: y_servo
|
||||||
|
address: 2
|
||||||
|
|
||||||
|
number:
|
||||||
|
- platform: ftservo
|
||||||
|
ftservo_id: x_servo
|
||||||
|
angle:
|
||||||
|
id: servo_x_angle
|
||||||
|
name: Servo X Angle
|
||||||
|
min_value: -164
|
||||||
|
max_value: 164
|
||||||
|
use_raw_angle: false
|
||||||
|
angle_offset: 164
|
||||||
|
step: 5
|
||||||
|
|
||||||
|
light:
|
||||||
|
- platform: m5ioe1
|
||||||
|
id: light_bar
|
||||||
|
name: Light Bar
|
||||||
|
num_led: 12
|
||||||
|
- platform: monochromatic
|
||||||
|
id: lcd_backlight
|
||||||
|
name: LCD Backlight
|
||||||
|
output: lcd_backlight_output
|
||||||
|
restore_mode: ALWAYS_ON
|
||||||
|
|
||||||
|
remote_transmitter:
|
||||||
|
id: ir_tx
|
||||||
|
pin: 5
|
||||||
|
carrier_duty_percent: 50%
|
||||||
|
non_blocking: true
|
||||||
|
|
||||||
|
remote_receiver:
|
||||||
|
id: ir_rx
|
||||||
|
pin: 10
|
||||||
|
|
||||||
|
time:
|
||||||
|
- platform: bm8563
|
||||||
|
id: rtc_time
|
||||||
|
i2c_id: i2c_bsp
|
||||||
|
update_interval: never
|
||||||
@@ -358,5 +358,3 @@ valve:
|
|||||||
close_action:
|
close_action:
|
||||||
- switch.turn_off: irrigation_relay_8
|
- switch.turn_off: irrigation_relay_8
|
||||||
|
|
||||||
time:
|
|
||||||
- !include common/time/homeassistant.yaml
|
|
||||||
Reference in new issue
Block a user