mirror of
https://github.com/randybb/esphome-configs.git
synced 2026-10-14 08:35:12 +02:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
369578a63a | ||
|
|
bdf49db1c2 | ||
|
|
349f699572 | ||
|
|
329830e553 | ||
|
|
d813717d2c |
No files matched your search
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1194" height="240" fill="none" viewBox="0 0 1194 240"><g clip-path="url(#clip0_2_63)"><path fill="#18BCF2" d="M240 219C240 227.24 233.24 234 225 234H15C6.76 234 0 227.24 0 219V129C0 120.76 4.78 109.22 10.6 103.4L109.4 4.61999C115.24 -1.22001 124.78 -1.22001 130.62 4.61999L229.4 103.4C235.24 109.24 240 120.76 240 129V219Z"/><path fill="#F2F4F9" d="M160 78.66H80C76.68 78.66 74 81.34 74 84.66V234H86V90.66H154V102.66H104C100.68 102.66 98 105.34 98 108.66V132.66C98 135.98 100.68 138.66 104 138.66H154V150.66H104C100.68 150.66 98 153.34 98 156.66V180.66C98 183.98 100.68 186.66 104 186.66H154V198.66H104C100.68 198.66 98 201.34 98 204.66C98 207.98 100.68 210.66 104 210.66H160C163.32 210.66 166 207.98 166 204.66V180.66C166 177.34 163.32 174.66 160 174.66H110V162.66H160C163.32 162.66 166 159.98 166 156.66V132.66C166 129.34 163.32 126.66 160 126.66H110V114.66H160C163.32 114.66 166 111.98 166 108.66V84.66C166 81.34 163.32 78.66 160 78.66Z"/><path fill="#F2F4F9" d="M423 167.61V190.11H336V49.95H423V72.63H359.82V108.91H416.26V130.01H359.82V167.61H423Z"/><path fill="#F2F4F9" d="M491.08 47.41C503.9 47.41 514.54 50.53 523.04 56.79C531.54 63.03 536.76 71.39 538.7 81.83L516.3 87.93C515.04 81.93 512.12 77.31 507.54 74.05C502.94 70.81 497.28 69.17 490.52 69.17C483.26 69.17 477.52 70.91 473.28 74.37C469.02 77.83 466.9 82.45 466.9 88.19C466.9 97.25 472.52 103.13 483.78 105.81L506.66 111.53C518.48 114.59 527.32 119.45 533.2 126.11C539.08 132.77 542.02 140.97 542.02 150.71C542.02 163.21 537.48 173.31 528.42 181.03C519.36 188.75 507.24 192.61 492.04 192.61C478.36 192.61 466.78 189.39 457.36 182.95C448.04 176.27 442.54 167.45 440.86 156.51L463.26 150.69C464.08 156.87 467.16 161.73 472.5 165.23C477.84 168.73 484.64 170.47 492.9 170.47C501.16 170.47 507.08 168.79 511.6 165.45C516.14 162.11 518.4 157.57 518.4 151.81C518.4 142.81 512.78 136.87 501.52 133.99L478.64 128.45C466.88 125.77 458.06 120.99 452.16 114.15C446.26 107.31 443.3 98.95 443.3 89.07C443.3 76.69 447.7 66.65 456.48 58.93C465.26 51.21 476.8 47.35 491.12 47.35L491.08 47.41Z"/><path fill="#F2F4F9" d="M666.1 95.61C666.1 109.37 661.92 120.49 653.58 128.99C645.24 137.49 634 141.75 619.88 141.75H588.38V190.13H564.56V49.95H620.06C634.3 49.95 645.54 54.07 653.76 62.27C661.98 70.49 666.08 81.59 666.08 95.59L666.1 95.61ZM641.16 95.05C641.16 87.99 638.96 82.25 634.56 77.85C630.16 73.45 623.92 71.25 615.86 71.25H588.4V120.85H615.5C623.74 120.85 630.1 118.51 634.54 113.81C638.98 109.13 641.2 102.87 641.2 95.05H641.16Z"/><path fill="#F2F4F9" d="M687.38 49.95H711.2V108.55L772.98 108.73V49.95H796.98V190.11H772.98V129.83L711.2 129.65V190.11H687.38V49.95Z"/><path fill="#F2F4F9" d="M868.6 92.79C883.1 92.79 894.92 97.45 904.04 106.75C913.16 116.07 917.72 128.03 917.72 142.65C917.72 157.27 913.16 169.15 904.04 178.47C894.92 187.79 883.1 192.43 868.6 192.43C854.1 192.43 842.1 187.77 832.98 178.47C823.86 169.15 819.3 157.23 819.3 142.65C819.3 128.07 823.86 115.97 832.98 106.69C842.1 97.41 853.98 92.77 868.6 92.77V92.79ZM868.6 172.01C876.34 172.01 882.64 169.27 887.5 163.81C892.34 158.35 894.76 151.23 894.76 142.49C894.76 133.75 892.34 126.67 887.5 121.25C882.66 115.85 876.36 113.15 868.6 113.15C860.84 113.15 854.26 115.85 849.38 121.25C844.5 126.65 842.06 133.73 842.06 142.49C842.06 151.25 844.5 158.35 849.38 163.81C854.26 169.27 860.66 172.01 868.6 172.01Z"/><path fill="#F2F4F9" d="M1081.6 129.83V190.11H1059.56V133.29C1059.56 126.73 1057.88 121.61 1054.5 117.91C1051.12 114.23 1046.54 112.37 1040.72 112.37C1034.54 112.37 1029.6 114.39 1025.9 118.41C1022.22 122.45 1020.36 128.09 1020.36 135.33V190.09H998.04V133.27C998.04 126.71 996.4 121.59 993.12 117.89C989.84 114.21 985.3 112.35 979.48 112.35C973.3 112.35 968.32 114.37 964.58 118.39C960.84 122.41 958.96 128.07 958.96 135.31V190.07H936.28V95.39H957.38L958.14 104.49C963.64 96.43 972.6 92.39 985.04 92.39C992.28 92.39 998.58 93.89 1003.88 96.89C1009.2 99.89 1013.22 104.27 1015.98 110.01C1018.3 104.51 1022.12 100.19 1027.46 97.07C1032.8 93.95 1039.16 92.39 1046.54 92.39C1057.28 92.39 1065.82 95.71 1072.14 102.33C1078.46 108.95 1081.6 118.11 1081.6 129.79V129.83Z"/><path fill="#F2F4F9" d="M1193.54 151.11H1120.98C1121.74 158.23 1124.42 163.67 1129.04 167.43C1133.66 171.19 1139.54 173.05 1146.66 173.05C1158.22 173.05 1166.32 168.23 1170.94 158.61L1189.88 166.11C1186.32 174.43 1180.7 180.93 1173.06 185.61C1165.4 190.29 1156.6 192.65 1146.66 192.65C1132.66 192.65 1121.3 188.07 1112.58 178.91C1103.86 169.75 1099.5 157.73 1099.5 142.87C1099.5 128.01 1103.9 115.91 1112.68 106.59C1121.46 97.27 1132.94 92.63 1147.14 92.63C1161.34 92.63 1172.3 97.23 1180.8 106.41C1189.3 115.59 1193.56 127.69 1193.56 142.69V151.13L1193.54 151.11ZM1121.36 135.17H1170.48C1170.04 127.49 1167.72 121.63 1163.5 117.59C1159.28 113.55 1153.6 111.55 1146.48 111.55C1139.36 111.55 1133.66 113.59 1129.24 117.69C1124.8 121.79 1122.18 127.61 1121.36 135.17Z"/></g><defs><clipPath id="clip0_2_63"><rect width="1193.56" height="240" fill="#fff"/></cliLine truncated
|
||||
|
Before Width: | Height: | Size: 4.9 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" id="Layer_2" data-name="Layer 2" viewBox="0 0 681.98 201.98"><defs><style>.cls-1{fill:none;stroke:#1d2126;stroke-miterlimit:10;stroke-width:1.01px}.cls-2{fill:#1d2126}.cls-2,.cls-3,.cls-4{stroke-width:0}.cls-3{fill:#f2f4f9}.cls-4{fill:#18bcf2}</style></defs><g id="Layer_1-2" data-name="Layer 1"><g><g><rect width="680.97" height="200.97" x=".51" y=".51" class="cls-3" rx="40.48" ry="40.48"/><rect width="680.97" height="200.97" x=".51" y=".51" class="cls-1" rx="40.48" ry="40.48"/></g><g><path d="M236.13,43.11v26.53h-5.29v-14.5l-5.68,14.5h-5.08l-5.61-14.4v14.4h-5.12v-26.53h5.12l8.16,20.27,8.21-20.27h5.29Z" class="cls-4"/><path d="M259.54,63.84h-9.04l-2.02,5.8h-5.59l9.51-26.53h5.33l9.47,26.53h-5.7l-1.97-5.8ZM258,59.34l-2.95-8.7-2.99,8.7h5.94Z" class="cls-4"/><path d="M296.93,56.42c.02,1.89-.3,3.66-.98,5.31s-1.61,3.06-2.8,4.22-2.61,2.07-4.26,2.73-3.4.98-5.26.96h-9.65v-26.53h9.65c2.43-.04,4.66.52,6.71,1.68s3.67,2.76,4.86,4.82,1.78,4.33,1.74,6.81ZM291.25,56.42c0-2.46-.71-4.46-2.12-5.99s-3.27-2.29-5.58-2.29h-4.22v16.52h4.22c2.34,0,4.21-.75,5.61-2.26s2.09-3.5,2.09-5.99Z" class="cls-4"/><path d="M321.38,64.59v5.04h-16.7v-26.53h16.7v5.1h-11.34v5.66h10.07v4.75h-10.07v5.98h11.34Z" class="cls-4"/><path d="M348.35,48.17v6.1h9.58l-.04,4.82h-9.54v10.55h-5.36v-26.53h16.73l-.04,5.06h-11.34Z" class="cls-4"/><path d="M378.62,42.64c1.9,0,3.66.34,5.27,1.03s2.99,1.63,4.11,2.84,2,2.66,2.63,4.35.94,3.52.94,5.49c.02,1.95-.28,3.77-.91,5.48s-1.52,3.17-2.65,4.39-2.52,2.17-4.15,2.86-3.39,1.03-5.27,1c-2.5.04-4.74-.54-6.72-1.72s-3.52-2.82-4.6-4.92-1.6-4.45-1.56-7.05c-.02-1.95.28-3.77.91-5.48s1.51-3.18,2.65-4.39,2.51-2.17,4.13-2.87,3.37-1.03,5.23-1ZM378.65,64.82c2.24,0,4.01-.76,5.32-2.28s1.96-3.58,1.96-6.2-.65-4.71-1.95-6.22-3.08-2.27-5.33-2.27-4.03.76-5.33,2.27-1.95,3.59-1.95,6.22.65,4.71,1.95,6.21,3.08,2.26,5.33,2.26Z" class="cls-4"/><path d="M419.5,69.64h-6.01l-4.08-9.58h-4.89v9.58h-5.36v-26.53h10.21c3.28.01,5.76.74,7.44,2.19s2.51,3.55,2.51,6.3c0,1.79-.39,3.34-1.17,4.65s-1.89,2.28-3.33,2.91l4.68,10.48ZM413.63,51.6c0-1.15-.36-2.05-1.07-2.72s-1.78-.99-3.18-.99h-4.85v7.49h4.85c1.41,0,2.47-.34,3.18-1.01s1.07-1.6,1.07-2.77Z" class="cls-4"/></g><g><path d="M251.92,146.83v11.25h-43.5v-70.08h43.5v11.34h-31.59v18.14h28.22v10.55h-28.22v18.8h31.59Z" class="cls-2"/><path d="M285,86.74c6.41,0,11.73,1.56,15.98,4.69,4.25,3.12,6.86,7.3,7.83,12.52l-11.2,3.05c-.62-3-2.09-5.31-4.38-6.94-2.3-1.62-5.13-2.44-8.51-2.44-3.62,0-6.5.87-8.62,2.6s-3.19,4.04-3.19,6.91c0,4.53,2.81,7.47,8.44,8.81l11.44,2.86c5.91,1.53,10.33,3.96,13.27,7.29s4.41,7.43,4.41,12.3c0,6.25-2.27,11.3-6.8,15.16-4.53,3.86-10.59,5.79-18.19,5.79-6.84,0-12.62-1.61-17.34-4.83-4.66-3.34-7.41-7.75-8.25-13.22l11.2-2.91c.41,3.09,1.95,5.52,4.62,7.27,2.67,1.75,6.07,2.62,10.2,2.62s7.09-.84,9.35-2.51c2.27-1.67,3.4-3.94,3.4-6.82,0-4.5-2.81-7.47-8.44-8.91l-11.44-2.77c-5.88-1.34-10.29-3.73-13.24-7.15s-4.43-7.6-4.43-12.54c0-6.19,2.2-11.21,6.59-15.07,4.39-3.86,10.16-5.79,17.32-5.79Z" class="cls-2"/><path d="M371.55,110.83c0,6.88-2.09,12.44-6.26,16.69-4.17,4.25-9.79,6.38-16.85,6.38h-15.75v24.19h-11.91v-70.08h27.75c7.12,0,12.74,2.06,16.85,6.16,4.11,4.11,6.16,9.66,6.16,16.66ZM359.08,110.55c0-3.53-1.1-6.4-3.3-8.6s-5.32-3.3-9.35-3.3h-13.73v24.8h13.55c4.12,0,7.3-1.17,9.52-3.52s3.33-5.47,3.33-9.38Z" class="cls-2"/><path d="M381.23,88h11.91v29.3l30.89.09v-29.39h12v70.08h-12v-30.14l-30.89-.09v30.23h-11.91v-70.08Z" class="cls-2"/><path d="M470.88,109.43c7.25,0,13.16,2.33,17.72,6.98,4.56,4.66,6.84,10.64,6.84,17.95s-2.28,13.25-6.84,17.91c-4.56,4.66-10.47,6.98-17.72,6.98s-13.25-2.33-17.81-6.98c-4.56-4.66-6.84-10.62-6.84-17.91s2.28-13.34,6.84-17.98,10.5-6.96,17.81-6.96ZM470.88,149.04c3.88,0,7.02-1.37,9.45-4.1,2.42-2.73,3.63-6.29,3.63-10.66s-1.21-7.91-3.63-10.62c-2.42-2.7-5.57-4.05-9.45-4.05s-7.17,1.35-9.61,4.05c-2.44,2.7-3.66,6.24-3.66,10.62s1.22,7.93,3.66,10.66c2.44,2.73,5.64,4.1,9.61,4.1Z" class="cls-2"/><path d="M576.42,127.94v30.14h-11.02v-28.41c0-3.28-.84-5.84-2.53-7.69-1.69-1.84-3.98-2.77-6.89-2.77-3.09,0-5.56,1.01-7.41,3.02s-2.77,4.84-2.77,8.46v27.38h-11.16v-28.41c0-3.28-.82-5.84-2.46-7.69-1.64-1.84-3.91-2.77-6.82-2.77-3.09,0-5.58,1.01-7.45,3.02s-2.81,4.84-2.81,8.46v27.38h-11.34v-47.34h10.55l.38,4.55c2.75-4.03,7.23-6.05,13.45-6.05,3.62,0,6.77.75,9.42,2.25s4.67,3.69,6.05,6.56c1.16-2.75,3.07-4.91,5.74-6.47,2.67-1.56,5.85-2.34,9.54-2.34,5.38,0,9.64,1.66,12.8,4.97,3.16,3.31,4.73,7.89,4.73,13.73Z" class="cls-2"/><path d="M631.43,138.58h-36.28c.38,3.56,1.72,6.28,4.03,8.16s5.25,2.81,8.81,2.81c5.78,0,9.83-2.41,12.14-7.22l9.47,3.75c-1.78,4.16-4.59,7.41-8.41,9.75s-8.23,3.52-13.2,3.52c-7,0-12.68-2.29-17.04-6.87-4.36-4.58-6.54-10.59-6.54-18.02s2.2-13.48,6.59-18.14c4.39-4.66,10.13-6.98,17.23-6.98s12.58,2.3,16.83,6.89,6.38,10.64,6.38,18.14v4.22ZM595.34,130.61h24.56c-.22-3.84-1.38-6.77-3.49-8.79s-4.95-3.02-8.51-3.02-6.41,1.02-8.62,3.07c-2.22,2.05-3.53,4.96-3.94,8.74Z" class="cls-2"/></g><g><path d="M170.55,153.49c0,4.12-3.38,7.5-7.5,7.5H58.05c-4.12,0-7.5-3.38-7.5-7.5v-45c0-4.12,2.39-9.89,5.3-12Line truncated
|
||||
|
After Width: | Height: | Size: 5.3 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" id="Layer_2" data-name="Layer 2" viewBox="0 0 681.98 201.98"><defs><style>.cls-1{fill:#1d2126}.cls-1,.cls-2,.cls-3{stroke-width:0}.cls-2{fill:#f2f4f9}.cls-3{fill:#18bcf2}.cls-4{fill:none;stroke:#f2f4f9;stroke-miterlimit:10;stroke-width:1.01px}</style></defs><g id="Layer_1-2" data-name="Layer 1"><g><g><rect width="680.97" height="200.97" x=".51" y=".51" class="cls-1" rx="40.48" ry="40.48"/><rect width="680.97" height="200.97" x=".51" y=".51" class="cls-4" rx="40.48" ry="40.48"/></g><g><path d="M236.13,43.11v26.53h-5.29v-14.5l-5.68,14.5h-5.08l-5.61-14.4v14.4h-5.12v-26.53h5.12l8.16,20.27,8.21-20.27h5.29Z" class="cls-3"/><path d="M259.54,63.84h-9.04l-2.02,5.8h-5.59l9.51-26.53h5.33l9.47,26.53h-5.7l-1.97-5.8ZM258,59.34l-2.95-8.7-2.99,8.7h5.94Z" class="cls-3"/><path d="M296.93,56.42c.02,1.89-.3,3.66-.98,5.31s-1.61,3.06-2.8,4.22-2.61,2.07-4.26,2.73-3.4.98-5.26.96h-9.65v-26.53h9.65c2.43-.04,4.66.52,6.71,1.68s3.67,2.76,4.86,4.82,1.78,4.33,1.74,6.81ZM291.25,56.42c0-2.46-.71-4.46-2.12-5.99s-3.27-2.29-5.58-2.29h-4.22v16.52h4.22c2.34,0,4.21-.75,5.61-2.26s2.09-3.5,2.09-5.99Z" class="cls-3"/><path d="M321.38,64.59v5.04h-16.7v-26.53h16.7v5.1h-11.34v5.66h10.07v4.75h-10.07v5.98h11.34Z" class="cls-3"/><path d="M348.35,48.17v6.1h9.58l-.04,4.82h-9.54v10.55h-5.36v-26.53h16.73l-.04,5.06h-11.34Z" class="cls-3"/><path d="M378.62,42.64c1.9,0,3.66.34,5.27,1.03s2.99,1.63,4.11,2.84,2,2.66,2.63,4.35.94,3.52.94,5.49c.02,1.95-.28,3.77-.91,5.48s-1.52,3.17-2.65,4.39-2.52,2.17-4.15,2.86-3.39,1.03-5.27,1c-2.5.04-4.74-.54-6.72-1.72s-3.52-2.82-4.6-4.92-1.6-4.45-1.56-7.05c-.02-1.95.28-3.77.91-5.48s1.51-3.18,2.65-4.39,2.51-2.17,4.13-2.87,3.37-1.03,5.23-1ZM378.65,64.82c2.24,0,4.01-.76,5.32-2.28s1.96-3.58,1.96-6.2-.65-4.71-1.95-6.22-3.08-2.27-5.33-2.27-4.03.76-5.33,2.27-1.95,3.59-1.95,6.22.65,4.71,1.95,6.21,3.08,2.26,5.33,2.26Z" class="cls-3"/><path d="M419.5,69.64h-6.01l-4.08-9.58h-4.89v9.58h-5.36v-26.53h10.21c3.28.01,5.76.74,7.44,2.19s2.51,3.55,2.51,6.3c0,1.79-.39,3.34-1.17,4.65s-1.89,2.28-3.33,2.91l4.68,10.48ZM413.63,51.6c0-1.15-.36-2.05-1.07-2.72s-1.78-.99-3.18-.99h-4.85v7.49h4.85c1.41,0,2.47-.34,3.18-1.01s1.07-1.6,1.07-2.77Z" class="cls-3"/></g><g><path d="M251.92,146.83v11.25h-43.5v-70.08h43.5v11.34h-31.59v18.14h28.22v10.55h-28.22v18.8h31.59Z" class="cls-2"/><path d="M285,86.74c6.41,0,11.73,1.56,15.98,4.69,4.25,3.12,6.86,7.3,7.83,12.52l-11.2,3.05c-.62-3-2.09-5.31-4.38-6.94-2.3-1.62-5.13-2.44-8.51-2.44-3.62,0-6.5.87-8.62,2.6s-3.19,4.04-3.19,6.91c0,4.53,2.81,7.47,8.44,8.81l11.44,2.86c5.91,1.53,10.33,3.96,13.27,7.29s4.41,7.43,4.41,12.3c0,6.25-2.27,11.3-6.8,15.16-4.53,3.86-10.59,5.79-18.19,5.79-6.84,0-12.62-1.61-17.34-4.83-4.66-3.34-7.41-7.75-8.25-13.22l11.2-2.91c.41,3.09,1.95,5.52,4.62,7.27,2.67,1.75,6.07,2.62,10.2,2.62s7.09-.84,9.35-2.51c2.27-1.67,3.4-3.94,3.4-6.82,0-4.5-2.81-7.47-8.44-8.91l-11.44-2.77c-5.88-1.34-10.29-3.73-13.24-7.15s-4.43-7.6-4.43-12.54c0-6.19,2.2-11.21,6.59-15.07,4.39-3.86,10.16-5.79,17.32-5.79Z" class="cls-2"/><path d="M371.55,110.83c0,6.88-2.09,12.44-6.26,16.69-4.17,4.25-9.79,6.38-16.85,6.38h-15.75v24.19h-11.91v-70.08h27.75c7.12,0,12.74,2.06,16.85,6.16,4.11,4.11,6.16,9.66,6.16,16.66ZM359.08,110.55c0-3.53-1.1-6.4-3.3-8.6s-5.32-3.3-9.35-3.3h-13.73v24.8h13.55c4.12,0,7.3-1.17,9.52-3.52s3.33-5.47,3.33-9.38Z" class="cls-2"/><path d="M381.23,88h11.91v29.3l30.89.09v-29.39h12v70.08h-12v-30.14l-30.89-.09v30.23h-11.91v-70.08Z" class="cls-2"/><path d="M470.88,109.43c7.25,0,13.16,2.33,17.72,6.98,4.56,4.66,6.84,10.64,6.84,17.95s-2.28,13.25-6.84,17.91c-4.56,4.66-10.47,6.98-17.72,6.98s-13.25-2.33-17.81-6.98c-4.56-4.66-6.84-10.62-6.84-17.91s2.28-13.34,6.84-17.98,10.5-6.96,17.81-6.96ZM470.88,149.04c3.88,0,7.02-1.37,9.45-4.1,2.42-2.73,3.63-6.29,3.63-10.66s-1.21-7.91-3.63-10.62c-2.42-2.7-5.57-4.05-9.45-4.05s-7.17,1.35-9.61,4.05c-2.44,2.7-3.66,6.24-3.66,10.62s1.22,7.93,3.66,10.66c2.44,2.73,5.64,4.1,9.61,4.1Z" class="cls-2"/><path d="M576.42,127.94v30.14h-11.02v-28.41c0-3.28-.84-5.84-2.53-7.69-1.69-1.84-3.98-2.77-6.89-2.77-3.09,0-5.56,1.01-7.41,3.02s-2.77,4.84-2.77,8.46v27.38h-11.16v-28.41c0-3.28-.82-5.84-2.46-7.69-1.64-1.84-3.91-2.77-6.82-2.77-3.09,0-5.58,1.01-7.45,3.02s-2.81,4.84-2.81,8.46v27.38h-11.34v-47.34h10.55l.38,4.55c2.75-4.03,7.23-6.05,13.45-6.05,3.62,0,6.77.75,9.42,2.25s4.67,3.69,6.05,6.56c1.16-2.75,3.07-4.91,5.74-6.47,2.67-1.56,5.85-2.34,9.54-2.34,5.38,0,9.64,1.66,12.8,4.97,3.16,3.31,4.73,7.89,4.73,13.73Z" class="cls-2"/><path d="M631.43,138.58h-36.28c.38,3.56,1.72,6.28,4.03,8.16s5.25,2.81,8.81,2.81c5.78,0,9.83-2.41,12.14-7.22l9.47,3.75c-1.78,4.16-4.59,7.41-8.41,9.75s-8.23,3.52-13.2,3.52c-7,0-12.68-2.29-17.04-6.87-4.36-4.58-6.54-10.59-6.54-18.02s2.2-13.48,6.59-18.14c4.39-4.66,10.13-6.98,17.23-6.98s12.58,2.3,16.83,6.89,6.38,10.64,6.38,18.14v4.22ZM595.34,130.61h24.56c-.22-3.84-1.38-6.77-3.49-8.79s-4.95-3.02-8.51-3.02-6.41,1.02-8.62,3.07c-2.22,2.05-3.53,4.96-3.94,8.74Z" class="cls-2"/></g><g><path d="M170.55,153.49c0,4.12-3.38,7.5-7.5,7.5H58.05c-4.12,0-7.5-3.38-7.5-7.5v-45c0-4.12,2.39-9.89,5.3-12Line truncated
|
||||
|
After Width: | Height: | Size: 5.3 KiB |
+2
-2
@@ -24,8 +24,8 @@ text_sensor:
|
||||
# - <<: !include text_sensor/uptime.yaml
|
||||
- <<: !include text_sensor/wifi_info.yaml
|
||||
|
||||
# time:
|
||||
#. - <<: !include time/homeassistant.yaml
|
||||
time:
|
||||
- <<: !include time/homeassistant.yaml
|
||||
|
||||
network:
|
||||
enable_ipv6: true
|
||||
@@ -24,5 +24,8 @@ text_sensor:
|
||||
- <<: !include text_sensor/version.yaml
|
||||
- <<: !include text_sensor/wifi_info.yaml
|
||||
|
||||
time:
|
||||
- <<: !include time/homeassistant.yaml
|
||||
|
||||
network:
|
||||
enable_ipv6: true
|
||||
@@ -23,5 +23,8 @@ text_sensor:
|
||||
- <<: !include text_sensor/version.yaml
|
||||
- <<: !include text_sensor/wifi_info.yaml
|
||||
|
||||
time:
|
||||
- <<: !include time/homeassistant.yaml
|
||||
|
||||
network:
|
||||
enable_ipv6: true
|
||||
@@ -21,5 +21,8 @@ text_sensor:
|
||||
- <<: !include text_sensor/version.yaml
|
||||
- <<: !include text_sensor/ethernet_info.yaml
|
||||
|
||||
time:
|
||||
- <<: !include time/homeassistant.yaml
|
||||
|
||||
network:
|
||||
enable_ipv6: true
|
||||
@@ -27,3 +27,6 @@ text_sensor:
|
||||
- <<: !include text_sensor/git_ref.yaml
|
||||
- <<: !include text_sensor/version.yaml
|
||||
- <<: !include text_sensor/wifi_info.yaml
|
||||
|
||||
time:
|
||||
- <<: !include time/homeassistant.yaml
|
||||
@@ -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
|
||||
# one_wire_id: hub_1
|
||||
|
||||
time:
|
||||
- !include common/time/homeassistant.yaml
|
||||
# time:
|
||||
# - platform: sntp
|
||||
# id: sntp_time
|
||||
# timezone: Europe/Bratislava
|
||||
|
||||
+1
-2
@@ -139,8 +139,7 @@ switch:
|
||||
id: relay_2
|
||||
|
||||
time:
|
||||
- platform: homeassistant
|
||||
timezone: Europe/Bratislava
|
||||
- id: !extend ha_time
|
||||
on_time:
|
||||
- seconds: 0
|
||||
minutes: /15
|
||||
|
||||
@@ -31,11 +31,6 @@ packages:
|
||||
|
||||
debug:
|
||||
|
||||
time:
|
||||
- platform: homeassistant
|
||||
id: ha_time
|
||||
timezone: Europe/Bratislava
|
||||
|
||||
i2c:
|
||||
# - id: i2c0
|
||||
sda: 32
|
||||
|
||||
@@ -109,11 +109,6 @@ globals:
|
||||
restore_value: no
|
||||
initial_value: "false"
|
||||
|
||||
time:
|
||||
- platform: homeassistant
|
||||
id: ha_time
|
||||
timezone: Europe/Bratislava
|
||||
|
||||
display:
|
||||
- platform: tm1637
|
||||
id: tm1637_display
|
||||
|
||||
@@ -51,11 +51,6 @@ globals:
|
||||
restore_value: no
|
||||
initial_value: "false"
|
||||
|
||||
time:
|
||||
- platform: homeassistant
|
||||
id: ha_time
|
||||
timezone: Europe/Bratislava
|
||||
|
||||
binary_sensor:
|
||||
- platform: gpio
|
||||
name: Button
|
||||
|
||||
@@ -242,11 +242,6 @@ binary_sensor:
|
||||
# on_clockwise:
|
||||
# - display_menu.down:
|
||||
|
||||
time:
|
||||
- platform: homeassistant
|
||||
id: ha_time
|
||||
timezone: Europe/Bratislava
|
||||
|
||||
# display:
|
||||
# - platform: tm1637
|
||||
# id: tm1637_display
|
||||
|
||||
@@ -44,11 +44,6 @@ packages:
|
||||
# Relay1 G22
|
||||
# Relay2 G19
|
||||
|
||||
time:
|
||||
- platform: homeassistant
|
||||
id: ha_time
|
||||
timezone: Europe/Bratislava
|
||||
|
||||
i2c:
|
||||
# - id: i2c0
|
||||
# sda: 26
|
||||
|
||||
+1
-3
@@ -129,9 +129,7 @@ globals:
|
||||
initial_value: '20.5'
|
||||
|
||||
time:
|
||||
- platform: homeassistant
|
||||
id: ha_time
|
||||
timezone: Europe/Bratislava
|
||||
- id: !extend ha_time
|
||||
on_time:
|
||||
- seconds: 0
|
||||
minutes: /1
|
||||
|
||||
@@ -55,11 +55,6 @@ api:
|
||||
# Relay1 G22
|
||||
# Relay2 G19
|
||||
|
||||
time:
|
||||
- platform: homeassistant
|
||||
id: ha_time
|
||||
timezone: Europe/Bratislava
|
||||
|
||||
i2c:
|
||||
# - id: i2c0
|
||||
# sda: 26
|
||||
|
||||
+556
-140
@@ -4,6 +4,13 @@ substitutions:
|
||||
name: Trainer Display
|
||||
area: Basement
|
||||
comment: "${area} | Pain Cave"
|
||||
light_basement: light.mcu_basement_2_light
|
||||
light_workbench: light.mcu_basement_2_workbench_light
|
||||
light_exercise: light.exercise_room
|
||||
fan: light.mcu_dimmer_exercise_room_fan
|
||||
|
||||
# Pages (480x320, navigation bar at the bottom):
|
||||
# clock, lights, training, fan, stopwatch, status
|
||||
|
||||
esphome:
|
||||
name: mcu-${device}
|
||||
@@ -17,7 +24,6 @@ esp32:
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
|
||||
packages:
|
||||
common: !include common/common.yaml
|
||||
|
||||
@@ -36,71 +42,17 @@ spi:
|
||||
clk_pin: 47
|
||||
data_pins: [21, 48, 40, 39]
|
||||
|
||||
color:
|
||||
- id: my_red
|
||||
red: 100%
|
||||
green: 0%
|
||||
blue: 0%
|
||||
- id: my_orange
|
||||
red: 100%
|
||||
green: 50%
|
||||
blue: 0%
|
||||
- id: my_yellow
|
||||
red: 100%
|
||||
green: 100%
|
||||
blue: 0%
|
||||
- id: my_green
|
||||
red: 0%
|
||||
green: 100%
|
||||
blue: 0%
|
||||
- id: my_blue
|
||||
red: 0%
|
||||
green: 0%
|
||||
blue: 100%
|
||||
- id: my_teal
|
||||
red: 0%
|
||||
green: 100%
|
||||
blue: 100%
|
||||
- id: my_gray
|
||||
red: 70%
|
||||
green: 70%
|
||||
blue: 70%
|
||||
- id: my_white
|
||||
red: 100%
|
||||
green: 100%
|
||||
blue: 100%
|
||||
|
||||
font:
|
||||
- file: "gfonts://Roboto"
|
||||
id: Roboto_48
|
||||
size: 48
|
||||
- file: "gfonts://Roboto"
|
||||
id: Roboto_36
|
||||
size: 36
|
||||
- file: "gfonts://Roboto"
|
||||
id: Roboto_24
|
||||
size: 24
|
||||
- file: "gfonts://Roboto"
|
||||
id: Roboto_12
|
||||
size: 12
|
||||
- file: "gfonts://Roboto"
|
||||
id: font_std
|
||||
size: 40
|
||||
glyphs: "!\"%()+=,-_.:°0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz/\\[]|&@#'"
|
||||
- file: "gfonts://Roboto@700"
|
||||
id: font_title
|
||||
size: 40
|
||||
glyphs: "!\"%()+=,-_.:°0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz/\\[]|&@#'"
|
||||
|
||||
globals:
|
||||
- id: bgcolor
|
||||
type: Color
|
||||
initial_value: "Color::random_color()"
|
||||
|
||||
# power_supply:
|
||||
# id: main_backlight
|
||||
# pin: 1
|
||||
# enable_on_boot: true
|
||||
# stopwatch: accumulated time of finished runs + start of the current run
|
||||
- id: sw_running
|
||||
type: bool
|
||||
initial_value: 'false'
|
||||
- id: sw_start
|
||||
type: uint32_t
|
||||
initial_value: '0'
|
||||
- id: sw_accum
|
||||
type: uint32_t
|
||||
initial_value: '0'
|
||||
|
||||
output:
|
||||
- platform: ledc
|
||||
@@ -119,7 +71,6 @@ display:
|
||||
- platform: mipi_spi
|
||||
model: AXS15231
|
||||
data_rate: 40MHz #20mhz is default
|
||||
#spi_mode: MODE2
|
||||
id: main_display
|
||||
spi_id: display_qspi
|
||||
dimensions:
|
||||
@@ -127,11 +78,6 @@ display:
|
||||
width: 320
|
||||
cs_pin: 45
|
||||
auto_clear_enabled: false
|
||||
# show_test_card: true
|
||||
# lambda: |-
|
||||
# it.fill(id(bgcolor));
|
||||
# it.print(it.get_width()/2, it.get_height()/2-20, id(font_title), TextAlign::CENTER, "ESPHome");
|
||||
# it.print(it.get_width()/2, it.get_height()/2+20, id(font_std), TextAlign::CENTER, "@UTBDK");
|
||||
|
||||
touchscreen:
|
||||
- platform: axs15231
|
||||
@@ -142,27 +88,8 @@ touchscreen:
|
||||
# interrupt_pin:
|
||||
# number: 3
|
||||
# ignore_strapping_warning: true
|
||||
calibration:
|
||||
x_min: 0
|
||||
x_max: 479
|
||||
y_min: 0
|
||||
y_max: 319
|
||||
transform:
|
||||
swap_xy: true
|
||||
mirror_x: false
|
||||
mirror_y: true
|
||||
# on_touch:
|
||||
# - lambda: |-
|
||||
# Color newColor;
|
||||
# do { newColor = Color::random_color(); } while (newColor == id(bgcolor));
|
||||
# id(bgcolor) = newColor;
|
||||
|
||||
# ESP_LOGI("cal", "x=%d, y=%d, x_raw=%d, y_raw=%0d",
|
||||
# touch.x,
|
||||
# touch.y,
|
||||
# touch.x_raw,
|
||||
# touch.y_raw
|
||||
# );
|
||||
# no calibration/transform: the driver defaults to the native 320x480 panel and
|
||||
# LVGL applies its own rotation to touch points
|
||||
on_release:
|
||||
- if:
|
||||
condition: lvgl.is_paused
|
||||
@@ -183,107 +110,596 @@ uart:
|
||||
|
||||
binary_sensor:
|
||||
- platform: homeassistant
|
||||
id: exercise_room_light
|
||||
entity_id: light.exercise_room
|
||||
id: basement_light
|
||||
entity_id: ${light_basement}
|
||||
trigger_on_initial_state: true
|
||||
on_state:
|
||||
then:
|
||||
lvgl.widget.update:
|
||||
id: exercise_room_light_btn
|
||||
- lvgl.widget.update:
|
||||
id: basement_light_btn
|
||||
state:
|
||||
checked: !lambda return x;
|
||||
- platform: homeassistant
|
||||
id: exercise_room_fan
|
||||
entity_id: light.mcu_dimmer_exercise_room_fan
|
||||
id: workbench_light
|
||||
entity_id: ${light_workbench}
|
||||
trigger_on_initial_state: true
|
||||
on_state:
|
||||
then:
|
||||
lvgl.widget.update:
|
||||
id: exercise_room_fan_btn
|
||||
- lvgl.widget.update:
|
||||
id: workbench_light_btn
|
||||
state:
|
||||
checked: !lambda return x;
|
||||
- platform: homeassistant
|
||||
id: exercise_light
|
||||
entity_id: ${light_exercise}
|
||||
trigger_on_initial_state: true
|
||||
on_state:
|
||||
- lvgl.widget.update:
|
||||
id: exercise_light_btn
|
||||
state:
|
||||
checked: !lambda return x;
|
||||
# brightness and color temperature only make sense while the light is on
|
||||
- if:
|
||||
condition:
|
||||
lambda: return x;
|
||||
then:
|
||||
- lvgl.widget.show: exercise_controls
|
||||
else:
|
||||
- lvgl.widget.hide: exercise_controls
|
||||
- platform: homeassistant
|
||||
id: fan_on
|
||||
entity_id: ${fan}
|
||||
trigger_on_initial_state: true
|
||||
on_state:
|
||||
- script.execute: update_fan_buttons
|
||||
|
||||
sensor:
|
||||
- platform: homeassistant
|
||||
id: fan_brightness
|
||||
entity_id: ${fan}
|
||||
attribute: brightness
|
||||
on_value:
|
||||
- script.execute: update_fan_buttons
|
||||
|
||||
text_sensor:
|
||||
# light attributes read as text: they are 'None' while the light is off
|
||||
- platform: homeassistant
|
||||
id: exercise_brightness
|
||||
entity_id: ${light_exercise}
|
||||
attribute: brightness
|
||||
on_value:
|
||||
- if:
|
||||
condition:
|
||||
lambda: return parse_number<float>(x).has_value();
|
||||
then:
|
||||
- lvgl.slider.update:
|
||||
id: exercise_brightness_slider
|
||||
value: !lambda return roundf(*parse_number<float>(x) * 100.0f / 255.0f);
|
||||
- platform: homeassistant
|
||||
id: exercise_color_temp
|
||||
entity_id: ${light_exercise}
|
||||
attribute: color_temp_kelvin
|
||||
on_value:
|
||||
- if:
|
||||
condition:
|
||||
lambda: return parse_number<float>(x).has_value();
|
||||
then:
|
||||
- lvgl.slider.update:
|
||||
id: exercise_color_temp_slider
|
||||
value: !lambda return *parse_number<float>(x);
|
||||
# a running automation (e.g. its delay) has current > 0
|
||||
- platform: homeassistant
|
||||
id: training_start_running
|
||||
entity_id: automation.zwift_enable_fan
|
||||
attribute: current
|
||||
on_value:
|
||||
- script.execute: update_training_status
|
||||
- platform: homeassistant
|
||||
id: training_stop_running
|
||||
entity_id: automation.zwift_disable_fan
|
||||
attribute: current
|
||||
on_value:
|
||||
- script.execute: update_training_status
|
||||
|
||||
script:
|
||||
- id: update_fan_buttons
|
||||
then:
|
||||
- lambda: |-
|
||||
// fan level rounded to the 10 % steps of the buttons, 0 = off
|
||||
int pct = 0;
|
||||
if (id(fan_on).state && !std::isnan(id(fan_brightness).state))
|
||||
pct = (int) roundf(id(fan_brightness).state * 10.0f / 255.0f) * 10;
|
||||
const int levels[] = {0, 50, 60, 70, 80, 90, 100};
|
||||
lv_obj_t *matrix = id(fan_matrix)->obj;
|
||||
for (int i = 0; i < 7; i++) {
|
||||
if (levels[i] == pct)
|
||||
lv_buttonmatrix_set_button_ctrl(matrix, i, LV_BUTTONMATRIX_CTRL_CHECKED);
|
||||
else
|
||||
lv_buttonmatrix_clear_button_ctrl(matrix, i, LV_BUTTONMATRIX_CTRL_CHECKED);
|
||||
}
|
||||
- lvgl.label.update:
|
||||
id: fan_status_label
|
||||
text: !lambda |-
|
||||
if (!id(fan_on).state)
|
||||
return std::string("Ventilátor vypnutý");
|
||||
if (std::isnan(id(fan_brightness).state))
|
||||
return std::string("Ventilátor zapnutý");
|
||||
return str_sprintf("Ventilátor %.0f %%", id(fan_brightness).state * 100.0f / 255.0f);
|
||||
|
||||
- id: update_training_status
|
||||
then:
|
||||
- lvgl.label.update:
|
||||
id: training_status_label
|
||||
text: !lambda |-
|
||||
if (id(training_start_running).state != "0" && !id(training_start_running).state.empty())
|
||||
return std::string("Tréning sa spúšťa, ventilátor sa zapne o 10 min");
|
||||
if (id(training_stop_running).state != "0" && !id(training_stop_running).state.empty())
|
||||
return std::string("Tréning sa ukončuje, ventilátor sa vypne o 2 min");
|
||||
return std::string("");
|
||||
|
||||
- id: update_stopwatch
|
||||
then:
|
||||
- lvgl.label.update:
|
||||
id: stopwatch_label
|
||||
text: !lambda |-
|
||||
const uint32_t ms = id(sw_accum) + (id(sw_running) ? millis() - id(sw_start) : 0);
|
||||
const uint32_t s = ms / 1000;
|
||||
if (s >= 3600)
|
||||
return str_sprintf("%u:%02u:%02u", s / 3600, (s / 60) % 60, s % 60);
|
||||
return str_sprintf("%02u:%02u.%u", s / 60, s % 60, (ms / 100) % 10);
|
||||
- lvgl.label.update:
|
||||
id: stopwatch_start_label
|
||||
text: !lambda |-
|
||||
return std::string(id(sw_running) ? "Stop" : "Štart");
|
||||
|
||||
- id: update_status
|
||||
then:
|
||||
- lvgl.label.update:
|
||||
id: status_label
|
||||
text: !lambda |-
|
||||
std::string out;
|
||||
if (wifi::global_wifi_component->is_connected()) {
|
||||
out = str_sprintf("WiFi: connected (%d dBm)", wifi::global_wifi_component->wifi_rssi());
|
||||
// IPv6 first (skip link-local), then IPv4
|
||||
char buf[network::IP_ADDRESS_BUFFER_SIZE];
|
||||
for (bool v6 : {true, false}) {
|
||||
for (auto &ip : wifi::global_wifi_component->get_ip_addresses()) {
|
||||
if (!ip.is_set() || ip.is_ip6() != v6)
|
||||
continue;
|
||||
ip.str_to(buf);
|
||||
if (v6 && strncmp(buf, "fe80", 4) == 0)
|
||||
continue;
|
||||
out += "\n ";
|
||||
out += buf;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
out = "WiFi: connecting...";
|
||||
}
|
||||
out += api::global_api_server->is_connected_with_state_subscription() ? "\nHome Assistant: connected"
|
||||
: "\nHome Assistant: waiting...";
|
||||
const uint32_t up = millis() / 1000;
|
||||
out += str_sprintf("\nESPHome %s · up %ud %02u:%02u", ESPHOME_VERSION, up / 86400, (up / 3600) % 24,
|
||||
(up / 60) % 60);
|
||||
return out;
|
||||
|
||||
interval:
|
||||
- interval: 1s
|
||||
then:
|
||||
- lvgl.label.update:
|
||||
id: clock_label
|
||||
text: !lambda |-
|
||||
auto now = id(ha_time).now();
|
||||
if (!now.is_valid())
|
||||
return std::string("--:--:--");
|
||||
return now.strftime("%H:%M:%S");
|
||||
- interval: 100ms
|
||||
then:
|
||||
- if:
|
||||
condition:
|
||||
and:
|
||||
- lambda: return id(sw_running);
|
||||
- lvgl.page.is_showing: stopwatch_page
|
||||
then:
|
||||
- script.execute: update_stopwatch
|
||||
- interval: 2s
|
||||
then:
|
||||
- if:
|
||||
condition:
|
||||
lvgl.page.is_showing: status_page
|
||||
then:
|
||||
- script.execute: update_status
|
||||
|
||||
image:
|
||||
- platform: file
|
||||
file: assets/images/made-for-esphome-white-on-black.svg
|
||||
id: esphome_logo
|
||||
resize: 280x83
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
|
||||
font:
|
||||
- file: "gfonts://Roboto@500"
|
||||
id: font_digits
|
||||
size: 110
|
||||
glyphs: "0123456789:.-"
|
||||
- file: "gfonts://Roboto"
|
||||
id: font_ui
|
||||
size: 24
|
||||
glyphs: &sk_glyphs "ÁÄČĎÉÍĹĽŇÓÔŔŠŤÚÝŽáäčďéíĺľňóôŕšťúýž°·"
|
||||
glyphsets: [GF_Latin_Kernel]
|
||||
- file: "gfonts://Roboto@500"
|
||||
id: font_button
|
||||
size: 34
|
||||
glyphs: *sk_glyphs
|
||||
glyphsets: [GF_Latin_Kernel]
|
||||
- file: "gfonts://Roboto"
|
||||
id: font_small
|
||||
size: 18
|
||||
glyphs: *sk_glyphs
|
||||
glyphsets: [GF_Latin_Kernel]
|
||||
|
||||
lvgl:
|
||||
rotation: 90
|
||||
buffer_size: 100%
|
||||
log_level: WARN
|
||||
color_depth: 16
|
||||
default_font: montserrat_18
|
||||
# on_idle:
|
||||
# - timeout: !lambda "return (id(display_timeout).state * 1000);"
|
||||
# then:
|
||||
# - logger.log: "LVGL is idle"
|
||||
# - light.turn_off:
|
||||
# id: display_backlight
|
||||
# transition_length: 2500ms
|
||||
# - lvgl.pause:
|
||||
default_font: font_ui
|
||||
bg_color: 0x000000
|
||||
text_color: 0xFFFFFF
|
||||
theme:
|
||||
button:
|
||||
bg_color: 0x2B2F36
|
||||
radius: 12
|
||||
checked:
|
||||
bg_color: 0xE08A00
|
||||
buttonmatrix:
|
||||
bg_opa: TRANSP
|
||||
border_width: 0
|
||||
items:
|
||||
bg_color: 0x2B2F36
|
||||
text_color: 0xFFFFFF
|
||||
radius: 12
|
||||
checked:
|
||||
bg_color: 0xE08A00
|
||||
slider:
|
||||
bg_color: 0x2B2F36
|
||||
indicator:
|
||||
bg_color: 0xE08A00
|
||||
knob:
|
||||
bg_color: 0xFFFFFF
|
||||
top_layer:
|
||||
widgets:
|
||||
- buttonmatrix:
|
||||
id: nav_bar
|
||||
align: bottom_mid
|
||||
# styles: header_footer
|
||||
pad_all: 0
|
||||
outline_width: 0
|
||||
id: top_layer
|
||||
# items:
|
||||
# styles: header_footer
|
||||
width: 100%
|
||||
height: 56
|
||||
pad_all: 4
|
||||
pad_column: 6
|
||||
items:
|
||||
text_font: montserrat_28
|
||||
rows:
|
||||
- buttons:
|
||||
- id: page_prev
|
||||
text: "\uF053"
|
||||
text: ""
|
||||
on_press:
|
||||
then:
|
||||
lvgl.page.previous:
|
||||
- id: page_home
|
||||
text: "\uF015"
|
||||
text: ""
|
||||
on_press:
|
||||
then:
|
||||
lvgl.page.show: main_page
|
||||
lvgl.page.show: clock_page
|
||||
- id: page_next
|
||||
text: "\uF054"
|
||||
text: ""
|
||||
on_press:
|
||||
then:
|
||||
lvgl.page.next:
|
||||
pages:
|
||||
- id: main_page
|
||||
# 1: clock (default after boot)
|
||||
- id: clock_page
|
||||
widgets:
|
||||
- label:
|
||||
text: "ESPHome LVGL Display"
|
||||
id: clock_label
|
||||
align: CENTER
|
||||
text_align: CENTER
|
||||
text_color: 0xFFFFFF
|
||||
- id: excercise_page
|
||||
y: -28
|
||||
text_font: font_digits
|
||||
text: "--:--:--"
|
||||
|
||||
# 2: basement lights
|
||||
- id: lights_page
|
||||
pad_all: 10
|
||||
widgets:
|
||||
- label:
|
||||
align: CENTER
|
||||
text: 'Hello World!'
|
||||
- button:
|
||||
id: exercise_room_light_btn
|
||||
id: basement_light_btn
|
||||
align: TOP_LEFT
|
||||
width: 100
|
||||
height: 70
|
||||
width: 148
|
||||
height: 80
|
||||
checkable: true
|
||||
widgets:
|
||||
- label:
|
||||
align: CENTER
|
||||
text: 'Light'
|
||||
text: "Pivnica"
|
||||
on_click:
|
||||
- homeassistant.action:
|
||||
action: light.toggle
|
||||
data:
|
||||
entity_id: light.exercise_room
|
||||
entity_id: ${light_basement}
|
||||
- button:
|
||||
id: exercise_room_fan_btn
|
||||
align: TOP_RIGHT
|
||||
width: 100
|
||||
height: 70
|
||||
id: workbench_light_btn
|
||||
align: TOP_MID
|
||||
width: 148
|
||||
height: 80
|
||||
checkable: true
|
||||
widgets:
|
||||
- label:
|
||||
align: CENTER
|
||||
text: 'Fan'
|
||||
text: "Pracovný stôl"
|
||||
on_click:
|
||||
- homeassistant.action:
|
||||
action: light.toggle
|
||||
data:
|
||||
entity_id: light.mcu_dimmer_exercise_room_fan
|
||||
entity_id: ${light_workbench}
|
||||
- button:
|
||||
id: exercise_light_btn
|
||||
align: TOP_RIGHT
|
||||
width: 148
|
||||
height: 80
|
||||
checkable: true
|
||||
widgets:
|
||||
- label:
|
||||
align: CENTER
|
||||
text: "Posilka"
|
||||
on_click:
|
||||
- homeassistant.action:
|
||||
action: light.toggle
|
||||
data:
|
||||
entity_id: ${light_exercise}
|
||||
- obj:
|
||||
id: exercise_controls
|
||||
hidden: true
|
||||
y: 110
|
||||
width: 460
|
||||
height: 120
|
||||
pad_all: 0
|
||||
bg_opa: TRANSP
|
||||
border_width: 0
|
||||
scrollbar_mode: "OFF"
|
||||
widgets:
|
||||
- label:
|
||||
x: 4
|
||||
y: 0
|
||||
text_font: font_small
|
||||
text: "Posilka jas"
|
||||
- slider:
|
||||
id: exercise_brightness_slider
|
||||
x: 14
|
||||
y: 32
|
||||
width: 430
|
||||
height: 16
|
||||
min_value: 1
|
||||
max_value: 100
|
||||
value: 100
|
||||
on_release:
|
||||
- homeassistant.action:
|
||||
action: light.turn_on
|
||||
data:
|
||||
entity_id: ${light_exercise}
|
||||
brightness_pct: !lambda return (int) x;
|
||||
- label:
|
||||
x: 4
|
||||
y: 62
|
||||
text_font: font_small
|
||||
text: "Posilka teplota (2700 – 6500 K)"
|
||||
- slider:
|
||||
id: exercise_color_temp_slider
|
||||
x: 14
|
||||
y: 94
|
||||
width: 430
|
||||
height: 16
|
||||
min_value: 2700
|
||||
max_value: 6500
|
||||
value: 2700
|
||||
on_release:
|
||||
- homeassistant.action:
|
||||
action: light.turn_on
|
||||
data:
|
||||
entity_id: ${light_exercise}
|
||||
color_temp_kelvin: !lambda return (int) x;
|
||||
|
||||
# 3: training start / stop
|
||||
- id: training_page
|
||||
pad_all: 10
|
||||
widgets:
|
||||
- button:
|
||||
align: TOP_LEFT
|
||||
y: 10
|
||||
width: 222
|
||||
height: 150
|
||||
bg_color: 0x1E7D32
|
||||
widgets:
|
||||
- label:
|
||||
align: CENTER
|
||||
text_font: font_button
|
||||
text: "Štart"
|
||||
on_click:
|
||||
- homeassistant.action:
|
||||
action: automation.trigger
|
||||
data:
|
||||
entity_id: automation.zwift_enable_fan
|
||||
- button:
|
||||
align: TOP_RIGHT
|
||||
y: 10
|
||||
width: 222
|
||||
height: 150
|
||||
bg_color: 0xB71C1C
|
||||
widgets:
|
||||
- label:
|
||||
align: CENTER
|
||||
text_font: font_button
|
||||
text: "Stop"
|
||||
on_click:
|
||||
- homeassistant.action:
|
||||
action: automation.trigger
|
||||
data:
|
||||
entity_id: automation.zwift_disable_fan
|
||||
- label:
|
||||
id: training_status_label
|
||||
align: TOP_MID
|
||||
y: 185
|
||||
width: 440
|
||||
text_align: CENTER
|
||||
text_font: font_small
|
||||
text: ""
|
||||
|
||||
# 4: fan
|
||||
- id: fan_page
|
||||
pad_all: 10
|
||||
widgets:
|
||||
- buttonmatrix:
|
||||
id: fan_matrix
|
||||
align: TOP_MID
|
||||
width: 460
|
||||
height: 180
|
||||
pad_row: 8
|
||||
pad_column: 8
|
||||
one_checked: true
|
||||
items:
|
||||
text_font: font_button
|
||||
rows:
|
||||
- buttons:
|
||||
- text: "Vyp"
|
||||
control:
|
||||
checkable: true
|
||||
on_press:
|
||||
- homeassistant.action:
|
||||
action: light.turn_off
|
||||
data:
|
||||
entity_id: ${fan}
|
||||
- text: "50"
|
||||
control:
|
||||
checkable: true
|
||||
on_press:
|
||||
- homeassistant.action:
|
||||
action: light.turn_on
|
||||
data:
|
||||
entity_id: ${fan}
|
||||
brightness_pct: "50"
|
||||
- text: "60"
|
||||
control:
|
||||
checkable: true
|
||||
on_press:
|
||||
- homeassistant.action:
|
||||
action: light.turn_on
|
||||
data:
|
||||
entity_id: ${fan}
|
||||
brightness_pct: "60"
|
||||
- text: "70"
|
||||
control:
|
||||
checkable: true
|
||||
on_press:
|
||||
- homeassistant.action:
|
||||
action: light.turn_on
|
||||
data:
|
||||
entity_id: ${fan}
|
||||
brightness_pct: "70"
|
||||
- buttons:
|
||||
- text: "80"
|
||||
control:
|
||||
checkable: true
|
||||
on_press:
|
||||
- homeassistant.action:
|
||||
action: light.turn_on
|
||||
data:
|
||||
entity_id: ${fan}
|
||||
brightness_pct: "80"
|
||||
- text: "90"
|
||||
control:
|
||||
checkable: true
|
||||
on_press:
|
||||
- homeassistant.action:
|
||||
action: light.turn_on
|
||||
data:
|
||||
entity_id: ${fan}
|
||||
brightness_pct: "90"
|
||||
- text: "100"
|
||||
control:
|
||||
checkable: true
|
||||
on_press:
|
||||
- homeassistant.action:
|
||||
action: light.turn_on
|
||||
data:
|
||||
entity_id: ${fan}
|
||||
brightness_pct: "100"
|
||||
- label:
|
||||
id: fan_status_label
|
||||
align: TOP_MID
|
||||
y: 200
|
||||
text_font: font_small
|
||||
text: ""
|
||||
|
||||
# 5: stopwatch
|
||||
- id: stopwatch_page
|
||||
pad_all: 10
|
||||
widgets:
|
||||
- label:
|
||||
id: stopwatch_label
|
||||
align: TOP_MID
|
||||
y: 10
|
||||
text_font: font_digits
|
||||
text: "00:00.0"
|
||||
- button:
|
||||
align: TOP_LEFT
|
||||
y: 150
|
||||
width: 222
|
||||
height: 90
|
||||
widgets:
|
||||
- label:
|
||||
id: stopwatch_start_label
|
||||
align: CENTER
|
||||
text_font: font_button
|
||||
text: "Štart"
|
||||
on_click:
|
||||
- lambda: |-
|
||||
if (id(sw_running)) {
|
||||
id(sw_accum) += millis() - id(sw_start);
|
||||
id(sw_running) = false;
|
||||
} else {
|
||||
id(sw_start) = millis();
|
||||
id(sw_running) = true;
|
||||
}
|
||||
- script.execute: update_stopwatch
|
||||
- button:
|
||||
align: TOP_RIGHT
|
||||
y: 150
|
||||
width: 222
|
||||
height: 90
|
||||
widgets:
|
||||
- label:
|
||||
align: CENTER
|
||||
text_font: font_button
|
||||
text: "Reset"
|
||||
on_click:
|
||||
- lambda: |-
|
||||
id(sw_accum) = 0;
|
||||
id(sw_start) = millis();
|
||||
- script.execute: update_stopwatch
|
||||
|
||||
# 6: status
|
||||
- id: status_page
|
||||
on_load:
|
||||
- script.execute: update_status
|
||||
widgets:
|
||||
- image:
|
||||
src: esphome_logo
|
||||
align: TOP_MID
|
||||
y: 8
|
||||
- label:
|
||||
align: TOP_MID
|
||||
y: 96
|
||||
text: "${name}"
|
||||
- label:
|
||||
id: status_label
|
||||
x: 20
|
||||
y: 130
|
||||
text_font: font_small
|
||||
text: ""
|
||||
@@ -158,10 +158,6 @@ text_sensor:
|
||||
icon: mdi:clock-start
|
||||
internal: True
|
||||
|
||||
time:
|
||||
- platform: homeassistant
|
||||
id: time_comp
|
||||
|
||||
number:
|
||||
- platform: template
|
||||
initial_value: 0
|
||||
|
||||
+564
-77
@@ -4,94 +4,370 @@ substitutions:
|
||||
name: M5Paper
|
||||
area: DEV
|
||||
comment: M5Paper
|
||||
weather_entity: weather.openweathermap
|
||||
|
||||
# Power modes
|
||||
# - USB: always on, WiFi + API, forecast refreshed every hour and redrawn every 15 min
|
||||
# - battery: RTC wakes the board every 15 min (off 1:00-5:00), it draws the forecast
|
||||
# from EEPROM and powers itself off; WiFi only to refresh the forecast every 6 h
|
||||
# Screens: status (after power-on, until Home Assistant connects) and forecast
|
||||
# The mode is detected by trying to power off: on USB the board keeps running.
|
||||
|
||||
esphome:
|
||||
name: mcu-${device}
|
||||
friendly_name: ${name}
|
||||
area: ${area}
|
||||
comment: ${comment}
|
||||
includes:
|
||||
- include/m5paper_weather.h
|
||||
on_boot:
|
||||
- priority: 600
|
||||
then:
|
||||
- lambda: |-
|
||||
// BM8563 CONTROL_STATUS_2: TF (bit 2) is set when the timer woke us up
|
||||
const uint8_t reg = 0x01;
|
||||
uint8_t status = 0;
|
||||
if (id(i2c_main)->write_readv(0x51, ®, 1, &status, 1) == i2c::ERROR_OK) {
|
||||
id(rtc_wake) = status & 0x04;
|
||||
// clear TF and TIE so the RTC releases the power latch
|
||||
const uint8_t clear[2] = {reg, uint8_t(status & ~0x05)};
|
||||
id(i2c_main)->write(0x51, clear, 2);
|
||||
}
|
||||
m5paper_weather::load(id(i2c_main), m5paper_weather::forecast());
|
||||
ESP_LOGI("m5paper", "Wake-up: %s, forecast: %u h", id(rtc_wake) ? "RTC" : "power-on", m5paper_weather::forecast().count);
|
||||
- priority: -100
|
||||
then:
|
||||
- if:
|
||||
condition:
|
||||
lambda: return id(rtc_wake);
|
||||
then:
|
||||
- script.execute: battery_cycle
|
||||
else:
|
||||
# power button / USB: stay awake for a while (logs, OTA), then decide the mode
|
||||
- wifi.enable:
|
||||
# IT8951 init runs asynchronously after setup
|
||||
- delay: 3s
|
||||
- component.update: m5paper_display
|
||||
- delay: 2min
|
||||
- script.execute: battery_cycle
|
||||
|
||||
esp32:
|
||||
variant: esp32
|
||||
toolchain: esp-idf
|
||||
framework:
|
||||
type: esp-idf
|
||||
advanced:
|
||||
flash_chip: generic
|
||||
minimum_chip_revision: 3.0
|
||||
sram1_as_iram: true
|
||||
|
||||
psram:
|
||||
|
||||
external_components:
|
||||
- source: github://Passific/m5paper_esphome
|
||||
|
||||
packages:
|
||||
common: !include common/common.yaml
|
||||
|
||||
time:
|
||||
- platform: homeassistant
|
||||
id: ha_time
|
||||
timezone: Europe/Bratislava
|
||||
on_time_sync:
|
||||
- bm8563.write_time
|
||||
- platform: bm8563
|
||||
id: rtc_time
|
||||
sleep_duration: 3600000ms
|
||||
globals:
|
||||
- id: show_status
|
||||
type: bool
|
||||
initial_value: 'true'
|
||||
- id: usb_mode
|
||||
type: bool
|
||||
initial_value: 'false'
|
||||
- id: rtc_wake
|
||||
type: bool
|
||||
initial_value: 'false'
|
||||
# battery cycle running: forecast screen regardless of the connection state
|
||||
- id: weather_page
|
||||
type: bool
|
||||
initial_value: 'false'
|
||||
- id: fetch_done
|
||||
type: bool
|
||||
initial_value: 'false'
|
||||
- id: sleep_s
|
||||
type: int
|
||||
initial_value: '900'
|
||||
|
||||
m5paper:
|
||||
battery_power_pin: GPIO5
|
||||
main_power_pin: GPIO2
|
||||
# update_interval: 10s
|
||||
# battery_voltage:
|
||||
# name: "${name} Battery"
|
||||
wifi:
|
||||
enable_on_boot: false
|
||||
on_connect:
|
||||
- if:
|
||||
condition:
|
||||
lambda: return id(show_status) && !id(weather_page);
|
||||
then:
|
||||
- component.update: m5paper_display
|
||||
|
||||
api:
|
||||
# status screen until Home Assistant connects (log viewers don't subscribe to states)
|
||||
on_client_connected:
|
||||
- delay: 2s
|
||||
- if:
|
||||
condition:
|
||||
api.connected:
|
||||
state_subscription_only: true
|
||||
then:
|
||||
- lambda: id(show_status) = false;
|
||||
- if:
|
||||
condition:
|
||||
lambda: return !id(weather_page);
|
||||
then:
|
||||
- component.update: m5paper_display
|
||||
# outside the battery cycle: refresh a forecast older than an hour
|
||||
- if:
|
||||
condition:
|
||||
and:
|
||||
- lambda: |-
|
||||
const auto &fc = m5paper_weather::forecast();
|
||||
const auto now = id(rtc_time).now();
|
||||
return !id(weather_page) && (!fc.valid() || !now.is_valid() || now.timestamp - fc.fetched_at >= 3600);
|
||||
- api.connected:
|
||||
state_subscription_only: true
|
||||
then:
|
||||
- script.execute: fetch_forecast
|
||||
on_client_disconnected:
|
||||
- if:
|
||||
condition:
|
||||
not:
|
||||
api.connected:
|
||||
state_subscription_only: true
|
||||
then:
|
||||
- lambda: id(show_status) = true;
|
||||
- if:
|
||||
condition:
|
||||
lambda: return !id(weather_page);
|
||||
then:
|
||||
- component.update: m5paper_display
|
||||
|
||||
# I2C devices (i2c_main)
|
||||
# 0x44 SHT30
|
||||
# 0x50
|
||||
# 0x51
|
||||
# 0x5D GT911
|
||||
# 0x50 FM24C02 EEPROM (forecast storage, see include/m5paper_weather.h)
|
||||
# 0x51 BM8563 RTC
|
||||
# 0x5D GT911 touch (docs say 0x14; the driver tries 0x5D, then 0x14)
|
||||
|
||||
i2c:
|
||||
- id: i2c_main
|
||||
sda: 21
|
||||
scl: 22
|
||||
scan: true #false
|
||||
scan: true
|
||||
# Port A
|
||||
# - id: i2c_a
|
||||
# sda: 25
|
||||
# scl: 32
|
||||
# scan: true #false
|
||||
# Port B
|
||||
# - id: i2c_b
|
||||
# sda: 26
|
||||
# scl: 33
|
||||
# scan: true #false
|
||||
# Port C
|
||||
# - id: i2c_c
|
||||
# sda: 18
|
||||
# scl: 19
|
||||
# scan: true #false
|
||||
|
||||
spi:
|
||||
clk_pin: 14
|
||||
miso_pin: 13
|
||||
mosi_pin: 12
|
||||
|
||||
switch:
|
||||
# Power control MOS (no PMU on M5Paper); on battery the device powers off when this goes low
|
||||
- platform: gpio
|
||||
id: main_power
|
||||
pin:
|
||||
number: GPIO2
|
||||
ignore_strapping_warning: true
|
||||
restore_mode: ALWAYS_ON
|
||||
internal: true
|
||||
# External power (Grove ports)
|
||||
- platform: gpio
|
||||
id: ext_power
|
||||
pin:
|
||||
number: GPIO5
|
||||
ignore_strapping_warning: true
|
||||
restore_mode: ALWAYS_ON
|
||||
internal: true
|
||||
|
||||
script:
|
||||
# One wake-up on battery: refresh the forecast if it is old, draw it, power off
|
||||
- id: battery_cycle
|
||||
mode: single
|
||||
then:
|
||||
- lambda: id(weather_page) = true;
|
||||
- wait_until:
|
||||
condition:
|
||||
lambda: return id(rtc_time).now().is_valid();
|
||||
timeout: 10s
|
||||
- if:
|
||||
condition:
|
||||
lambda: |-
|
||||
const auto &fc = m5paper_weather::forecast();
|
||||
const auto now = id(rtc_time).now();
|
||||
return !fc.valid() || !now.is_valid() || now.timestamp - fc.fetched_at >= 6 * 3600 - 600;
|
||||
then:
|
||||
- wifi.enable:
|
||||
- wait_until:
|
||||
condition:
|
||||
api.connected:
|
||||
state_subscription_only: true
|
||||
timeout: 45s
|
||||
- if:
|
||||
condition:
|
||||
api.connected:
|
||||
state_subscription_only: true
|
||||
then:
|
||||
- script.execute: fetch_forecast
|
||||
- wait_until:
|
||||
condition:
|
||||
lambda: return id(fetch_done);
|
||||
timeout: 15s
|
||||
- wait_until:
|
||||
condition:
|
||||
lambda: return !std::isnan(id(current_temperature).state);
|
||||
timeout: 5s
|
||||
- component.update: m5paper_display
|
||||
# let the e-paper refresh finish before cutting power
|
||||
- delay: 4s
|
||||
- script.execute: try_sleep
|
||||
|
||||
- id: fetch_forecast
|
||||
mode: single
|
||||
then:
|
||||
- lambda: id(fetch_done) = false;
|
||||
- homeassistant.action:
|
||||
action: weather.get_forecasts
|
||||
data:
|
||||
entity_id: ${weather_entity}
|
||||
type: hourly
|
||||
capture_response: true
|
||||
on_success:
|
||||
- lambda: |-
|
||||
auto &fc = m5paper_weather::forecast();
|
||||
if (m5paper_weather::parse(response, "${weather_entity}", id(rtc_time).now().timestamp, fc)) {
|
||||
const bool saved = m5paper_weather::save(id(i2c_main), fc);
|
||||
ESP_LOGI("m5paper", "Forecast: %u h%s", fc.count, saved ? "" : " (EEPROM write failed)");
|
||||
} else {
|
||||
ESP_LOGW("m5paper", "Forecast: unexpected response");
|
||||
}
|
||||
id(fetch_done) = true;
|
||||
# the battery cycle draws on its own
|
||||
- if:
|
||||
condition:
|
||||
lambda: return !id(weather_page);
|
||||
then:
|
||||
- component.update: m5paper_display
|
||||
on_error:
|
||||
- lambda: |-
|
||||
ESP_LOGW("m5paper", "Forecast: %s", error.c_str());
|
||||
id(fetch_done) = true;
|
||||
|
||||
# Arm the RTC and cut power; if we are still running afterwards, we are on USB
|
||||
- id: try_sleep
|
||||
mode: single
|
||||
then:
|
||||
- lambda: |-
|
||||
// next quarter hour, 1:00-5:00 in one go
|
||||
int duration = 900;
|
||||
const auto now = id(rtc_time).now();
|
||||
if (now.is_valid()) {
|
||||
const int sod = now.hour * 3600 + now.minute * 60 + now.second;
|
||||
int next = (sod / 900 + 1) * 900;
|
||||
if (next - sod < 90) // the 1/60 Hz RTC timer may wake us up to a minute early
|
||||
next += 900;
|
||||
const int next_hour = (next / 3600) % 24;
|
||||
if (next_hour >= 1 && next_hour < 5)
|
||||
next = (next / 86400) * 86400 + 5 * 3600;
|
||||
duration = next - sod;
|
||||
}
|
||||
if (duration > 255) // longer timers count whole minutes
|
||||
duration = (duration + 59) / 60 * 60;
|
||||
id(sleep_s) = std::min(duration, 255 * 60);
|
||||
ESP_LOGI("m5paper", "Powering off for %d s", id(sleep_s));
|
||||
- bm8563.start_timer:
|
||||
duration: !lambda return id(sleep_s);
|
||||
# a battery wake-up is shorter than boot_is_good_after: confirm the OTA image
|
||||
# before cutting power, otherwise the bootloader rolls back
|
||||
- safe_mode.mark_successful:
|
||||
- delay: 200ms
|
||||
- switch.turn_off: main_power
|
||||
- delay: 2s
|
||||
# still alive -> USB power
|
||||
- switch.turn_on: main_power
|
||||
- lambda: |-
|
||||
ESP_LOGI("m5paper", "Still running, USB power");
|
||||
id(usb_mode) = true;
|
||||
id(weather_page) = false;
|
||||
id(show_status) = !api::global_api_server->is_connected_with_state_subscription();
|
||||
- wifi.enable:
|
||||
- component.update: m5paper_display
|
||||
|
||||
interval:
|
||||
# USB mode: the battery voltage drops once USB is unplugged -> go to battery mode
|
||||
- interval: 5min
|
||||
then:
|
||||
- if:
|
||||
condition:
|
||||
lambda: |-
|
||||
static float last = NAN;
|
||||
const float v = id(battery_voltage).state;
|
||||
const bool dropped = id(usb_mode) && !std::isnan(last) && last - v > 0.03f;
|
||||
last = v;
|
||||
return dropped;
|
||||
then:
|
||||
- lambda: |-
|
||||
ESP_LOGI("m5paper", "Battery voltage dropping, checking USB");
|
||||
id(usb_mode) = false;
|
||||
- script.execute: battery_cycle
|
||||
|
||||
time:
|
||||
- id: !extend ha_time
|
||||
on_time_sync:
|
||||
- bm8563.write_time
|
||||
- if:
|
||||
condition:
|
||||
lambda: return !id(weather_page);
|
||||
then:
|
||||
- component.update: m5paper_display
|
||||
on_time:
|
||||
- seconds: 0
|
||||
minutes: /15
|
||||
then:
|
||||
- if:
|
||||
condition:
|
||||
lambda: return !id(show_status) && !id(weather_page);
|
||||
then:
|
||||
- component.update: m5paper_display
|
||||
# USB mode: keep the stored forecast fresh for battery mode
|
||||
- seconds: 0
|
||||
minutes: 5
|
||||
then:
|
||||
- if:
|
||||
condition:
|
||||
and:
|
||||
- lambda: return id(usb_mode);
|
||||
- api.connected:
|
||||
state_subscription_only: true
|
||||
then:
|
||||
- script.execute: fetch_forecast
|
||||
- platform: bm8563
|
||||
id: rtc_time
|
||||
|
||||
binary_sensor:
|
||||
- platform: gpio
|
||||
name: ${name} Button UP
|
||||
id: button_up
|
||||
pin:
|
||||
number: 37
|
||||
number: GPIO37
|
||||
inverted: true
|
||||
on_press:
|
||||
- component.update: m5paper_display
|
||||
on_release:
|
||||
- component.update: m5paper_display
|
||||
- platform: gpio
|
||||
name: ${name} Button Press
|
||||
id: button_press
|
||||
pin:
|
||||
number: 38
|
||||
number: GPIO38
|
||||
inverted: true
|
||||
- platform: gpio
|
||||
name: ${name} Button Down
|
||||
id: button_down
|
||||
pin:
|
||||
number: 39
|
||||
number: GPIO39
|
||||
inverted: true
|
||||
- platform: touchscreen
|
||||
name: ${name} Top Left Touch Button
|
||||
@@ -103,51 +379,19 @@ binary_sensor:
|
||||
y_min: 0
|
||||
y_max: 100
|
||||
on_press:
|
||||
# Example of applying the sleep duration and shutting down to the lowest power mode
|
||||
- bm8563.apply_sleep_duration
|
||||
- delay: 1s
|
||||
- m5paper.shutdown_main_power
|
||||
- script.execute: battery_cycle
|
||||
|
||||
font:
|
||||
- file: "gfonts://Roboto"
|
||||
id: font_default
|
||||
size: 20
|
||||
- file: "gfonts://Roboto"
|
||||
id: roboto_32
|
||||
size: 32
|
||||
- file: "gfonts://Roboto"
|
||||
id: roboto_24
|
||||
size: 24
|
||||
- file: "gfonts://Roboto"
|
||||
id: roboto_12
|
||||
size: 12
|
||||
|
||||
display:
|
||||
- platform: it8951e
|
||||
id: m5paper_display
|
||||
cs_pin: GPIO15
|
||||
reset_pin: GPIO23
|
||||
busy_pin: GPIO27
|
||||
rotation: 90
|
||||
reversed: False
|
||||
update_interval: "never"
|
||||
lambda: |-
|
||||
it.printf(25, 25, id(roboto_32), "%.1f°", id(current_temperature).state);
|
||||
it.strftime(350, 25, id(roboto_32), "%H:%M:%S", id(rtc_time).now());
|
||||
|
||||
touchscreen:
|
||||
- platform: gt911
|
||||
display: m5paper_display
|
||||
id: gt911_touchscreen
|
||||
interrupt_pin: GPIO36
|
||||
|
||||
# display:
|
||||
# - platform: waveshare_epaper
|
||||
# cs_pin: 15
|
||||
# model: 2.90in
|
||||
# full_update_every: 30
|
||||
# lambda: |-
|
||||
# it.print(0, 0, id(font1), "Hello World!");
|
||||
button:
|
||||
- platform: template
|
||||
name: Refresh Display
|
||||
entity_category: config
|
||||
on_press:
|
||||
- component.update: m5paper_display
|
||||
- platform: template
|
||||
name: Refresh Forecast
|
||||
entity_category: config
|
||||
on_press:
|
||||
- script.execute: fetch_forecast
|
||||
|
||||
sensor:
|
||||
- platform: sht3xd
|
||||
@@ -156,9 +400,252 @@ sensor:
|
||||
id: current_temperature
|
||||
humidity:
|
||||
name: Humidity
|
||||
id: current_humidity
|
||||
address: 0x44
|
||||
update_interval: 60s
|
||||
- platform: adc
|
||||
id: battery_voltage
|
||||
name: Battery Voltage
|
||||
pin: GPIO35
|
||||
attenuation: 12db
|
||||
samples: 16
|
||||
update_interval: 60s
|
||||
entity_category: diagnostic
|
||||
filters:
|
||||
- multiply: 2.0 # 1:2 voltage divider
|
||||
- platform: copy
|
||||
source_id: battery_voltage
|
||||
id: battery_level
|
||||
name: Battery
|
||||
unit_of_measurement: "%"
|
||||
device_class: battery
|
||||
accuracy_decimals: 0
|
||||
filters:
|
||||
- calibrate_linear:
|
||||
- 3.3 -> 0.0
|
||||
- 4.15 -> 100.0
|
||||
- clamp:
|
||||
min_value: 0
|
||||
max_value: 100
|
||||
|
||||
# touchscreen:
|
||||
# - platform: gt911
|
||||
# interrupt_pin: 36
|
||||
.image_defaults:
|
||||
icon_large: &icon_large {platform: file, type: BINARY, transparency: chroma_key, resize: 150x150}
|
||||
icon_small: &icon_small {platform: file, type: BINARY, transparency: chroma_key, resize: 44x44}
|
||||
|
||||
image:
|
||||
- platform: file
|
||||
file: assets/images/made-for-esphome-white-on-black.svg
|
||||
id: esphome_logo
|
||||
resize: 460x136
|
||||
type: GRAYSCALE
|
||||
transparency: alpha_channel
|
||||
# weather icons, same order as m5paper_weather::CONDITIONS
|
||||
- {<<: *icon_large, file: "mdi:weather-night", id: wl_clear_night}
|
||||
- {<<: *icon_large, file: "mdi:weather-cloudy", id: wl_cloudy}
|
||||
- {<<: *icon_large, file: "mdi:alert-circle-outline", id: wl_exceptional}
|
||||
- {<<: *icon_large, file: "mdi:weather-fog", id: wl_fog}
|
||||
- {<<: *icon_large, file: "mdi:weather-hail", id: wl_hail}
|
||||
- {<<: *icon_large, file: "mdi:weather-lightning", id: wl_lightning}
|
||||
- {<<: *icon_large, file: "mdi:weather-lightning-rainy", id: wl_lightning_rainy}
|
||||
- {<<: *icon_large, file: "mdi:weather-partly-cloudy", id: wl_partlycloudy}
|
||||
- {<<: *icon_large, file: "mdi:weather-pouring", id: wl_pouring}
|
||||
- {<<: *icon_large, file: "mdi:weather-rainy", id: wl_rainy}
|
||||
- {<<: *icon_large, file: "mdi:weather-snowy", id: wl_snowy}
|
||||
- {<<: *icon_large, file: "mdi:weather-snowy-rainy", id: wl_snowy_rainy}
|
||||
- {<<: *icon_large, file: "mdi:weather-sunny", id: wl_sunny}
|
||||
- {<<: *icon_large, file: "mdi:weather-windy", id: wl_windy}
|
||||
- {<<: *icon_large, file: "mdi:weather-windy-variant", id: wl_windy_variant}
|
||||
- {<<: *icon_small, file: "mdi:weather-night", id: ws_clear_night}
|
||||
- {<<: *icon_small, file: "mdi:weather-cloudy", id: ws_cloudy}
|
||||
- {<<: *icon_small, file: "mdi:alert-circle-outline", id: ws_exceptional}
|
||||
- {<<: *icon_small, file: "mdi:weather-fog", id: ws_fog}
|
||||
- {<<: *icon_small, file: "mdi:weather-hail", id: ws_hail}
|
||||
- {<<: *icon_small, file: "mdi:weather-lightning", id: ws_lightning}
|
||||
- {<<: *icon_small, file: "mdi:weather-lightning-rainy", id: ws_lightning_rainy}
|
||||
- {<<: *icon_small, file: "mdi:weather-partly-cloudy", id: ws_partlycloudy}
|
||||
- {<<: *icon_small, file: "mdi:weather-pouring", id: ws_pouring}
|
||||
- {<<: *icon_small, file: "mdi:weather-rainy", id: ws_rainy}
|
||||
- {<<: *icon_small, file: "mdi:weather-snowy", id: ws_snowy}
|
||||
- {<<: *icon_small, file: "mdi:weather-snowy-rainy", id: ws_snowy_rainy}
|
||||
- {<<: *icon_small, file: "mdi:weather-sunny", id: ws_sunny}
|
||||
- {<<: *icon_small, file: "mdi:weather-windy", id: ws_windy}
|
||||
- {<<: *icon_small, file: "mdi:weather-windy-variant", id: ws_windy_variant}
|
||||
|
||||
font:
|
||||
- file: "gfonts://Roboto"
|
||||
id: font_title
|
||||
size: 48
|
||||
- file: "gfonts://Roboto"
|
||||
id: font_status
|
||||
size: 28
|
||||
glyphs: &sk_glyphs "ÁÄČĎÉÍĹĽŇÓÔŔŠŤÚÝŽáäčďéíĺľňóôŕšťúýž°·"
|
||||
glyphsets: [GF_Latin_Kernel]
|
||||
- file: "gfonts://Roboto"
|
||||
id: font_small
|
||||
size: 24
|
||||
glyphs: *sk_glyphs
|
||||
glyphsets: [GF_Latin_Kernel]
|
||||
- file: "gfonts://Roboto"
|
||||
id: font_tiny
|
||||
size: 18
|
||||
glyphs: "0123456789% "
|
||||
- file: "gfonts://Roboto@500"
|
||||
id: font_date
|
||||
size: 34
|
||||
glyphs: *sk_glyphs
|
||||
glyphsets: [GF_Latin_Kernel]
|
||||
- file: "gfonts://Roboto@500"
|
||||
id: font_hour
|
||||
size: 28
|
||||
glyphs: "0123456789-°"
|
||||
- file: "gfonts://Roboto"
|
||||
id: font_temp
|
||||
size: 110
|
||||
glyphs: "0123456789.,-°C "
|
||||
|
||||
color:
|
||||
- id: ink
|
||||
red: 0%
|
||||
green: 0%
|
||||
blue: 0%
|
||||
- id: paper
|
||||
red: 100%
|
||||
green: 100%
|
||||
blue: 100%
|
||||
|
||||
display:
|
||||
- platform: it8951
|
||||
id: m5paper_display
|
||||
model: m5stack-m5paper
|
||||
rotation: 270
|
||||
update_interval: never
|
||||
# 1bpp: updates use the fast DU waveform, full GC16 refresh every 30 updates
|
||||
grayscale: false
|
||||
dithering: false
|
||||
lambda: |-
|
||||
// 540x960 portrait
|
||||
it.fill(id(paper));
|
||||
const int w = it.get_width();
|
||||
const int cx = w / 2;
|
||||
const float batt = id(battery_level).state;
|
||||
const float temp = id(current_temperature).state;
|
||||
const float hum = id(current_humidity).state;
|
||||
|
||||
// status screen after power-on, until Home Assistant connects
|
||||
if (id(show_status) && !id(weather_page)) {
|
||||
it.image(cx, 200, id(esphome_logo), ImageAlign::CENTER);
|
||||
it.print(cx, 320, id(font_title), id(ink), TextAlign::CENTER, "${name}");
|
||||
|
||||
const bool wifi_ok = wifi::global_wifi_component->is_connected();
|
||||
if (wifi_ok) {
|
||||
it.printf(40, 440, id(font_status), id(ink), "WiFi: connected (%d dBm)", wifi::global_wifi_component->wifi_rssi());
|
||||
// IPv6 first (skip link-local), then IPv4
|
||||
int y = 484;
|
||||
char buf[network::IP_ADDRESS_BUFFER_SIZE];
|
||||
for (bool v6 : {true, false}) {
|
||||
for (auto &ip : wifi::global_wifi_component->get_ip_addresses()) {
|
||||
if (!ip.is_set() || ip.is_ip6() != v6 || y > 590)
|
||||
continue;
|
||||
ip.str_to(buf);
|
||||
if (v6 && strncmp(buf, "fe80", 4) == 0)
|
||||
continue;
|
||||
it.print(60, y, id(font_small), id(ink), buf);
|
||||
y += 34;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
it.print(40, 440, id(font_status), id(ink), "WiFi: connecting...");
|
||||
}
|
||||
const bool api_ok = api::global_api_server->is_connected_with_state_subscription();
|
||||
it.print(40, 620, id(font_status), id(ink),
|
||||
api_ok ? "Home Assistant: connected" : "Home Assistant: waiting...");
|
||||
if (!std::isnan(batt))
|
||||
it.printf(40, 670, id(font_status), id(ink), "Battery: %.0f%% (%.2f V)", batt, id(battery_voltage).state);
|
||||
it.printf(40, 720, id(font_status), id(ink), "Forecast: %u h stored", m5paper_weather::forecast().count);
|
||||
it.printf(40, 900, id(font_small), id(ink), "ESPHome %s", ESPHOME_VERSION);
|
||||
return;
|
||||
}
|
||||
|
||||
// forecast screen
|
||||
using namespace m5paper_weather;
|
||||
image::Image *large[] = {id(wl_clear_night), id(wl_cloudy), id(wl_exceptional), id(wl_fog),
|
||||
id(wl_hail), id(wl_lightning), id(wl_lightning_rainy), id(wl_partlycloudy),
|
||||
id(wl_pouring), id(wl_rainy), id(wl_snowy), id(wl_snowy_rainy),
|
||||
id(wl_sunny), id(wl_windy), id(wl_windy_variant)};
|
||||
image::Image *small[] = {id(ws_clear_night), id(ws_cloudy), id(ws_exceptional), id(ws_fog),
|
||||
id(ws_hail), id(ws_lightning), id(ws_lightning_rainy), id(ws_partlycloudy),
|
||||
id(ws_pouring), id(ws_rainy), id(ws_snowy), id(ws_snowy_rainy),
|
||||
id(ws_sunny), id(ws_windy), id(ws_windy_variant)};
|
||||
static const char *const DAYS[] = {"Nedeľa", "Pondelok", "Utorok", "Streda", "Štvrtok", "Piatok", "Sobota"};
|
||||
|
||||
const auto &fc = m5paper_weather::forecast();
|
||||
const auto now = id(rtc_time).now();
|
||||
|
||||
// header: date, battery
|
||||
if (now.is_valid())
|
||||
it.printf(24, 16, id(font_date), id(ink), "%s %d. %d.", DAYS[(now.day_of_week + 6) % 7], now.day_of_month,
|
||||
now.month);
|
||||
if (!std::isnan(batt))
|
||||
it.printf(w - 24, 22, id(font_small), id(ink), TextAlign::TOP_RIGHT, "%.0f %%", batt);
|
||||
|
||||
const Hour *cur = now.is_valid() ? fc.at(now.timestamp) : nullptr;
|
||||
if (cur == nullptr) {
|
||||
it.print(cx, 230, id(font_status), id(ink), TextAlign::CENTER, "Predpoveď nie je k dispozícii");
|
||||
} else {
|
||||
// current hour
|
||||
if (cur->cond < NUM_CONDITIONS)
|
||||
it.image(24, 80, large[cur->cond], id(ink));
|
||||
if (cur->temp2 != NO_TEMP)
|
||||
it.printf(200, 70, id(font_temp), id(ink), "%.0f°", cur->temp2 / 2.0f);
|
||||
it.print(204, 200, id(font_status), id(ink), condition_name(cur->cond));
|
||||
if (cur->wind != NO_VALUE)
|
||||
it.printf(204, 245, id(font_small), id(ink), "vietor %u km/h", cur->wind);
|
||||
if (cur->pop != NO_VALUE)
|
||||
it.printf(204, 280, id(font_small), id(ink), "zrážky %u %%", cur->pop);
|
||||
|
||||
// next 24 hours: 3 columns x 8 rows
|
||||
const uint32_t first = (now.timestamp / 3600 + 1) * 3600;
|
||||
const int top = 440, row_h = 60, col_w = w / 3;
|
||||
for (int i = 0; i < 24; i++) {
|
||||
const Hour *h = fc.at(first + i * 3600);
|
||||
if (h == nullptr)
|
||||
break;
|
||||
const int x = (i / 8) * col_w + 12;
|
||||
const int y = top + (i % 8) * row_h;
|
||||
const auto t = ESPTime::from_epoch_local(first + i * 3600);
|
||||
it.printf(x, y + 14, id(font_hour), id(ink), "%02d", t.hour);
|
||||
if (h->cond < NUM_CONDITIONS)
|
||||
it.image(x + 42, y + 8, small[h->cond], id(ink));
|
||||
if (h->temp2 != NO_TEMP)
|
||||
it.printf(x + 94, y + 6, id(font_hour), id(ink), "%.0f°", h->temp2 / 2.0f);
|
||||
if (h->pop != NO_VALUE && h->pop >= 10)
|
||||
it.printf(x + 94, y + 38, id(font_tiny), id(ink), "%u %%", h->pop);
|
||||
}
|
||||
for (int c = 1; c < 3; c++)
|
||||
it.vertical_line(c * col_w, top, 8 * row_h, id(ink));
|
||||
}
|
||||
|
||||
// indoor
|
||||
it.horizontal_line(24, 340, w - 48, id(ink));
|
||||
it.printf(cx, 380, id(font_status), id(ink), TextAlign::CENTER, "Doma %.1f °C · %.0f %%", temp, hum);
|
||||
it.horizontal_line(24, 420, w - 48, id(ink));
|
||||
|
||||
// footer
|
||||
if (fc.valid()) {
|
||||
const auto f = ESPTime::from_epoch_local(fc.fetched_at);
|
||||
it.printf(24, 944, id(font_small), id(ink), TextAlign::BOTTOM_LEFT, "predpoveď %d. %d. %02d:%02d",
|
||||
f.day_of_month, f.month, f.hour, f.minute);
|
||||
}
|
||||
if (now.is_valid())
|
||||
it.printf(w - 24, 944, id(font_small), id(ink), TextAlign::BOTTOM_RIGHT, "%02d:%02d", now.hour, now.minute);
|
||||
|
||||
touchscreen:
|
||||
- platform: gt911
|
||||
id: gt911_touchscreen
|
||||
display: m5paper_display
|
||||
# GPIO36 is input-only; INT is only driven as output when reset_pin is set
|
||||
interrupt_pin:
|
||||
number: GPIO36
|
||||
mode:
|
||||
input: true
|
||||
output: false
|
||||
@@ -228,9 +228,7 @@ button:
|
||||
- pzemac.reset_energy: pzemac_l3
|
||||
|
||||
time:
|
||||
- platform: homeassistant
|
||||
id: ha_time
|
||||
timezone: Europe/Bratislava
|
||||
- id: !extend ha_time
|
||||
on_time:
|
||||
- cron: '0 0 0 * * *'
|
||||
then:
|
||||
|
||||
@@ -136,9 +136,7 @@ nspanel_lovelace:
|
||||
thermo_entity_id: climate.bt_trv_bedroom
|
||||
|
||||
time:
|
||||
- platform: homeassistant
|
||||
id: ha_time
|
||||
timezone: Europe/Bratislava
|
||||
- id: !extend ha_time
|
||||
on_time_sync:
|
||||
- script.execute: apply_sleep_dim
|
||||
|
||||
|
||||
@@ -540,11 +540,6 @@ sensor:
|
||||
- platform: template
|
||||
id: room_temperature_id
|
||||
|
||||
time:
|
||||
platform: homeassistant
|
||||
id: ha_time
|
||||
timezone: Europe/Bratislava
|
||||
|
||||
text_sensor:
|
||||
- platform: homeassistant
|
||||
id: name_day
|
||||
|
||||
@@ -185,11 +185,6 @@ nspanel_lovelace:
|
||||
- name: Auto-zatváranie
|
||||
entity_id: switch.gate_auto_close
|
||||
|
||||
time:
|
||||
- platform: homeassistant
|
||||
id: ha_time
|
||||
timezone: Europe/Bratislava
|
||||
|
||||
binary_sensor:
|
||||
- platform: gpio
|
||||
name: Left Button
|
||||
|
||||
@@ -38,6 +38,16 @@ external_components:
|
||||
packages:
|
||||
common: !include common/common.yaml
|
||||
|
||||
script:
|
||||
- id: apply_sleep_dim
|
||||
then:
|
||||
- lambda: |-
|
||||
uint8_t dim;
|
||||
if (id(bedroom_lights).state) dim = 5; // sleepOverride
|
||||
else if (id(sun_id).is_above_horizon(-6)) dim = 5; // day
|
||||
else dim = 0; // night
|
||||
id(nspanel_id).set_display_inactive_dim(dim);
|
||||
|
||||
api:
|
||||
homeassistant_services: true
|
||||
homeassistant_states: true
|
||||
@@ -135,9 +145,9 @@ nspanel_lovelace:
|
||||
entity_id: switch.gate_auto_close
|
||||
|
||||
time:
|
||||
- platform: homeassistant
|
||||
id: ha_time
|
||||
timezone: Europe/Bratislava
|
||||
- id: !extend ha_time
|
||||
on_time_sync:
|
||||
- script.execute: apply_sleep_dim
|
||||
|
||||
binary_sensor:
|
||||
- platform: gpio
|
||||
@@ -161,6 +171,12 @@ binary_sensor:
|
||||
entity_id: light.bedside_left
|
||||
- lambda: 'id(nspanel_id).render_default_page();'
|
||||
|
||||
- platform: homeassistant # sleepOverride
|
||||
id: bedroom_lights
|
||||
entity_id: light.office_j_lights
|
||||
on_state:
|
||||
- script.execute: apply_sleep_dim
|
||||
|
||||
output:
|
||||
- platform: ledc
|
||||
id: buzzer_out
|
||||
@@ -186,6 +202,20 @@ light:
|
||||
# # name: Relay 2
|
||||
# # output: relay_2
|
||||
|
||||
sun:
|
||||
id: sun_id
|
||||
time_id: ha_time
|
||||
latitude: 48.15°
|
||||
longitude: 17.11°
|
||||
on_sunrise:
|
||||
- elevation: -6°
|
||||
then:
|
||||
- script.execute: apply_sleep_dim
|
||||
on_sunset:
|
||||
- elevation: -6°
|
||||
then:
|
||||
- script.execute: apply_sleep_dim
|
||||
|
||||
switch:
|
||||
- platform: gpio
|
||||
name: Screen Power
|
||||
|
||||
@@ -158,11 +158,6 @@ nspanel_lovelace:
|
||||
- name: Auto-zatváranie
|
||||
entity_id: switch.gate_auto_close
|
||||
|
||||
time:
|
||||
- platform: homeassistant
|
||||
id: ha_time
|
||||
timezone: Europe/Bratislava
|
||||
|
||||
binary_sensor:
|
||||
- platform: gpio
|
||||
name: Left Button
|
||||
|
||||
@@ -214,11 +214,6 @@ nspanel_lovelace:
|
||||
- name: Rast trávy
|
||||
entity_id: automation.irrigation_grow_grass
|
||||
|
||||
time:
|
||||
- platform: homeassistant
|
||||
id: ha_time
|
||||
timezone: Europe/Bratislava
|
||||
|
||||
binary_sensor:
|
||||
- platform: gpio
|
||||
name: Left Button
|
||||
|
||||
@@ -54,11 +54,6 @@ packages:
|
||||
# Relay1 G22
|
||||
# Relay2 G19
|
||||
|
||||
time:
|
||||
- platform: homeassistant
|
||||
id: ha_time
|
||||
timezone: Europe/Bratislava
|
||||
|
||||
i2c:
|
||||
# - id: i2c0
|
||||
# sda: 26
|
||||
|
||||
@@ -187,5 +187,3 @@ switch:
|
||||
- switch.turn_off: relay_off # bi-stable relay so no need to keep on
|
||||
interlock: [relay_on]
|
||||
|
||||
time:
|
||||
- !include common/time/homeassistant.yaml
|
||||
+1
-4
@@ -2,7 +2,7 @@
|
||||
substitutions:
|
||||
device: pow-washer
|
||||
name: Washer POW
|
||||
comment:
|
||||
comment: "spare"
|
||||
area: Washroom
|
||||
update_interval: 10s
|
||||
|
||||
@@ -186,6 +186,3 @@ switch:
|
||||
- delay: 500ms
|
||||
- switch.turn_off: relay_off # bi-stable relay so no need to keep on
|
||||
interlock: [relay_on]
|
||||
|
||||
time:
|
||||
- !include common/time/homeassistant.yaml
|
||||
@@ -109,5 +109,3 @@ switch:
|
||||
number: 19
|
||||
mode: OUTPUT
|
||||
|
||||
time:
|
||||
- !include common/time/homeassistant.yaml
|
||||
+1
-2
@@ -121,8 +121,7 @@ switch:
|
||||
# number: GPIO00
|
||||
# inverted: True
|
||||
|
||||
time:
|
||||
- !include common/time/homeassistant.yaml
|
||||
# time:
|
||||
# - platform: homeassistant
|
||||
# timezone: Europe/Bratislava
|
||||
# on_time:
|
||||
|
||||
+1
-2
@@ -122,8 +122,7 @@ switch:
|
||||
on_turn_off:
|
||||
- light.turn_off: light_red
|
||||
|
||||
time:
|
||||
- !include common/time/homeassistant.yaml
|
||||
# time:
|
||||
# - platform: homeassistant
|
||||
# timezone: Europe/Bratislava
|
||||
# on_time:
|
||||
|
||||
+1
-2
@@ -144,8 +144,7 @@ switch:
|
||||
# number: GPIO00
|
||||
# inverted: True
|
||||
|
||||
time:
|
||||
- !include common/time/homeassistant.yaml
|
||||
# time:
|
||||
# - platform: homeassistant
|
||||
# timezone: Europe/Bratislava
|
||||
# on_time:
|
||||
|
||||
+22
-9
@@ -464,9 +464,9 @@ spi:
|
||||
|
||||
image:
|
||||
- platform: file
|
||||
file: assets/images/esphome-logo-text-on-dark.svg
|
||||
file: assets/images/made-for-esphome-white-on-black.svg
|
||||
id: esphome_logo
|
||||
resize: 280x56
|
||||
resize: 230x68
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
- platform: file
|
||||
@@ -545,22 +545,36 @@ display:
|
||||
if (id(show_status)) {
|
||||
first_draw = true;
|
||||
it.fill(id(face_bg));
|
||||
it.image(160, 45, id(esphome_logo), ImageAlign::CENTER);
|
||||
it.image(160, 46, id(esphome_logo), ImageAlign::CENTER);
|
||||
it.print(160, 95, id(font_title), id(face_fg), TextAlign::CENTER, "${name}");
|
||||
|
||||
if (wifi_ok) {
|
||||
it.printf(20, 135, id(font_status), id(status_ok), "WiFi: %s (%d dBm)",
|
||||
id(ip_address_sensor).state.c_str(), rssi);
|
||||
it.printf(20, 114, id(font_status), id(status_ok), "WiFi: connected (%d dBm)", rssi);
|
||||
// one global IPv6 (skip link-local) and one IPv4 address
|
||||
int y = 133;
|
||||
char buf[network::IP_ADDRESS_BUFFER_SIZE];
|
||||
for (bool v6 : {true, false}) {
|
||||
for (auto &ip : wifi::global_wifi_component->get_ip_addresses()) {
|
||||
if (!ip.is_set() || ip.is_ip6() != v6)
|
||||
continue;
|
||||
ip.str_to(buf);
|
||||
if (v6 && strncmp(buf, "fe80", 4) == 0)
|
||||
continue;
|
||||
it.print(30, y, id(font_status), id(status_dim), buf);
|
||||
y += 19;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
it.print(20, 135, id(font_status), id(status_wait), "WiFi: connecting...");
|
||||
it.print(20, 114, id(font_status), id(status_wait), "WiFi: connecting...");
|
||||
}
|
||||
|
||||
const bool api_ok = api::global_api_server->is_connected();
|
||||
it.print(20, 162, id(font_status), api_ok ? id(status_ok) : id(status_wait),
|
||||
it.print(20, 172, id(font_status), api_ok ? id(status_ok) : id(status_wait),
|
||||
api_ok ? "Home Assistant: connected" : "Home Assistant: waiting...");
|
||||
|
||||
if (batt >= 0) {
|
||||
it.printf(20, 189, id(font_status), batt <= 20 && !charging ? id(status_low) : id(status_dim),
|
||||
it.printf(20, 191, id(font_status), batt <= 20 && !charging ? id(status_low) : id(status_dim),
|
||||
"Battery: %d%%%s", batt, charging ? " (charging)" : "");
|
||||
}
|
||||
it.printf(20, 212, id(font_status), id(status_dim), "ESPHome %s", ESPHOME_VERSION);
|
||||
@@ -883,4 +897,3 @@ time:
|
||||
id: rtc_time
|
||||
i2c_id: i2c_bsp
|
||||
update_interval: never
|
||||
- !include common/time/homeassistant.yaml
|
||||
@@ -358,5 +358,3 @@ valve:
|
||||
close_action:
|
||||
- switch.turn_off: irrigation_relay_8
|
||||
|
||||
time:
|
||||
- !include common/time/homeassistant.yaml
|
||||
Reference in new issue
Block a user