├── sample.png ├── fonts ├── OpenSans-Bold.ttf ├── OpenSans-Medium.ttf └── materialdesignicons-webfont.ttf ├── .gitignore ├── .vscode └── settings.json ├── README.md ├── common.h └── dashboard.yaml /sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tolnai/esphome-dashboard-lilygo-t5/HEAD/sample.png -------------------------------------------------------------------------------- /fonts/OpenSans-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tolnai/esphome-dashboard-lilygo-t5/HEAD/fonts/OpenSans-Bold.ttf -------------------------------------------------------------------------------- /fonts/OpenSans-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tolnai/esphome-dashboard-lilygo-t5/HEAD/fonts/OpenSans-Medium.ttf -------------------------------------------------------------------------------- /fonts/materialdesignicons-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tolnai/esphome-dashboard-lilygo-t5/HEAD/fonts/materialdesignicons-webfont.ttf -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Gitignore settings for ESPHome 2 | # This is an example and may include too much for your use-case. 3 | # You can modify this file to suit your needs. 4 | /.esphome/ 5 | /secrets.yaml 6 | venv 7 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | "[A-Z]*.js": "javascriptreact", 4 | "algorithm": "cpp", 5 | "array": "cpp", 6 | "bitset": "cpp", 7 | "chrono": "cpp", 8 | "cinttypes": "cpp", 9 | "cmath": "cpp", 10 | "cstdarg": "cpp", 11 | "cstddef": "cpp", 12 | "cstdint": "cpp", 13 | "cstdio": "cpp", 14 | "cstdlib": "cpp", 15 | "cstring": "cpp", 16 | "ctime": "cpp", 17 | "cwchar": "cpp", 18 | "deque": "cpp", 19 | "exception": "cpp", 20 | "functional": "cpp", 21 | "initializer_list": "cpp", 22 | "iomanip": "cpp", 23 | "ios": "cpp", 24 | "iosfwd": "cpp", 25 | "iostream": "cpp", 26 | "istream": "cpp", 27 | "iterator": "cpp", 28 | "limits": "cpp", 29 | "list": "cpp", 30 | "locale": "cpp", 31 | "map": "cpp", 32 | "memory": "cpp", 33 | "mutex": "cpp", 34 | "new": "cpp", 35 | "ostream": "cpp", 36 | "queue": "cpp", 37 | "ratio": "cpp", 38 | "regex": "cpp", 39 | "set": "cpp", 40 | "sstream": "cpp", 41 | "stdexcept": "cpp", 42 | "streambuf": "cpp", 43 | "string": "cpp", 44 | "system_error": "cpp", 45 | "thread": "cpp", 46 | "tuple": "cpp", 47 | "type_traits": "cpp", 48 | "typeinfo": "cpp", 49 | "unordered_map": "cpp", 50 | "utility": "cpp", 51 | "vector": "cpp", 52 | "xfacet": "cpp", 53 | "xhash": "cpp", 54 | "xiosbase": "cpp", 55 | "xlocale": "cpp", 56 | "xlocbuf": "cpp", 57 | "xlocinfo": "cpp", 58 | "xlocmes": "cpp", 59 | "xlocmon": "cpp", 60 | "xlocnum": "cpp", 61 | "xloctime": "cpp", 62 | "xmemory": "cpp", 63 | "xmemory0": "cpp", 64 | "xstddef": "cpp", 65 | "xstring": "cpp", 66 | "xtr1common": "cpp", 67 | "xtree": "cpp", 68 | "xutility": "cpp" 69 | } 70 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ESPHome based dashboard using Lilygo T5 4,7" e-ink display + ESP32 2 | 3 | ⚠️ _Note:_ The "időkép" scraper module has been moved to a separate repo: 4 | 5 | ⚠️ _Megjegyzés:_ Az "időkép" scraper modul külön repóba költözött: 6 | 7 | ![Sample](/sample.png) 8 | 9 | This is my hobby project to create an information-dense kitchen countertop dashboard for my Home Assistant based smart home. 10 | 11 | My work was inspired by geekuillaume and Plawasan. 12 | 13 | Disclaimer: this is really not a cleaned up version yet. 🙂 14 | 15 | ## Device 16 | 17 | I used a Lilygo T5 4,7" e-ink display + ESP32 ordered from Aliexpress, PH 2.0 9102 Chip version. The device came in a nice package with a USB cable included. Once installing the proper driver it was ready to use. 18 | 19 | ## Getting started 20 | 21 | Steps I needed to get this project up and running on Windows: 22 | 23 | - install latest python 24 | - install esp-idf from [https://docs.espressif.com/projects/esp-idf/en/latest/esp32/get-started/windows-setup.html] 25 | - (this was needed for Windows to recognize my Lilygo T5 properly) 26 | - install ESPHome addon and integration to Home Assistant 27 | - follow the [ESPHome CLI guide](https://esphome.io/guides/getting_started_command_line.html) 28 | - add passwords to `secrets.yaml` (not pushed to this repo) 29 | 30 | ```bash 31 | pip3 install esphome 32 | esphome wizard dashboard.yaml 33 | esphome run dashboard.yaml 34 | ``` 35 | 36 | ## Custom solutions behind 37 | 38 | My dashboard code may not be reusable directly for anyone else, but it might give some ideas, or perhaps a motivation that it is not that difficult to just create something like this from scratch. The layout is fully custom and tailored for my own needs. I tried to produce just a little bit better code than a 5 year old would do, but I'm not that proud of it (could use a lot of refactoring). Oh boy, I haven't used C++ in ages... 🙂 39 | 40 | Date related things and some other stuff are localized in a perhaps ugly way, since strftime can't do that. 41 | 42 | Data sources used behind: 43 | 44 | - HA date and time 45 | - `sun` and `moon` HA sensors 46 | - google calendar to get today's "nameday" 47 | - spotify 48 | - temperature+humidity sensors 49 | - custom weather integration (scraping a local provider in HA, that's why everything seems to be a separate sensor) 50 | - mobile apps for tracking zones 51 | 52 | ## Other resources 53 | 54 | - [https://github.com/tiaanv/esphome-components] 55 | - [https://github.com/Xinyuan-LilyGO/LilyGo-EPD47] 56 | - [https://github.com/vroland/epdiy] 57 | - [https://esphome.io/components/display/index.html] 58 | - [https://www.reddit.com/r/homeassistant/comments/rm71z4/lilygo_t5_epaper_display_now_using_esphome/] 59 | - [https://www.reddit.com/r/homeassistant/comments/rwwy6r/i_built_a_personal_dashboard_with_a_47_epaper/] 60 | - [https://pictogrammers.github.io/@mdi/font/5.3.45/] 61 | 62 | ## License 63 | 64 | Feel free to use any code from this repo, in any way you want. (WTFPL) 65 | 66 | Buy Me A Coffee 67 | -------------------------------------------------------------------------------- /common.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | std::string generateDateFormat(esphome::ESPTime time, std::string nameday) { 5 | std::string months[12] = {"Jan", "Feb", "Már", "Ápr", "Máj", "Jún", "Júl", "Aug", "Sze", "Okt", "Nov", "Dec"}; 6 | std::string weekdays[7] = {"Vasárnap", "Hétfő", "Kedd", "Szerda", "Csütörtök", "Péntek", "Szombat"}; 7 | std::string dateFormat = months[atoi(time.strftime("%m").c_str()) - 1] + std::string(" %d, ") + weekdays[atoi(time.strftime("%w").c_str())] + " | " + nameday; 8 | return dateFormat; 9 | } 10 | 11 | #define ICON_stop "\U000F04DB" 12 | #define ICON_play "\U000F040A" 13 | #define ICON_pause "\U000F03E4" 14 | 15 | std::string playbackStatusToIcon(bool playing, bool paused) { 16 | if (playing) return ICON_play; 17 | else if (paused) return ICON_pause; 18 | else return ICON_stop; 19 | } 20 | 21 | #define ICON_w_clear_night "\U000F0594" 22 | #define ICON_w_cloudy "\U000F0590" 23 | #define ICON_w_fog "\U000F0591" 24 | #define ICON_w_hail "\U000F0592" 25 | #define ICON_w_lightning "\U000F0593" 26 | #define ICON_w_lightning_rainy "\U000F067E" 27 | #define ICON_w_night_partly_cloudy "\U000F0F31" 28 | #define ICON_w_partly_cloudy "\U000F0595" 29 | #define ICON_w_pouring "\U000F0596" 30 | #define ICON_w_rainy "\U000F0597" 31 | #define ICON_w_snowy "\U000F0F36" 32 | #define ICON_w_snowy_rainy "\U000F067F" 33 | #define ICON_w_sunny "\U000F0599" 34 | #define ICON_w_windy "\U000F059D" 35 | #define ICON_w_windy_variant "\U000F059E" 36 | #define ICON_w_exceptional "\U000F0F38" 37 | 38 | std::string conditionToIcon(std::string condition, bool daytime) 39 | { 40 | if (condition == "clear-night") return ICON_w_clear_night; 41 | if (condition == "cloudy") return ICON_w_cloudy; 42 | if (condition == "fog") return ICON_w_fog; 43 | if (condition == "hail") return ICON_w_hail; 44 | if (condition == "lightning") return ICON_w_lightning; 45 | if (condition == "lightning-rainy") return ICON_w_lightning_rainy; 46 | if (condition == "partlycloudy" && !daytime) return ICON_w_night_partly_cloudy; 47 | if (condition == "partlycloudy" && daytime) return ICON_w_partly_cloudy; 48 | if (condition == "pouring") return ICON_w_pouring; 49 | if (condition == "rainy") return ICON_w_rainy; 50 | if (condition == "snowy") return ICON_w_snowy; 51 | if (condition == "snowy-rainy") return ICON_w_snowy_rainy; 52 | if (condition == "sunny") return ICON_w_sunny; 53 | if (condition == "windy") return ICON_w_windy; 54 | if (condition == "windy-variant") return ICON_w_windy_variant; 55 | if (condition == "exceptional") return ICON_w_exceptional; 56 | return ""; 57 | } 58 | 59 | #define ICON_moon_first_quarter "\U000F0F61" 60 | #define ICON_moon_full "\U000F0F62" 61 | #define ICON_moon_last_quarter "\U000F0F63" 62 | #define ICON_moon_new "\U000F0F64" 63 | #define ICON_moon_waning_crescent "\U000F0F65" 64 | #define ICON_moon_waning_gibbous "\U000F0F66" 65 | #define ICON_moon_waxing_crescent "\U000F0F67" 66 | #define ICON_moon_waxing_gibbous "\U000F0F68" 67 | 68 | std::string moonToIcon(std::string moonPhase) 69 | { 70 | if (moonPhase == "new_moon") return ICON_moon_new; 71 | if (moonPhase == "waxing_crescent") return ICON_moon_waxing_crescent; 72 | if (moonPhase == "first_quarter") return ICON_moon_first_quarter; 73 | if (moonPhase == "waxing_gibbous") return ICON_moon_waxing_gibbous; 74 | if (moonPhase == "full_moon") return ICON_moon_full; 75 | if (moonPhase == "waning_gibbous") return ICON_moon_waning_gibbous; 76 | if (moonPhase == "last_quarter") return ICON_moon_last_quarter; 77 | if (moonPhase == "waning_crescent") return ICON_moon_waning_crescent; 78 | return ""; 79 | } 80 | 81 | std::string locationToHungarian(std::string location, std::string distance) 82 | { 83 | if (location == "home") return "Otthon"; 84 | if (location == "not_home") return "-> " + distance; 85 | return "-> " + location; 86 | } 87 | -------------------------------------------------------------------------------- /dashboard.yaml: -------------------------------------------------------------------------------- 1 | substitutions: 2 | esp_name: ESP Dashboard 3 | esp_hostname: esp-dashboard 4 | ip_address: 192.168.0.234 5 | ip_gw: 192.168.0.1 6 | ip_netmask: 255.255.255.0 7 | 8 | esphome: 9 | name: dashboard 10 | includes: 11 | - common.h 12 | 13 | esp32: 14 | board: esp32dev 15 | framework: 16 | type: arduino 17 | 18 | logger: 19 | 20 | api: 21 | password: '' 22 | 23 | ota: 24 | platform: esphome 25 | password: '' 26 | 27 | wifi: 28 | ssid: 'Cruiser' 29 | password: !secret wifi_password 30 | manual_ip: 31 | static_ip: ${ip_address} 32 | gateway: ${ip_gw} 33 | subnet: ${ip_netmask} 34 | ap: 35 | ssid: '${esp_name} Fallback Hotspot' 36 | password: !secret wifi_password 37 | 38 | web_server: 39 | port: 80 40 | include_internal: true 41 | 42 | sun: 43 | latitude: 47.498406° 44 | longitude: 19.040758° 45 | 46 | time: 47 | - platform: homeassistant 48 | id: ntp 49 | timezone: Europe/Budapest 50 | on_time_sync: 51 | - then: 52 | - component.update: sunrise 53 | - component.update: sunset 54 | 55 | external_components: 56 | # - source: github://ashald/esphome@lilygo-t5-47 57 | # components: 58 | # - lilygo_t5_47 59 | # - source: 60 | # type: git 61 | # url: https://github.com/vbaksa/esphome 62 | # ref: dev 63 | # components: 64 | # - lilygo_t5_47_battery 65 | # - lilygo_t5_47_display 66 | - source: github://tiaanv/esphome-components 67 | components: 68 | - t547 69 | #- source: github://nickolay/esphome-lilygo-t547plus 70 | # components: ['t547'] 71 | # - source: 72 | # type: git 73 | # url: https://github.com/kaweksl/esphome 74 | # ref: dev 75 | # components: 76 | # - lilygo_t5_47_battery 77 | # - lilygo_t5_47_display 78 | 79 | script: 80 | - id: all_data_received 81 | then: 82 | - component.update: t5_display 83 | 84 | sensor: 85 | # weather 86 | - platform: homeassistant 87 | entity_id: sensor.idokep_temperature 88 | id: temperature 89 | internal: true 90 | - platform: homeassistant 91 | entity_id: sensor.idokep_daily_data 92 | attribute: day1_min 93 | id: day1_min 94 | internal: true 95 | - platform: homeassistant 96 | entity_id: sensor.idokep_daily_data 97 | attribute: day1_max 98 | id: day1_max 99 | internal: true 100 | - platform: homeassistant 101 | entity_id: sensor.idokep_daily_data 102 | attribute: day1_prec_mm 103 | id: day1_prec 104 | internal: true 105 | - platform: homeassistant 106 | entity_id: sensor.idokep_daily_data 107 | attribute: day2_min 108 | id: day2_min 109 | internal: true 110 | - platform: homeassistant 111 | entity_id: sensor.idokep_daily_data 112 | attribute: day2_max 113 | id: day2_max 114 | internal: true 115 | - platform: homeassistant 116 | entity_id: sensor.idokep_daily_data 117 | attribute: day2_prec_mm 118 | id: day2_prec 119 | internal: true 120 | - platform: homeassistant 121 | entity_id: sensor.idokep_daily_data 122 | attribute: day3_min 123 | id: day3_min 124 | internal: true 125 | - platform: homeassistant 126 | entity_id: sensor.idokep_daily_data 127 | attribute: day3_max 128 | id: day3_max 129 | internal: true 130 | - platform: homeassistant 131 | entity_id: sensor.idokep_daily_data 132 | attribute: day3_prec_mm 133 | id: day3_prec 134 | internal: true 135 | # temperature/humidity sensors 136 | - platform: homeassistant 137 | entity_id: sensor.halo_hmer_temperature 138 | id: temp_halo 139 | internal: true 140 | - platform: homeassistant 141 | entity_id: sensor.halo_hmer_humidity 142 | id: humidity_halo 143 | internal: true 144 | - platform: homeassistant 145 | entity_id: sensor.living_room_sensor_temperature 146 | id: temp_nappali 147 | internal: true 148 | - platform: homeassistant 149 | entity_id: sensor.living_room_sensor_humidity 150 | id: humidity_nappali 151 | internal: true 152 | - platform: homeassistant 153 | entity_id: sensor.office_sensor_temperature 154 | id: temp_iroda 155 | internal: true 156 | - platform: homeassistant 157 | entity_id: sensor.office_sensor_humidity 158 | id: humidity_iroda 159 | internal: true 160 | - platform: homeassistant 161 | entity_id: sensor.bedroom_sensor_temperature 162 | id: temp_dani 163 | internal: true 164 | - platform: homeassistant 165 | entity_id: sensor.bedroom_sensor_humidity 166 | id: humidity_dani 167 | internal: true 168 | - platform: homeassistant 169 | entity_id: sensor.matthew_sensor_temperature 170 | id: temp_mate 171 | internal: true 172 | - platform: homeassistant 173 | entity_id: sensor.matthew_sensor_humidity 174 | id: humidity_mate 175 | internal: true 176 | - platform: homeassistant 177 | entity_id: sensor.lucy_sensor_temperature 178 | id: temp_luca 179 | internal: true 180 | - platform: homeassistant 181 | entity_id: sensor.lucy_sensor_humidity 182 | id: humidity_luca 183 | internal: true 184 | 185 | binary_sensor: 186 | - platform: gpio 187 | pin: 188 | number: GPIO39 189 | inverted: true 190 | name: 'lilygo-t5 Button 1' 191 | on_press: 192 | then: 193 | - component.update: t5_display 194 | - platform: homeassistant 195 | entity_id: binary_sensor.idokep_temp_falling 196 | id: temp_falling 197 | internal: true 198 | - platform: homeassistant 199 | entity_id: binary_sensor.idokep_temp_rising 200 | id: temp_rising 201 | internal: true 202 | 203 | text_sensor: 204 | # sun/moon 205 | - platform: sun 206 | id: sunrise 207 | name: Sun Next Sunrise 208 | type: sunrise 209 | format: '%H:%M' 210 | internal: true 211 | - platform: sun 212 | id: sunset 213 | name: Sun Next Sunset 214 | type: sunset 215 | format: '%H:%M' 216 | internal: true 217 | - platform: homeassistant 218 | entity_id: sun.sun 219 | id: hasun 220 | internal: true 221 | - platform: homeassistant 222 | entity_id: sensor.moon 223 | id: moon 224 | internal: true 225 | # weather 226 | - platform: homeassistant 227 | entity_id: sensor.idokep_condition 228 | id: condition 229 | internal: true 230 | on_value: # Actions to perform once data for the last sensor has been received 231 | then: 232 | - script.execute: all_data_received 233 | - platform: homeassistant 234 | entity_id: sensor.idokep_daily_data 235 | attribute: day1_dow 236 | id: day1_dow 237 | internal: true 238 | - platform: homeassistant 239 | entity_id: sensor.idokep_daily_data 240 | attribute: day1_condition 241 | id: day1_condition 242 | internal: true 243 | - platform: homeassistant 244 | entity_id: sensor.idokep_daily_data 245 | attribute: day2_dow 246 | id: day2_dow 247 | internal: true 248 | - platform: homeassistant 249 | entity_id: sensor.idokep_daily_data 250 | attribute: day2_condition 251 | id: day2_condition 252 | internal: true 253 | - platform: homeassistant 254 | entity_id: sensor.idokep_daily_data 255 | attribute: day3_dow 256 | id: day3_dow 257 | internal: true 258 | - platform: homeassistant 259 | entity_id: sensor.idokep_daily_data 260 | attribute: day3_condition 261 | id: day3_condition 262 | internal: true 263 | # spotify 264 | # - platform: homeassistant 265 | # entity_id: media_player.spotify_totesz 266 | # id: spotify_playing_status 267 | # internal: true 268 | # - platform: homeassistant 269 | # entity_id: media_player.spotify_totesz 270 | # attribute: media_title 271 | # id: spotify_playing_title 272 | # internal: true 273 | # - platform: homeassistant 274 | # entity_id: media_player.spotify_totesz 275 | # attribute: media_artist 276 | # id: spotify_playing_artist 277 | # internal: true 278 | # on_value: # Actions to perform once data for the last sensor has been received 279 | # then: 280 | # - script.execute: all_data_received 281 | # calendar 282 | - platform: homeassistant 283 | entity_id: calendar.nevnapok 284 | attribute: message 285 | id: nevnap 286 | internal: true 287 | # zone tracking 288 | - platform: homeassistant 289 | entity_id: device_tracker.gabor_fon 290 | id: gabor_location 291 | internal: true 292 | - platform: homeassistant 293 | entity_id: sensor.gabor_distance 294 | id: gabor_distance 295 | internal: true 296 | - platform: homeassistant 297 | entity_id: device_tracker.verus_fon_3 298 | id: verus_location 299 | internal: true 300 | - platform: homeassistant 301 | entity_id: sensor.verus_distance 302 | id: verus_distance 303 | internal: true 304 | 305 | font: 306 | - file: 'fonts/OpenSans-Medium.ttf' 307 | id: font_small 308 | size: 28 309 | glyphs: |- 310 | !"%()+=,-_.:°0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz'/&|ÁÉÍÓÖŐÚÜŰáéíóöőúüű 311 | - file: 'fonts/OpenSans-Medium.ttf' 312 | id: font_medium 313 | size: 38 314 | glyphs: |- 315 | !"%()+=,-_.:°0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz'/&|ÁÉÍÓÖŐÚÜŰáéíóöőúüű 316 | - file: 'fonts/OpenSans-Bold.ttf' 317 | id: font_medium_bold 318 | size: 38 319 | glyphs: |- 320 | !"%()+=,-_.:°0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz'/&|ÁÉÍÓÖŐÚÜŰáéíóöőúüű 321 | - file: 'fonts/OpenSans-Medium.ttf' 322 | id: font_big 323 | size: 48 324 | glyphs: |- 325 | !"%()+=,-_.:°0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz'/&|ÁÉÍÓÖŐÚÜŰáéíóöőúüű 326 | - file: 'fonts/OpenSans-Bold.ttf' 327 | id: font_big_bold 328 | size: 48 329 | glyphs: |- 330 | !"%()+=,-_.:°0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz'/&|ÁÉÍÓÖŐÚÜŰáéíóöőúüű 331 | - file: 'fonts/OpenSans-Medium.ttf' 332 | id: font_large 333 | size: 56 334 | glyphs: |- 335 | !"%()+=,-_.:°0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz'/&|ÁÉÍÓÖŐÚÜŰáéíóöőúüű 336 | - file: 'fonts/OpenSans-Bold.ttf' 337 | id: font_large_bold 338 | size: 56 339 | glyphs: |- 340 | !"%()+=,-_.:°0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz'/&|ÁÉÍÓÖŐÚÜŰáéíóöőúüű 341 | - file: 'fonts/OpenSans-Bold.ttf' 342 | id: font_xlarge_bold 343 | size: 72 344 | glyphs: |- 345 | !"%()+=,-_.:°0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz'/&|ÁÉÍÓÖŐÚÜŰáéíóöőúüű 346 | # material UI icons - from https://pictogrammers.github.io/@mdi/font/5.3.45/ 347 | - file: 'fonts/materialdesignicons-webfont.ttf' 348 | id: font_icons 349 | size: 130 350 | glyphs: 351 | - "\U000F0594" # clear-night 352 | - "\U000F0590" # cloudy 353 | - "\U000F0591" # fog 354 | - "\U000F0592" # hail 355 | - "\U000F0593" # lightning 356 | - "\U000F067E" # lightning-rainy 357 | - "\U000F0F31" # night-partly-cloudy 358 | - "\U000F0595" # partly-cloudy 359 | - "\U000F0596" # pouring 360 | - "\U000F0597" # rainy 361 | - "\U000F0F36" # snowy 362 | - "\U000F067F" # snowy-rainy 363 | - "\U000F0599" # sunny 364 | - "\U000F059D" # windy 365 | - "\U000F059E" # windy-variant 366 | - "\U000F0F38" # exCentereptional 367 | - file: 'fonts/materialdesignicons-webfont.ttf' 368 | id: font_icons_small 369 | size: 56 370 | glyphs: 371 | # weather 372 | - "\U000F0594" # clear-night 373 | - "\U000F0590" # cloudy 374 | - "\U000F0591" # fog 375 | - "\U000F0592" # hail 376 | - "\U000F0593" # lightning 377 | - "\U000F067E" # lightning-rainy 378 | - "\U000F0F31" # night-partly-cloudy 379 | - "\U000F0595" # partly-cloudy 380 | - "\U000F0596" # pouring 381 | - "\U000F0597" # rainy 382 | - "\U000F0F36" # snowy 383 | - "\U000F067F" # snowy-rainy 384 | - "\U000F0599" # sunny 385 | - "\U000F059D" # windy 386 | - "\U000F059E" # windy-variant 387 | - "\U000F0F38" # exceptional 388 | # moon phases 389 | - "\U000F0F61" # moon-first-quarter 390 | - "\U000F0F62" # moon-full 391 | - "\U000F0F63" # moon-last-quarter 392 | - "\U000F0F64" # moon-new 393 | - "\U000F0F65" # moon-waning-crescent 394 | - "\U000F0F66" # moon-waning-gibbous 395 | - "\U000F0F67" # moon-waxing-crescent 396 | - "\U000F0F68" # moon-waxing-gibbous 397 | # other icons 398 | - "\U000F10C2" # Temperature High 399 | - "\U000F10C3" # Temperature Low 400 | - "\U000F054B" # umbrella 401 | - "\U000F02E3" # Bed 402 | - "\U000F064D" # human-male 403 | - "\U000F0649" # human-female 404 | - "\U000F04B9" # sofa 405 | - "\U000F04DB" # stop 406 | - "\U000F040A" # play 407 | - "\U000F03E4" # pause 408 | - "\U000F0643" # man 409 | - "\U000F1078" # woman 410 | - "\U000F0E7C" # baby-face 411 | - "\U000F0379" # monitor 412 | - file: 'fonts/materialdesignicons-webfont.ttf' 413 | id: font_icons_tiny 414 | size: 32 415 | glyphs: 416 | - "\U000F005E" # arrow-up-thick 417 | - "\U000F0046" # arrow-down-thick 418 | - "\U000F059C" # Sunrise 419 | - "\U000F059B" # Sunset 420 | 421 | display: 422 | - id: t5_display 423 | #platform: lilygo_t5_47_display 424 | platform: t547 425 | #full_update_every: 2 # optional (default: 1): 0 - never, 1 (default) - every, 2+ - throttled 426 | #cycles_render: 20 # optional (default: 20): higher number -> less ghosting, crisper image, more time 427 | #cycles_invert: 20 # optional (default: 20): higher number -> more timef or full update, but mor burn-in protection 428 | rotation: 0 429 | update_interval: 300s 430 | lambda: |- 431 | // icon constants 432 | #define ICON_temp_high "\U000F10C2" 433 | #define ICON_temp_low "\U000F10C3" 434 | #define ICON_umbrella "\U000F054B" 435 | #define ICON_bed "\U000F02E3" 436 | #define ICON_human_male "\U000F064D" 437 | #define ICON_human_female "\U000F0649" 438 | #define ICON_monitor "\U000F0379" 439 | #define ICON_sofa "\U000F04B9" 440 | #define ICON_man "\U000F0643" 441 | #define ICON_woman "\U000F1078" 442 | #define ICON_baby "\U000F0E7C" 443 | #define ICON_up "\U000F005E" 444 | #define ICON_down "\U000F0046" 445 | #define ICON_sunrise "\U000F059C" 446 | #define ICON_sunset "\U000F059B" 447 | // positioning constants 448 | #define xRes 960 449 | #define yRes 540 450 | #define xCenter (xRes/2 + 40) // X position center 451 | #define pad 10 452 | #define celsiusSuperscript 12 453 | #define rowHeight 75 454 | #define temperatureCol 195 455 | #define humidityCol 300 456 | #define weatherCol 120 457 | #define weatherTempCorr 15 458 | 459 | // helper variables 460 | int y = 0; 461 | 462 | auto time = id(ntp).now(); 463 | if (time.hour < 6 && time.hour > 2) { 464 | return; 465 | } 466 | 467 | // === Date === 468 | 469 | // date + nameday 470 | auto dayTime = id(hasun).state == "above_horizon"; 471 | std::string dateFormat = generateDateFormat(time, id(nevnap).state); 472 | it.strftime(pad, 0, id(font_big_bold), TextAlign::TOP_LEFT, dateFormat.c_str(), time); 473 | // time 474 | it.strftime(xRes - pad - 55, 0, id(font_big_bold), TextAlign::TOP_RIGHT, "%H:%M", time); 475 | // moon phase 476 | auto moonIcon = moonToIcon(id(moon).state); 477 | it.printf(xRes - pad/2, pad, id(font_icons_small), TextAlign::TOP_RIGHT, moonIcon.c_str()); 478 | 479 | // === LEFT - Spotify === 480 | 481 | /* 482 | y = 150; 483 | auto playing = id(spotify_playing_status).state == "playing"; 484 | auto paused = id(spotify_playing_status).state == "paused"; 485 | auto spotifyIcon = playbackStatusToIcon(playing, paused); 486 | it.printf(pad, y, id(font_icons_small), TextAlign::BASELINE_LEFT, spotifyIcon.c_str()); 487 | if (playing || paused) { 488 | it.printf(77, y, id(font_big), TextAlign::BASELINE_LEFT, "%.25s", id(spotify_playing_title).state.c_str()); 489 | it.printf(77, y + 46, id(font_medium), TextAlign::BASELINE_LEFT, "%.40s", id(spotify_playing_artist).state.c_str()); 490 | } else { 491 | it.printf(77, y - 5, id(font_big), TextAlign::BASELINE_LEFT, "-"); 492 | } 493 | */ 494 | 495 | // === RIGHT - Weather === 496 | 497 | it.line(xCenter - 10, 75, xRes, 75); 498 | 499 | y = 135; 500 | // current temperature 501 | if (!isnan(id(temperature).state)) { 502 | it.printf(xRes - 210, y, id(font_xlarge_bold), TextAlign::CENTER_RIGHT, "%.0f", id(temperature).state); 503 | it.printf(xRes - 210, y - celsiusSuperscript, id(font_large), TextAlign::CENTER_LEFT, "°C"); 504 | if (id(temp_rising).state) { 505 | it.printf(xRes - 210, y + 3, id(font_icons_tiny), TextAlign::TOP_LEFT, ICON_up); 506 | } 507 | else if (id(temp_falling).state) { 508 | it.printf(xRes - 210, y + 3, id(font_icons_tiny), TextAlign::TOP_LEFT, ICON_down); 509 | } 510 | } 511 | // current condition icon 512 | auto conditionIcon = conditionToIcon(id(condition).state, dayTime); 513 | it.printf(xRes - pad, y, id(font_icons), TextAlign::CENTER_RIGHT, conditionIcon.c_str()); 514 | y = 200; 515 | it.printf(xCenter + weatherCol, y, id(font_icons_tiny), TextAlign::BASELINE_RIGHT, ICON_sunrise); 516 | it.printf(xCenter + weatherCol, y, id(font_small), TextAlign::BASELINE_LEFT, "%s", id(sunrise).state.c_str()); 517 | it.printf(xCenter + weatherCol*2, y, id(font_icons_tiny), TextAlign::BASELINE_RIGHT, ICON_sunset); 518 | it.printf(xCenter + weatherCol*2, y, id(font_small), TextAlign::BASELINE_LEFT, "%s", id(sunset).state.c_str()); 519 | 520 | // === LEFT - room info === 521 | 522 | it.line(0, 75, humidityCol + 40, 75); 523 | 524 | // Living room / Nappali 525 | y = 140; // old: 215 526 | it.printf(pad, y, id(font_icons_small), TextAlign::BASELINE_LEFT, ICON_sofa); 527 | if (!isnan(id(temp_nappali).state)) { 528 | it.printf(temperatureCol, y, id(font_large), TextAlign::BASELINE_RIGHT, "%.1f", id(temp_nappali).state); 529 | it.printf(temperatureCol, y - celsiusSuperscript, id(font_medium), TextAlign::BASELINE_LEFT, "°C"); 530 | } 531 | if (!isnan(id(humidity_nappali).state)) { 532 | it.printf(humidityCol, y, id(font_medium), TextAlign::BASELINE_RIGHT, "%.0f", id(humidity_nappali).state); 533 | it.printf(humidityCol, y, id(font_small), TextAlign::BASELINE_LEFT, "%%"); 534 | } 535 | // Office / Iroda 536 | y += rowHeight; // 290 537 | it.printf(pad, y, id(font_icons_small), TextAlign::BASELINE_LEFT, ICON_monitor); 538 | if (!isnan(id(temp_iroda).state)) { 539 | it.printf(temperatureCol, y, id(font_large), TextAlign::BASELINE_RIGHT, "%.1f", id(temp_iroda).state); 540 | it.printf(temperatureCol, y - celsiusSuperscript, id(font_medium), TextAlign::BASELINE_LEFT, "°C"); 541 | } 542 | if (!isnan(id(humidity_iroda).state)) { 543 | it.printf(humidityCol, y, id(font_medium), TextAlign::BASELINE_RIGHT, "%.0f", id(humidity_iroda).state); 544 | it.printf(humidityCol, y, id(font_small), TextAlign::BASELINE_LEFT, "%%"); 545 | } 546 | // Bedroom / Háló 547 | y += rowHeight; // 290 548 | it.printf(pad, y, id(font_icons_small), TextAlign::BASELINE_LEFT, ICON_bed); 549 | if (!isnan(id(temp_halo).state)) { 550 | it.printf(temperatureCol, y, id(font_large), TextAlign::BASELINE_RIGHT, "%.1f", id(temp_halo).state); 551 | it.printf(temperatureCol, y - celsiusSuperscript, id(font_medium), TextAlign::BASELINE_LEFT, "°C"); 552 | } 553 | if (!isnan(id(humidity_halo).state)) { 554 | it.printf(humidityCol, y, id(font_medium), TextAlign::BASELINE_RIGHT, "%.0f", id(humidity_halo).state); 555 | it.printf(humidityCol, y, id(font_small), TextAlign::BASELINE_LEFT, "%%"); 556 | } 557 | // Kid 3 / Dani 558 | y += rowHeight; // 350 559 | // it.printf(pad, y, id(font_icons_small), TextAlign::BASELINE_LEFT, ICON_baby); 560 | it.printf(pad + 28, y, id(font_large), TextAlign::BASELINE_CENTER, "D"); 561 | if (!isnan(id(temp_dani).state)) { 562 | it.printf(temperatureCol, y, id(font_large), TextAlign::BASELINE_RIGHT, "%.1f", id(temp_dani).state); 563 | it.printf(temperatureCol, y - celsiusSuperscript, id(font_medium), TextAlign::BASELINE_LEFT, "°C"); 564 | } 565 | if (!isnan(id(humidity_dani).state)) { 566 | it.printf(humidityCol, y, id(font_medium), TextAlign::BASELINE_RIGHT, "%.0f", id(humidity_dani).state); 567 | it.printf(humidityCol, y, id(font_small), TextAlign::BASELINE_LEFT, "%%"); 568 | } 569 | // Kid 1 / Máté 570 | y += rowHeight; // 425 571 | // it.printf(pad, y, id(font_icons_small), TextAlign::BASELINE_LEFT, ICON_human_male); 572 | it.printf(pad + 28, y, id(font_large), TextAlign::BASELINE_CENTER, "M"); 573 | if (!isnan(id(temp_mate).state)) { 574 | it.printf(temperatureCol, y, id(font_large), TextAlign::BASELINE_RIGHT, "%.1f", id(temp_mate).state); 575 | it.printf(temperatureCol, y - celsiusSuperscript, id(font_medium), TextAlign::BASELINE_LEFT, "°C"); 576 | } 577 | if (!isnan(id(humidity_mate).state)) { 578 | it.printf(humidityCol, y, id(font_medium), TextAlign::BASELINE_RIGHT, "%.0f", id(humidity_mate).state); 579 | it.printf(humidityCol, y, id(font_small), TextAlign::BASELINE_LEFT, "%%"); 580 | } 581 | // Kid 2 / Luca 582 | y += rowHeight; // 500 583 | // it.printf(pad, y, id(font_icons_small), TextAlign::BASELINE_LEFT, ICON_human_female); 584 | it.printf(pad + 28, y, id(font_large), TextAlign::BASELINE_CENTER, "L"); 585 | if (!isnan(id(temp_luca).state)) { 586 | it.printf(temperatureCol, y, id(font_large), TextAlign::BASELINE_RIGHT, "%.1f", id(temp_luca).state); 587 | it.printf(temperatureCol, y - celsiusSuperscript, id(font_medium), TextAlign::BASELINE_LEFT, "°C"); 588 | } 589 | if (!isnan(id(humidity_luca).state)) { 590 | it.printf(humidityCol, y, id(font_medium), TextAlign::BASELINE_RIGHT, "%.0f", id(humidity_luca).state); 591 | it.printf(humidityCol, y, id(font_small), TextAlign::BASELINE_LEFT, "%%"); 592 | } 593 | 594 | // === MIDDLE - Locations === 595 | 596 | it.line(humidityCol + 40, 75, humidityCol + 40, yRes); 597 | 598 | auto centerCol = (humidityCol + 40 + xCenter - 10) / 2; 599 | y = 150; 600 | it.printf(centerCol, y + rowHeight/4, id(font_icons_small), TextAlign::BASELINE_CENTER, ICON_man); 601 | y += rowHeight; // 350 602 | auto gaborLocationText = locationToHungarian(id(gabor_location).state, id(gabor_distance).state); 603 | it.printf(centerCol, y - rowHeight/4, id(font_medium), TextAlign::BASELINE_CENTER, "%s", gaborLocationText.c_str()); 604 | 605 | y += rowHeight; // 425 606 | y += rowHeight; // 425 607 | it.printf(centerCol, y + rowHeight/4, id(font_icons_small), TextAlign::BASELINE_CENTER, ICON_woman); 608 | y += rowHeight; // 500 609 | auto verusLocationText = locationToHungarian(id(verus_location).state, id(verus_distance).state); 610 | it.printf(centerCol, y - rowHeight/4, id(font_medium), TextAlign::BASELINE_CENTER, "%s", verusLocationText.c_str()); 611 | 612 | it.line(xCenter - 10, 75, xCenter - 10, yRes); 613 | 614 | // === RIGHT - Forecast === 615 | 616 | it.line(xCenter - 10, 205, xRes, 205); 617 | 618 | // Day of week 619 | y = 240; 620 | it.printf(xCenter + weatherCol, y, id(font_medium), TextAlign::BASELINE_CENTER, "%s", id(day1_dow).state.c_str()); 621 | it.printf(xCenter + weatherCol*2, y, id(font_medium), TextAlign::BASELINE_CENTER, "%s", id(day2_dow).state.c_str()); 622 | it.printf(xCenter + weatherCol*3, y, id(font_medium), TextAlign::BASELINE_CENTER, "%s", id(day3_dow).state.c_str()); 623 | 624 | // Condition 625 | y += 65; // 295 626 | auto day1Icon = conditionToIcon(id(day1_condition).state, dayTime); 627 | auto day2Icon = conditionToIcon(id(day2_condition).state, dayTime); 628 | auto day3Icon = conditionToIcon(id(day3_condition).state, dayTime); 629 | it.printf(xCenter + weatherCol, y, id(font_icons_small), TextAlign::BASELINE_CENTER, day1Icon.c_str()); 630 | it.printf(xCenter + weatherCol*2, y, id(font_icons_small), TextAlign::BASELINE_CENTER, day2Icon.c_str()); 631 | it.printf(xCenter + weatherCol*3, y, id(font_icons_small), TextAlign::BASELINE_CENTER, day3Icon.c_str()); 632 | 633 | // High/low temperature 634 | y += 60; // 350 635 | it.printf(xCenter, y, id(font_icons_small), TextAlign::BASELINE_LEFT, ICON_temp_high); 636 | if (!isnan(id(day1_max).state)) { 637 | it.printf(xCenter + weatherCol + weatherTempCorr, y, id(font_large), TextAlign::BASELINE_RIGHT, "%.0f", id(day1_max).state); 638 | it.printf(xCenter + weatherCol + weatherTempCorr, y - celsiusSuperscript, id(font_medium), TextAlign::BASELINE_LEFT, "°C"); 639 | } 640 | if (!isnan(id(day2_max).state)) { 641 | it.printf(xCenter + weatherCol*2 + weatherTempCorr, y, id(font_large), TextAlign::BASELINE_RIGHT, "%.0f", id(day2_max).state); 642 | it.printf(xCenter + weatherCol*2 + weatherTempCorr, y - celsiusSuperscript, id(font_medium), TextAlign::BASELINE_LEFT, "°C"); 643 | } 644 | if (!isnan(id(day3_max).state)) { 645 | it.printf(xCenter + weatherCol*3 + weatherTempCorr, y, id(font_large), TextAlign::BASELINE_RIGHT, "%.0f", id(day3_max).state); 646 | it.printf(xCenter + weatherCol*3 + weatherTempCorr, y - celsiusSuperscript, id(font_medium), TextAlign::BASELINE_LEFT, "°C"); 647 | } 648 | y += rowHeight; // 425 649 | it.printf(xCenter, y, id(font_icons_small), TextAlign::BASELINE_LEFT, ICON_temp_low); 650 | if (!isnan(id(day1_min).state)) { 651 | it.printf(xCenter + weatherCol + weatherTempCorr, y, id(font_large), TextAlign::BASELINE_RIGHT, "%.0f", id(day1_min).state); 652 | it.printf(xCenter + weatherCol + weatherTempCorr, y - celsiusSuperscript, id(font_medium), TextAlign::BASELINE_LEFT, "°C"); 653 | } 654 | if (!isnan(id(day2_min).state)) { 655 | it.printf(xCenter + weatherCol*2 + weatherTempCorr, y, id(font_large), TextAlign::BASELINE_RIGHT, "%.0f", id(day2_min).state); 656 | it.printf(xCenter + weatherCol*2 + weatherTempCorr, y - celsiusSuperscript, id(font_medium), TextAlign::BASELINE_LEFT, "°C"); 657 | } 658 | if (!isnan(id(day3_min).state)) { 659 | it.printf(xCenter + weatherCol*3 + weatherTempCorr, y, id(font_large), TextAlign::BASELINE_RIGHT, "%.0f", id(day3_min).state); 660 | it.printf(xCenter + weatherCol*3 + weatherTempCorr, y - celsiusSuperscript, id(font_medium), TextAlign::BASELINE_LEFT, "°C"); 661 | } 662 | 663 | // Precipitation 664 | y += rowHeight; // 425 665 | it.printf(xCenter, y, id(font_icons_small), TextAlign::BASELINE_LEFT, ICON_umbrella); 666 | // 20mm precipitation is using the max bar height, which is 25px 667 | if (!isnan(id(day1_prec).state)) { 668 | auto prec1size = max(min(id(day1_prec).state * 25.0 / 20.0, 25.0), 1.0); 669 | it.filled_rectangle(xCenter + weatherCol - 45, y - prec1size, weatherCol - pad*2, prec1size); 670 | if (id(day1_prec).state > 1) { 671 | it.printf(xCenter + weatherCol + 3, y-prec1size-pad/2, id(font_medium), TextAlign::BASELINE_RIGHT, "%.0f", id(day1_prec).state); 672 | it.printf(xCenter + weatherCol + 3, y-prec1size-pad/2, id(font_small), TextAlign::BASELINE_LEFT, "mm"); 673 | } else if (id(day1_prec).state > 0) { 674 | it.printf(xCenter + weatherCol + 3, y-prec1size-pad/2, id(font_medium), TextAlign::BASELINE_CENTER, ". . ."); 675 | } 676 | } 677 | if (!isnan(id(day2_prec).state)) { 678 | auto prec2size = max(min(id(day2_prec).state * 25.0 / 20.0, 25.0), 1.0); 679 | it.filled_rectangle(xCenter + weatherCol*2 - 45, y - prec2size, weatherCol - pad*2, prec2size); 680 | if (id(day2_prec).state > 1) { 681 | it.printf(xCenter + weatherCol*2 + 3, y-prec2size-pad/2, id(font_medium), TextAlign::BASELINE_RIGHT, "%.0f", id(day2_prec).state); 682 | it.printf(xCenter + weatherCol*2 + 3, y-prec2size-pad/2, id(font_small), TextAlign::BASELINE_LEFT, "mm"); 683 | } else if (id(day2_prec).state > 0) { 684 | it.printf(xCenter + weatherCol*2 + 3, y-prec2size-pad/2, id(font_medium), TextAlign::BASELINE_CENTER, ". . ."); 685 | } 686 | } 687 | if (!isnan(id(day3_prec).state)) { 688 | auto prec3size = max(min(id(day3_prec).state * 25.0 / 20.0, 25.0), 1.0); 689 | it.filled_rectangle(xCenter + weatherCol*3 - 45, y - prec3size, weatherCol - pad*2, prec3size); 690 | if (id(day3_prec).state > 1) { 691 | it.printf(xCenter + weatherCol*3 + 3, y-prec3size-pad/2, id(font_medium), TextAlign::BASELINE_RIGHT, "%.0f", id(day3_prec).state); 692 | it.printf(xCenter + weatherCol*3 + 3, y-prec3size-pad/2, id(font_small), TextAlign::BASELINE_LEFT, "mm"); 693 | } else if (id(day3_prec).state > 0) { 694 | it.printf(xCenter + weatherCol*3 + 3, y-prec3size-pad/2, id(font_medium), TextAlign::BASELINE_CENTER, ". . ."); 695 | } 696 | } 697 | 698 | // === Footer/debug === 699 | 700 | //it.printf(xRes - pad, yRes - pad/2, id(font_small), TextAlign::BASELINE_RIGHT, "%.2fV/%.0f%%", id(batt_volt).voltage->state, id(batt).state); 701 | //it.printf(pad, yRes - pad/2, id(font_small), TextAlign::BASELINE_LEFT, "%s", icon); 702 | --------------------------------------------------------------------------------