├── .gitattributes ├── LICENSE ├── README.de.md ├── README.md ├── __init__.py ├── components ├── __init__.py └── shys_m5_dial │ ├── __init__.py │ ├── global_image_data.h │ ├── globals.h │ ├── ha_api.h │ ├── ha_device.h │ ├── ha_device_climate.h │ ├── ha_device_cover.h │ ├── ha_device_fan.h │ ├── ha_device_light.h │ ├── ha_device_lock.h │ ├── ha_device_mediaplayer.h │ ├── ha_device_mode.h │ ├── ha_device_mode_climate_temperature.h │ ├── ha_device_mode_cover_position.h │ ├── ha_device_mode_fan_speed.h │ ├── ha_device_mode_light_brightness.h │ ├── ha_device_mode_light_color.h │ ├── ha_device_mode_light_on_off.h │ ├── ha_device_mode_light_tunable_white.h │ ├── ha_device_mode_lock.h │ ├── ha_device_mode_mediaplayer_play.h │ ├── ha_device_mode_mediaplayer_source.h │ ├── ha_device_mode_percentage.h │ ├── ha_device_mode_switch_on_off.h │ ├── ha_device_switch.h │ ├── m5dial_display.h │ ├── m5dial_eeprom.h │ ├── m5dial_rfid.h │ ├── m5dial_rotary.h │ ├── m5dial_touch.h │ ├── shys_m5_dial.cpp │ └── shys_m5_dial.h └── shys-m5-dial.yaml /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Daniel Scheidler 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.de.md: -------------------------------------------------------------------------------- 1 | #### Sprachauswahl: 2 | [![en](https://img.shields.io/badge/lang-en-red.svg)](README.md) 3 | [![de](https://img.shields.io/badge/lang-de-blue.svg)](README.de.md) 4 | 5 | # M5 Stack Dial Custom Component für ESPHome 6 | ![image](https://github.com/SmartHome-yourself/.github/blob/main/profile/assets/m5-dial-screens.png) 7 | Mehr Infos zum [M5 Stack Dial](https://shop.m5stack.com/products/m5stack-dial-esp32-s3-smart-rotary-knob-w-1-28-round-touch-screen?ref=smarthomeyourself) 8 | 9 | Mit dieser Komponente wird der M5 Stack Dial zu einer universal-Fernbedienung für Home Assistant. 10 | 11 | Aktuell werden folgende Entitäten unterstützt: 12 | - climate 13 | - cover 14 | - fan 15 | - light 16 | - switch 17 | - media_player 18 | - lock 19 | 20 | ## Video 21 | [![M5 Stack Dial](http://img.youtube.com/vi/4dE7YONEYVk/0.jpg)](https://www.youtube.com/watch?v=4dE7YONEYVk "M5 Dial als Home Assistant Fernbedienung") 22 | 23 |   24 | 25 | ## Discord 26 | Für einen schnellen Austausch, Anregungen so wie Info's zum aktuellen Entwicklungsstand steht auch unser Discord zur Verfügung. 27 | Einladungs-Link: https://discord.gg/pN7SpK7 28 | 29 |   30 | 31 | # Beispiel Konfiguration: 32 | ```yaml 33 | substitutions: 34 | devicename: "m5-dial" 35 | wifi_ssid: !secret wifi_ssid 36 | wifi_password: !secret wifi_password 37 | 38 | packages: 39 | m5_tough_package: 40 | url: https://github.com/SmartHome-yourself/m5-dial-for-esphome 41 | ref: main 42 | file: shys-m5-dial.yaml 43 | refresh: 1h 44 | 45 | shys_m5_dial: 46 | name: Dial 47 | screen_off_time: 45000 48 | rotary_step_width: 5 49 | long_press_duration: 1200 50 | font: FreeSans12pt7b 51 | font_factor: 1 52 | 53 | devices: 54 | lights: 55 | - entity: light.my_light1 56 | name: Light 1 57 | modes: 58 | dimm_mode: 59 | enable: true 60 | rotary_step_width: 10 61 | rgb_mode: 62 | enable: true 63 | rotary_step_width: 30 64 | white_mode: 65 | enable: true 66 | rotary_step_width: 100 67 | min_kelvin: 2500 68 | min_kelvin: 7000 69 | 70 | - entity: light.my_light2 71 | name: Light 2 72 | 73 | switches: 74 | - entity: switch.my_switch 75 | name: Switch 1 76 | 77 | climates: 78 | - entity: climate.my_climate1 79 | name: Heater 80 | modes: 81 | temp_mode: 82 | rotary_step_width: 1 83 | min_temperature: 4 84 | max_temperature: 30 85 | 86 | covers: 87 | - entity: cover.my_cover1 88 | name: Cover 1 89 | modes: 90 | position_mode: 91 | rotary_step_width: 5 92 | 93 | fans: 94 | - entity: fan.my_fan1 95 | name: Fan 1 96 | modes: 97 | speed_mode: 98 | changeable_direction: true 99 | rotary_step_width: 10 100 | 101 | media_player: 102 | - entity: media_player.my_player1 103 | name: MediaPlayer 1 104 | modes: 105 | play_mode: 106 | rotary_step_width: 10 107 | source_mode: 108 | rotary_step_width: 1 109 | sources: 110 | - name: 1Live 111 | content_id: 1Live 112 | content_type: TUNEIN 113 | - name: WDR2 114 | content_id: "http://wdr-wdr2-bergischesland.icecast.wdr.de/wdr/wdr2/bergischesland/mp3/128/stream.mp3" 115 | content_type: custom 116 | 117 | lock: 118 | - entity: lock.my_lock1 119 | name: Lock 1 120 | modes: 121 | lock_mode: 122 | rotary_step_width: 1 123 | open_on_button: false 124 | ``` 125 | 126 |   127 | 128 | # Konfiguration 129 | In den Substitutions muss auf jeden Fall der `devicename` angegeben werden. 130 | Die Funknetzwerk-Verbindung muss entweder über den normalen `wifi:` Abschnitt vollständig konfiguriert werden, oder man kann alternativ auch einfach die WLAN-SSID und das WLAN-Passwort in den Substitutions angeben. 131 | 132 | ## Substitutions 133 | ``` 134 | substitutions: 135 | name: "m5-dial" 136 | wifi_ssid: !secret wifi_ssid 137 | wifi_password: !secret wifi_password 138 | ``` 139 | 140 | **name** 141 | Der Hostname des Geräts. *(default: m5-dial)* 142 | 143 | **wifi_ssid** 144 | Der Hostname des Geräts. *(default: !secret wifi_ssid)* 145 | 146 | **wifi_password** 147 | Der Hostname des Geräts. *(default: !secret wifi_password)* 148 | 149 | 150 |   151 | 152 | ----- 153 | ## Custom-Component 154 | ----- 155 | ## Allgemeine Attribute 156 | Allgemeine Attribute sind alle Parameter, die direkt unter der Custom-Component "shys_m5_dial" zur Verfügung stehen. 157 | 158 | **Code:** 159 | ``` 160 | shys_m5_dial: 161 | name: Dial 162 | screenOffTime: 45000 163 | rotaryStepWidth: 5 164 | longPressDuration: 1200 165 | ``` 166 | 167 | **name** 168 | Legt den Namen der Komponente fest. 169 | 170 | **screenOffTime (optional)** *(Default: 30000)* 171 | Gibt an, nach wie viel Millisekunden das Display sich automatisch abschaltet 172 | 173 | **rotaryStepWidth (optional)** *(Default: 10)* 174 | Gibt die allgemeine Schrittweite an, um die der Wert bei Verwendung des Drehreglers pro Schritt verändert wird. 175 | Der hier eingestellte Wert gilt für alle Modi, bei denen keine abweichende Schrittweite angegeben wird. 176 | 177 | **longPressDuration (optional)** *(Default: 1200)* 178 | Gibt die Dauer an, ab wie viel Millisekunden ein Tastendruck als Long-Press gelten soll. 179 | 180 | **font (optional)** *(Default: FreeSans12pt7b)* 181 | Gibt die zu verwendende Schriftart an. 182 | Alle zur Verfügung stehenden Schriften sind in einer Map in [globals.h](components/shys_m5_dial/globals.h) definiert. 183 | 184 | **font_factor (optional)** *(Default: 1)* 185 | Gibt den Faktor an, der auf die Schriftgröße angewendet werden soll. 186 | *Gültige Werte: 0.1 - 10.0* 187 | 188 |   189 | 190 | ------ 191 | 192 | ## devices 193 | 194 | Unter Devices werden alle Entitäten angegeben, die mit dem M5 Dial gesteuert werden sollen. 195 | Die Angabe der Entitäten erfolgt in einzelnen Listen je Geräteart (Domain) wie z.B. Lichter, Schalter, Jalousien usw. 196 | 197 | **Code:** 198 | ``` 199 | shys_m5_dial: 200 | ... 201 | devices: 202 | ``` 203 | 204 |   205 | 206 | ------ 207 | 208 | ## **LIGHTS** 209 | Unter "devices - lights" werden alle Light-Entitäten angegeben. 210 | Ohne weitere Angaben lässt sich die Lampe über den Dial nur an-/aus-schalten. 211 | Um Farbe, Helligkeit oder den Weißwert einstellen zu können muss der jeweilige Mode aktiviert werden. 212 | 213 | **Code:** 214 | ``` 215 | shys_m5_dial: 216 | ... 217 | devices: 218 | lights: 219 | - entity: light.my_light1 220 | name: Light 1 221 | ``` 222 | 223 | **entity** 224 | Angabe der Light-Entity-ID aus Home Assistant, die gesteuert werden soll. 225 | 226 | **name** 227 | Der auf dem Display angezeigte Name der Entität. 228 | 229 | ## modes (optional) 230 | #### dimm_mode 231 | Mit Hilfe des Dimm-Mode lässt sich die Helligkeit der Light-Entität regeln. 232 | Ist der Dimm-Mode aktiv, ersetzt dieser den Standard-On/Off-Mode. 233 | Die Helligkeit lässt sich sowohl über den Drehregler als auch per Touch steuern. 234 | Ein Druck auf den Button schaltet das Licht an/aus. 235 | 236 | **Code:** 237 | ``` 238 | shys_m5_dial: 239 | ... 240 | devices: 241 | lights: 242 | - entity: light.my_light1 243 | name: Light 1 244 | modes: 245 | dimm_mode: 246 | enable: true 247 | rotary_step_width: 10 248 | ``` 249 | 250 | **enable** *(Default: false)* 251 | Durch setzen auf true wird der Modus für die Entität aktiviert. 252 | 253 | **rotary_step_width (optional)** 254 | Gibt die allgemeine Schrittweite an, um die der Wert bei Verwendung des Drehreglers pro Schritt verändert wird. 255 | Der hier eingestellte Wert überschreibt den allgemein eingestellten Wert und gilt nur für den Helligkeits-Modus dieser Light Entität. 256 | *Gültige Werte 1 - 100* 257 | 258 |   259 | 260 | #### white_mode 261 | Mit Hilfe des White-Mode lässt sich der Weißton für die Light-Entität in Kelvin regeln. 262 | Der Weiß-Ton lässt sich sowohl über den Drehregler als auch per Touch steuern. 263 | Ein Druck auf den Button schaltet das Licht an/aus. 264 | 265 | **Code:** 266 | ``` 267 | shys_m5_dial: 268 | ... 269 | devices: 270 | lights: 271 | - entity: light.my_light1 272 | name: Light 1 273 | modes: 274 | white_mode: 275 | enable: true 276 | rotary_step_width: 100 277 | min_kelvin: 2500 278 | min_kelvin: 7000 279 | ``` 280 | 281 | **enable** *(Default: false)* 282 | Durch setzen auf true wird der Modus für die Entität aktiviert. 283 | 284 | **rotary_step_width (optional)** 285 | Gibt die allgemeine Schrittweite an, um die der Wert bei Verwendung des Drehreglers pro Schritt verändert wird. 286 | Der hier eingestellte Wert überschreibt den allgemein eingestellten Wert und gilt nur für den Weiß-Ton Modus dieser Light Entität. 287 | *Gültige Werte 1 - 500* 288 | 289 | **min_kelvin (optional)** *(Default: 2000)* 290 | Gibt den Minimalwert in Kelvin für die Weiß-Ton Steuerung an. 291 | *Gültige Werte 1000 - 10000* 292 | 293 | **max_kelvin (optional)** *(Default: 6500)* 294 | Gibt den Maximalwert in Kelvin für die Weiß-Ton Steuerung an. 295 | *Gültige Werte 1000 - 10000* 296 | 297 |   298 | 299 | #### rgb_mode 300 | Mit Hilfe des RGB-Mode (Farbwahl) lässt sich die Farbe für die Light-Entität auswählen. 301 | Die Farbe lässt sich sowohl über den Drehregler als auch per Touch steuern. 302 | Ein Druck auf den Button schaltet das Licht an/aus. 303 | 304 | **Code:** 305 | ``` 306 | shys_m5_dial: 307 | ... 308 | devices: 309 | lights: 310 | - entity: light.my_light1 311 | name: Light 1 312 | modes: 313 | rgb_mode: 314 | enable: true 315 | rotary_step_width: 30 316 | ``` 317 | 318 | **enable** *Default: false* 319 | Durch setzen auf true wird der Modus für die Entität aktiviert. 320 | 321 | **rotary_step_width (optional)** 322 | Gibt die allgemeine Schrittweite an, um die der Wert bei Verwendung des Drehreglers pro Schritt verändert wird. 323 | Der hier eingestellte Wert überschreibt den allgemein eingestellten Wert und gilt nur für den Farbwahl-Modus dieser Light Entität. 324 | *Gültige Werte 1 - 100* 325 | 326 |   327 | 328 | ------ 329 | 330 | ## **CLIMATES** 331 | Unter "devices - climates" werden alle Climate-Entitäten angegeben. 332 | 333 | **Code:** 334 | ``` 335 | shys_m5_dial: 336 | ... 337 | devices: 338 | climates: 339 | - entity: climate.my_climate1 340 | name: Heater 341 | ``` 342 | 343 | **entity** 344 | Angabe der Light-Entity-ID aus Home Assistant, die gesteuert werden soll. 345 | 346 | **name** 347 | Der auf dem Display angezeigte Name der Entität. 348 | 349 | ## modes (optional) 350 | #### temp_mode 351 | Mit Hilfe des Temp-Mode lässt sich die Temperatur der Climate-Entität regeln. 352 | Die Temperatur lässt sich sowohl über den Drehregler als auch per Touch steuern. 353 | Ein Druck auf den Button schalten die Heizung an/aus. 354 | 355 | **Code:** 356 | ``` 357 | shys_m5_dial: 358 | ... 359 | devices: 360 | climates: 361 | - entity: climate.my_climate1 362 | name: Heater 363 | modes: 364 | temp_mode: 365 | rotary_step_width: 1 366 | min_temperature: 4 367 | max_temperature: 30 368 | ``` 369 | 370 | **rotary_step_width (optional)** *Default: 1* 371 | Gibt die allgemeine Schrittweite an, um die der Wert bei Verwendung des Drehreglers pro Schritt verändert wird. 372 | Bei Climates gilt die 1 als Default, egal was als allgemeiner Standard in der Component eingestellt wurde. 373 | *Gültige Werte 1 - 100* 374 | 375 | **min_temperature (optional)** *Default: 4* 376 | Legt die minimale Temperatur fest, die am Dial für diese Climate-Entität eingestellt werden kann. 377 | 378 | **max_temperature (optional)** *Default: 30* 379 | Legt die maximale Temperatur fest, die am Dial für diese Climate-Entität eingestellt werden kann. 380 | 381 |   382 | 383 | ------ 384 | 385 | ## **COVER** 386 | Unter "devices - cover" werden alle Cover-Entitäten angegeben. 387 | 388 | **Code:** 389 | ``` 390 | shys_m5_dial: 391 | ... 392 | devices: 393 | covers: 394 | - entity: cover.my_cover1 395 | name: Cover 1 396 | ``` 397 | 398 | **entity** 399 | Angabe der Light-Entity-ID aus Home Assistant, die gesteuert werden soll. 400 | 401 | **name** 402 | Der auf dem Display angezeigte Name der Entität. 403 | 404 | ## modes (optional) 405 | #### position_mode 406 | Mit Hilfe des Position-Mode lässt sich die Position der Cover-Entität steuern. 407 | Die Position lässt sich sowohl über den Drehregler als auch per Touch steuern. 408 | 409 | **Code:** 410 | ``` 411 | shys_m5_dial: 412 | ... 413 | devices: 414 | covers: 415 | - entity: cover.my_cover1 416 | name: Cover 1 417 | modes: 418 | position_mode: 419 | rotary_step_width: 5 420 | ``` 421 | 422 | **rotary_step_width (optional)** 423 | Gibt die allgemeine Schrittweite an, um die der Wert bei Verwendung des Drehreglers pro Schritt verändert wird. 424 | Der hier eingestellte Wert überschreibt den allgemein eingestellten Wert und gilt nur für den Position-Modus dieser Cover-Entität. 425 | *Gültige Werte 1 - 100* 426 | 427 |   428 | 429 | ------ 430 | 431 | ## **FANS** 432 | Unter "devices - cover" werden alle Fan-Entitäten angegeben. 433 | 434 | **Code:** 435 | ``` 436 | shys_m5_dial: 437 | ... 438 | devices: 439 | fans: 440 | - entity: fan.my_fan1 441 | name: Fan 1 442 | ``` 443 | 444 | **entity** 445 | Angabe der Fan-Entity-ID aus Home Assistant, die gesteuert werden soll. 446 | 447 | **name** 448 | Der auf dem Display angezeigte Name des Ventilators. 449 | 450 | ## modes (optional) 451 | #### speed_mode 452 | Mit Hilfe des Speed-Mode lässt sich die Geschwindigkeit der Fan-Entität steuern. 453 | Die Geschwindigkeit lässt sich sowohl über den Drehregler als auch per Touch steuern. 454 | Ein Druck auf den Button schalten den Ventilator an/aus. 455 | 456 | **Code:** 457 | ``` 458 | shys_m5_dial: 459 | ... 460 | devices: 461 | fans: 462 | - entity: fan.my_fan1 463 | name: Fan 1 464 | modes: 465 | speed_mode: 466 | changeable_direction: true 467 | rotary_step_width: 10 468 | ``` 469 | 470 | **changeable_direction (optional)** *(Default: false)* 471 | Gibt an, ob der Ventilator zwei Laufrichtungen hat und somit zwischen vorwärts und rückwärts umgestellt werden kann. 472 | False = Nur eine Laufrichtung. Drehen nach rechts erhöht die Geschwindigkeit, Drehen nach links reduziert die Geschwindigkeit. 473 | True = Zwei Laufrichtungen. Drehen nach rechts erhöht für Vorwärts die Geschwindigkeit. Drehen nach links reduziert die Geschwindigkeit für Vorwärts. 474 | Man kann allerdings jetzt über 0 hinweg drehen und somit die Drehrichtung zu ändern. 475 | Läuft der Lüfter rückwärts, erhöht links drehen die Geschwindigkeit und rechts drehen reduziert diese 476 | *Gültige Werte true / false* 477 | 478 | **rotary_step_width (optional)** 479 | Gibt die allgemeine Schrittweite an, um die der Wert bei Verwendung des Drehreglers pro Schritt verändert wird. 480 | Der hier eingestellte Wert überschreibt den allgemein eingestellten Wert und gilt nur für den Speed-Modus dieser Ventilator-Entität. 481 | *Gültige Werte 1 - 100* 482 | 483 |   484 | 485 | ------ 486 | 487 | ## **MEDIA PLAYER** 488 | Unter "devices - media_player" werden alle MediaPlayer-Entitäten angegeben. 489 | 490 | **Code:** 491 | ``` 492 | shys_m5_dial: 493 | ... 494 | devices: 495 | media_player: 496 | - entity: media_player.my_player1 497 | name: MediaPlayer 1 498 | ``` 499 | 500 | **entity** 501 | Angabe der MediaPlayer-Entity-ID aus Home Assistant, die gesteuert werden soll. 502 | 503 | **name** 504 | Der auf dem Display angezeigte Name des MediaPlayers. 505 | 506 | ## modes (optional) 507 | #### play_mode 508 | Mit Hilfe des Play-Mode lässt sich über den Drehregler die Lautstärke regeln, so wie per Touch die Wiedergabe starten/pausieren oder zum nächsten bzw. vorherigen Lied wechseln. 509 | Ein Druck auf den Button wechselt zwischen Play und Pause. 510 | 511 | **Code:** 512 | ``` 513 | shys_m5_dial: 514 | ... 515 | devices: 516 | media_player: 517 | - entity: media_player.my_player1 518 | name: MediaPlayer 1 519 | modes: 520 | play_mode: 521 | rotary_step_width: 10 522 | ``` 523 | 524 | **rotary_step_width (optional)** 525 | Gibt die allgemeine Schrittweite an, um die der Wert bei Verwendung des Drehreglers pro Schritt verändert wird. 526 | Der hier eingestellte Wert überschreibt den allgemein eingestellten Wert und gilt nur für den Play-Modus dieser MediaPlayer-Entität. 527 | *Gültige Werte 1 - 100* 528 | 529 | 530 |   531 | 532 | #### source_mode 533 | Mit Hilfe des Source-Mode lässt sich über den Drehregler eine Quelle aus den hinterlegten "Sources" auswählen, die abgespielt werden soll. 534 | Ein Druck auf den Button startet die Wiedergabe der ausgewählten Quelle. 535 | 536 | **Code:** 537 | ``` 538 | shys_m5_dial: 539 | ... 540 | devices: 541 | media_player: 542 | - entity: media_player.my_player1 543 | name: MediaPlayer 1 544 | modes: 545 | play_mode: 546 | rotary_step_width: 10 547 | source_mode: 548 | rotary_step_width: 1 549 | sources: 550 | - name: 1Live 551 | content_id: 1Live 552 | content_type: TUNEIN 553 | - name: WDR2 554 | content_id: "http://wdr-wdr2-bergischesland.icecast.wdr.de/wdr/wdr2/bergischesland/mp3/128/stream.mp3" 555 | content_type: custom 556 | ``` 557 | 558 | **rotary_step_width (optional)** *(Default: 1)* 559 | Gibt die allgemeine Schrittweite an, um die der Wert bei Verwendung des Drehreglers pro Schritt verändert wird. 560 | Bei source_mode von media_player gilt die 1 als Default, egal was als allgemeiner Standard in der Component eingestellt wurde. 561 | *Gültige Werte 1 - 100* 562 | 563 | **sources (optional)** 564 | Eine Liste von Audio-Quellen, die zum Abspielen zur Auswahl stehen sollen. 565 | Die Einträge müssen immer die Attribute "name", "content_id" und "content_type" enthalten! 566 | 567 | Beispiele für Amazon Echo (Alexa): 568 | ``` 569 | - name: 1Live 570 | content_id: 1Live 571 | content_type: TUNEIN 572 | 573 | - name: Metallica 574 | content_id: metallica 575 | content_type: AMAZON_MUSIC 576 | 577 | - name: Nothing else matters 578 | content_id: play metallica nothing else matters 579 | content_type: custom 580 | ``` 581 | 582 | Beispiel für DLNA Player: 583 | ``` 584 | - name: 1Live 585 | content_id: "https://wdr-1live-live.icecastssl.wdr.de/wdr/1live/live/mp3/128/stream.mp3" 586 | content_type: custom 587 | ``` 588 | 589 |   590 | 591 | ------ 592 | 593 | ## **LOCKS** 594 | Unter "devices - lock" werden alle Fan-Entitäten angegeben. 595 | 596 | **Code:** 597 | ``` 598 | shys_m5_dial: 599 | ... 600 | devices: 601 | lock: 602 | - entity: lock.my_lock1 603 | name: Lock 1 604 | ``` 605 | 606 | **entity** 607 | Angabe der Lock-Entity-ID aus Home Assistant, die gesteuert werden soll. 608 | 609 | **name** 610 | Der auf dem Display angezeigte Name des Schlosses. 611 | 612 | ## modes (optional) 613 | #### lock_mode 614 | Mit Hilfe des Lock-Mode lässt sich der Zustand der Lock-Entität steuern. 615 | Das Schloss lässt sich über den Drehregler aufschließen, abschließen und öffnen. 616 | Ein Druck auf den Button schließt normalerweise das Schloss je nach aktuellem Zustand auf oder ab. 617 | Ist *open_on_button* allerdings auf *true* gesetzt, wird das Schloss wenn es abgeschlossen ist bei Druck auf den Button nicht aufgeschlossen, sondern geöffnet. 618 | 619 | **Code:** 620 | ``` 621 | shys_m5_dial: 622 | ... 623 | devices: 624 | lock: 625 | - entity: lock.my_lock1 626 | name: Lock 1 627 | modes: 628 | lock_mode: 629 | open_on_button: true 630 | rotary_step_width: 10 631 | ``` 632 | 633 | **open_on_button (optional)** *(Default: false)* 634 | Durch setzen auf true, öffnet ein Druck auf den Button das Schloss direkt, anstatt nur das Schloss aufzuschließen. 635 | Steht der Parameter auf false (oder wird nicht angegeben), wird das Schloss nur aufgeschlossen, aber nicht ganz geöffnet. 636 | *Gültige Werte true / false* 637 | 638 | **rotary_step_width (optional)** 639 | Gibt die allgemeine Schrittweite an, um die der Wert bei Verwendung des Drehreglers pro Schritt verändert wird. 640 | Bei lock_mode von lock gilt die 1 als Default, egal was als allgemeiner Standard in der Component eingestellt wurde. 641 | Wird hier eine Schrittweite von 2 angegeben, lässt sich durch drehen das Schloss nicht mehr nur aufschließen, sondern wird automatisch direkt geöffnet. 642 | *Gültige Werte 1 - 2* 643 | 644 | 645 |   646 | 647 |   648 | 649 | ------ 650 | ------ 651 | 652 | ## Advanced 653 | Die folgenden Attribute sind zwar vorhanden, sollten aber eigentlich nicht geändert werden müssen. 654 | 655 | **send_value_delay** 656 | Gibt die Verzögerung in Millisekunden an, die bei Wertänderung gewartet wird, bevor die Änderung an Home Assistant übertragen wird. 657 | Das ist gerade bei Verwendung des Rotary-Encoders wichtig um nicht unnötig viele API-Aufrufe zu erzeugen. *(Default: 1200)* 658 | 659 | **send_value_lock** 660 | Gibt an, wie lange nach einem API-Aufruf gewartet werden soll, bevor der nächste API Aufruf erfolgen darf. *(Default: 3000)* 661 | 662 | 663 | ------ 664 | ------ 665 | 666 |   667 | 668 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #### Language Selection: 2 | [![en](https://img.shields.io/badge/lang-en-red.svg)](README.md) 3 | [![de](https://img.shields.io/badge/lang-de-blue.svg)](README.de.md) 4 | 5 | # M5 Stack Dial Custom Component for ESPHome 6 | ![image](https://github.com/SmartHome-yourself/.github/blob/main/profile/assets/m5-dial-screens.png) 7 | More information about the [M5 Stack Dial](https://shop.m5stack.com/products/m5stack-dial-esp32-s3-smart-rotary-knob-w-1-28-round-touch-screen?ref=smarthomeyourself) 8 | 9 | This component turns the M5 Stack Dial into a universal remote control for Home Assistant. 10 | 11 | Currently supported entities are: 12 | - climate 13 | - cover 14 | - fan 15 | - light 16 | - switch 17 | - media_player 18 | - lock 19 | 20 | ## Video 21 | [![M5 Stack Dial](http://img.youtube.com/vi/4dE7YONEYVk/0.jpg)](https://www.youtube.com/watch?v=4dE7YONEYVk "M5 Dial as Home Assistant Remote Control") 22 | 23 |   24 | 25 | ## Discord 26 | For quick exchanges, suggestions, and updates on the current development status, our Discord is also available. 27 | Invite Link: https://discord.gg/pN7SpK7 28 | 29 |   30 | 31 | # Example Configuration: 32 | ```yaml 33 | substitutions: 34 | name: "m5-dial" 35 | wifi_ssid: !secret wifi_ssid 36 | wifi_password: !secret wifi_password 37 | 38 | packages: 39 | m5_tough_package: 40 | url: https://github.com/SmartHome-yourself/m5-dial-for-esphome 41 | ref: main 42 | file: shys-m5-dial.yaml 43 | refresh: 1h 44 | 45 | shys_m5_dial: 46 | name: Dial 47 | screen_off_time: 45000 48 | rotary_step_width: 5 49 | long_press_duration: 1200 50 | font: FreeSans12pt7b 51 | font_factor: 1 52 | 53 | devices: 54 | lights: 55 | - entity: light.my_light1 56 | name: Light 1 57 | modes: 58 | dimm_mode: 59 | enable: true 60 | rotary_step_width: 10 61 | rgb_mode: 62 | enable: true 63 | rotary_step_width: 30 64 | white_mode: 65 | enable: true 66 | rotary_step_width: 100 67 | min_kelvin: 2500 68 | max_kelvin: 7000 69 | 70 | - entity: light.my_light2 71 | name: Light 2 72 | 73 | switches: 74 | - entity: switch.my_switch 75 | name: Switch 1 76 | 77 | climates: 78 | - entity: climate.my_climate1 79 | name: Heater 80 | modes: 81 | temp_mode: 82 | rotary_step_width: 1 83 | min_temperature: 4 84 | max_temperature: 30 85 | 86 | covers: 87 | - entity: cover.my_cover1 88 | name: Cover 1 89 | modes: 90 | position_mode: 91 | rotary_step_width: 5 92 | 93 | fans: 94 | - entity: fan.my_fan1 95 | name: Fan 1 96 | modes: 97 | speed_mode: 98 | changeable_direction: true 99 | rotary_step_width: 10 100 | 101 | media_player: 102 | - entity: media_player.my_player1 103 | name: MediaPlayer 1 104 | modes: 105 | play_mode: 106 | rotary_step_width: 10 107 | source_mode: 108 | rotary_step_width: 1 109 | sources: 110 | - name: 1Live 111 | content_id: 1Live 112 | content_type: TUNEIN 113 | - name: WDR2 114 | content_id: "http://wdr-wdr2-bergischesland.icecast.wdr.de/wdr/wdr2/bergischesland/mp3/128/stream.mp3" 115 | content_type: custom 116 | 117 | lock: 118 | - entity: lock.my_lock1 119 | name: Lock 1 120 | modes: 121 | lock_mode: 122 | rotary_step_width: 1 123 | open_on_button: false 124 | ``` 125 | 126 |   127 | 128 | # Configuration 129 | In substitutions, the `devicename` must be specified. 130 | The wireless network connection must be fully configured either through the normal `wifi:` section, or alternatively, you can simply specify the WLAN SSID and WLAN password in the substitutions. 131 | 132 | ## Substitutions 133 | ``` 134 | substitutions: 135 | name: "m5-dial" 136 | wifi_ssid: !secret wifi_ssid 137 | wifi_password: !secret wifi_password 138 | ``` 139 | 140 | **name** 141 | The hostname of the device. *(default: m5-dial)* 142 | 143 | **wifi_ssid** 144 | The hostname of the device. *(default: !secret wifi_ssid)* 145 | 146 | **wifi_password** 147 | The hostname of the device. *(default: !secret wifi_password)* 148 | 149 | 150 |   151 | 152 | ----- 153 | ## Custom-Component 154 | ----- 155 | ## General Attributes 156 | General attributes are all parameters available directly under the custom component "shys_m5_dial". 157 | 158 | **Code:** 159 | ``` 160 | shys_m5_dial: 161 | name: Dial 162 | screenOffTime: 45000 163 | rotaryStepWidth: 5 164 | longPressDuration: 1200 165 | ``` 166 | 167 | **name** 168 | Sets the name of the component. 169 | 170 | **screenOffTime (optional)** *(Default: 30000)* 171 | Indicates after how many milliseconds the display automatically turns off. 172 | 173 | **rotaryStepWidth (optional)** *(Default: 10)* 174 | Specifies the general step width by which the value changes per step when using the rotary encoder. 175 | The value set here applies to all modes where no different step width is specified. 176 | 177 | **longPressDuration (optional)** *(Default: 1200)* 178 | Indicates the duration, in milliseconds, after which a button press is considered a long press. 179 | 180 | **font (optional)** *(Default: FreeSans12pt7b)* 181 | Specifies the font to be used. 182 | All available fonts are defined in a map in [globals.h](components/shys_m5_dial/globals.h). 183 | 184 | **font_factor (optional)** *(Default: 1)* 185 | Specifies the factor to be applied to the font size. 186 | *Valid values: 0.1 - 10.0* 187 | 188 |   189 | 190 | ------ 191 | 192 | ## devices 193 | 194 | Under Devices, all entities to be controlled with the M5 Dial are specified. 195 | The entities are specified in individual lists per device type (domain) such as lights, switches, covers, etc. 196 | 197 | **Code:** 198 | ``` 199 | shys_m5_dial: 200 | ... 201 | devices: 202 | ``` 203 | 204 |   205 | 206 | ------ 207 | 208 | ## **LIGHTS** 209 | Under "devices - lights" all light entities are specified. 210 | Without further specifications, the lamp can only be turned on/off via the Dial. 211 | To adjust color, brightness, or white value, the respective mode must be activated. 212 | 213 | **Code:** 214 | ``` 215 | shys_m5_dial: 216 | ... 217 | devices: 218 | lights: 219 | - entity: light.my_light1 220 | name: Light 1 221 | ``` 222 | 223 | **entity** 224 | Specifies the Light Entity ID from Home Assistant to be controlled. 225 | 226 | **name** 227 | The name of the entity displayed on the screen. 228 | 229 | ## modes (optional) 230 | #### dimm_mode 231 | The Dimm Mode allows regulating the brightness of the light entity. 232 | When Dimm Mode is active, it replaces the standard On/Off Mode. 233 | Brightness can be controlled both via the rotary encoder and touch. 234 | A press on the button turns the light on/off. 235 | 236 | **Code:** 237 | ``` 238 | shys_m5_dial: 239 | ... 240 | devices: 241 | lights: 242 | - entity: light.my_light1 243 | name: Light 1 244 | modes: 245 | dimm_mode: 246 | enable: true 247 | rotary_step_width: 10 248 | ``` 249 | 250 | **enable** *(Default: false)* 251 | Setting to true activates the mode for the entity. 252 | 253 | **rotary_step_width (optional)** 254 | Specifies the general step width by which the value changes per step when using the rotary encoder. 255 | The value set here overrides the general setting and applies only to the brightness mode of this light entity. 256 | *Valid values: 1 - 100* 257 | 258 |   259 | 260 | #### white_mode 261 | The White Mode allows regulating the white tone for the light entity in Kelvin. 262 | The white tone can be controlled both via the rotary encoder and touch. 263 | A press on the button turns the light on/off. 264 | 265 | **Code:** 266 | ``` 267 | shys_m5_dial: 268 | ... 269 | devices: 270 | lights: 271 | - entity: light.my_light1 272 | name: Light 1 273 | modes: 274 | white_mode: 275 | enable: true 276 | rotary_step_width: 100 277 | min_kelvin: 2500 278 | max_kelvin: 7000 279 | ``` 280 | 281 | **enable** *(Default: false)* 282 | Setting to true activates the mode for the entity. 283 | 284 | **rotary_step_width (optional)** 285 | Specifies the general step width by which the value changes per step when using the rotary encoder. 286 | The value set here overrides the general setting and applies only to the white tone mode of this light entity. 287 | *Valid values: 1 - 500* 288 | 289 | **min_kelvin (optional)** *(Default: 2000)* 290 | Specifies the minimum value in Kelvin for white tone control. 291 | *Valid values: 1000 - 10000* 292 | 293 | **max_kelvin (optional)** *(Default: 6500)* 294 | Specifies the maximum value in Kelvin for white tone control. 295 | *Valid values: 1000 - 10000* 296 | 297 |   298 | 299 | #### rgb_mode 300 | The RGB Mode (color selection) allows selecting the color for the light entity. 301 | Color can be controlled both via the rotary encoder and touch. 302 | A press on the button turns the light on/off. 303 | 304 | **Code:** 305 | ``` 306 | shys_m5_dial: 307 | ... 308 | devices: 309 | lights: 310 | - entity: light.my_light1 311 | name: Light 1 312 | modes: 313 | rgb_mode: 314 | enable: true 315 | rotary_step_width: 30 316 | ``` 317 | 318 | **enable** *Default: false* 319 | Setting to true activates the mode for the entity. 320 | 321 | **rotary_step_width (optional)** 322 | Specifies the general step width by which the value changes per step when using the rotary encoder. 323 | The value set here overrides the general setting and applies only to the color selection mode of this light entity. 324 | *Valid values: 1 - 100* 325 | 326 |   327 | 328 | ------ 329 | 330 | ## **CLIMATES** 331 | Under "devices - climates" all climate entities are specified. 332 | 333 | **Code:** 334 | ``` 335 | shys_m5_dial: 336 | ... 337 | devices: 338 | climates: 339 | - entity: climate.my_climate1 340 | name: Heater 341 | ``` 342 | 343 | **entity** 344 | Specifies the Climate Entity ID from Home Assistant to be controlled. 345 | 346 | **name** 347 | The name of the entity displayed on the screen. 348 | 349 | ## modes (optional) 350 | #### temp_mode 351 | The Temp Mode allows regulating the temperature of the climate entity. 352 | Temperature can be controlled both via the rotary encoder and touch. 353 | A press on the button turns the heater on/off. 354 | 355 | **Code:** 356 | ``` 357 | shys_m5_dial: 358 | ... 359 | devices: 360 | climates: 361 | - entity: climate.my_climate1 362 | name: Heater 363 | modes: 364 | temp_mode: 365 | rotary_step_width: 1 366 | min_temperature: 4 367 | max_temperature: 30 368 | ``` 369 | 370 | **rotary_step_width (optional)** *Default: 1* 371 | Specifies the general step width by which the value changes per step when using the rotary encoder. 372 | For climates, 1 is the default, regardless of what is set as the general standard in the component. 373 | *Valid values: 1 - 100* 374 | 375 | **min_temperature (optional)** *Default: 4* 376 | Sets the minimum temperature that can be set for this climate entity on the Dial. 377 | 378 | **max_temperature (optional)** *Default: 30* 379 | Sets the maximum temperature that can be set for this climate entity on the Dial. 380 | 381 |   382 | 383 | ------ 384 | 385 | ## **COVER** 386 | Under "devices - cover" all cover entities are specified. 387 | 388 | **Code:** 389 | ``` 390 | shys_m5_dial: 391 | ... 392 | devices: 393 | covers: 394 | - entity: cover.my_cover1 395 | name: Cover 1 396 | ``` 397 | 398 | **entity** 399 | Specifies the Cover Entity ID from Home Assistant to be controlled. 400 | 401 | **name** 402 | The name of the entity displayed on the screen. 403 | 404 | ## modes (optional) 405 | #### position_mode 406 | The Position Mode allows controlling the position of the cover entity. 407 | Position can be controlled both via the rotary encoder and touch. 408 | 409 | **Code:** 410 | ``` 411 | shys_m5_dial: 412 | ... 413 | devices: 414 | covers: 415 | - entity: cover.my_cover1 416 | name: Cover 1 417 | modes: 418 | position_mode: 419 | rotary_step_width: 5 420 | ``` 421 | 422 | **rotary_step_width (optional)** 423 | Specifies the general step width by which the value changes per step when using the rotary encoder. 424 | The value set here overrides the general setting and applies only to the position mode of this cover entity. 425 | *Valid values: 1 - 100* 426 | 427 |   428 | 429 | ------ 430 | 431 | ## **FANS** 432 | Under "devices - cover" all fan entities are specified. 433 | 434 | **Code:** 435 | ``` 436 | shys_m5_dial: 437 | ... 438 | devices: 439 | fans: 440 | - entity: fan.my_fan1 441 | name: Fan 1 442 | ``` 443 | 444 | **entity** 445 | Specifies the Fan Entity ID from Home Assistant to be controlled. 446 | 447 | **name** 448 | The name of the fan displayed on the screen. 449 | 450 | ## modes (optional) 451 | #### speed_mode 452 | The Speed Mode allows controlling the speed of the fan entity. 453 | Speed can be controlled both via the rotary encoder and touch. 454 | A press on the button turns the fan on/off. 455 | 456 | **Code:** 457 | ``` 458 | shys_m5_dial: 459 | ... 460 | devices: 461 | fans: 462 | - entity: fan.my_fan1 463 | name: Fan 1 464 | modes: 465 | speed_mode: 466 | changeable_direction: true 467 | rotary_step_width: 10 468 | ``` 469 | 470 | **changeable_direction (optional)** *(Default: false)* 471 | Indicates whether the fan has two directions of rotation and can therefore be switched between forward and reverse. 472 | False = Only one direction of rotation. Turning right increases the speed, turning left reduces the speed. 473 | True = Two directions of rotation. Turning right increases the speed for forward, turning left reduces the speed for forward. 474 | However, you can now turn past 0 and thus change the direction of rotation. 475 | If the fan is running in reverse, turning left increases the speed and turning right reduces it. 476 | *Valid values: true / false* 477 | 478 | **rotary_step_width (optional)** 479 | Specifies the general step width by which the value changes per step when using the rotary encoder. 480 | The value set here overrides the general setting and applies only to the speed mode of this fan entity. 481 | *Valid values: 1 - 100* 482 | 483 |   484 | 485 | ------ 486 | 487 | ## **MEDIA PLAYER** 488 | Under "devices - media_player" all MediaPlayer entities are specified. 489 | 490 | **Code:** 491 | ``` 492 | shys_m5_dial: 493 | ... 494 | devices: 495 | media_player: 496 | - entity: media_player.my_player1 497 | name: MediaPlayer 1 498 | ``` 499 | 500 | **entity** 501 | Specifies the MediaPlayer Entity ID from Home Assistant to be controlled. 502 | 503 | **name** 504 | The name of the MediaPlayer displayed on the screen. 505 | 506 | ## modes (optional) 507 | #### play_mode 508 | The Play Mode allows adjusting the volume via the rotary encoder, as well as starting/pausing playback or skipping to the next or previous track via touch. 509 | A press on the button toggles between play and pause. 510 | 511 | **Code:** 512 | ``` 513 | shys_m5_dial: 514 | ... 515 | devices: 516 | media_player: 517 | - entity: media_player.my_player1 518 | name: MediaPlayer 1 519 | modes: 520 | play_mode: 521 | rotary_step_width: 10 522 | ``` 523 | 524 | **rotary_step_width (optional)** 525 | Specifies the general step width by which the value changes per step when using the rotary encoder. 526 | The value set here overrides the general setting and applies only to the play mode of this MediaPlayer entity. 527 | *Valid values: 1 - 100* 528 | 529 | 530 |   531 | 532 | #### source_mode 533 | The Source Mode allows selecting a source from the stored "Sources" using the rotary encoder for playback. 534 | A press on the button starts playback of the selected source. 535 | 536 | **Code:** 537 | ``` 538 | shys_m5_dial: 539 | ... 540 | devices: 541 | media_player: 542 | - entity: media_player.my_player1 543 | name: MediaPlayer 1 544 | modes: 545 | play_mode: 546 | rotary_step_width: 10 547 | source_mode: 548 | rotary_step_width: 1 549 | sources: 550 | - name: 1Live 551 | content_id: 1Live 552 | content_type: TUNEIN 553 | - name: WDR2 554 | content_id: "http://wdr-wdr2-bergischesland.icecast.wdr.de/wdr/wdr2/bergischesland/mp3/128/stream.mp3" 555 | content_type: custom 556 | ``` 557 | 558 | **rotary_step_width (optional)** *(Default: 1)* 559 | Specifies the general step width by which the value changes per step when using the rotary encoder. 560 | For source_mode of media_player, 1 is the default, regardless of what is set as the general standard in the component. 561 | *Valid values: 1 - 100* 562 | 563 | **sources (optional)** 564 | A list of audio sources available for playback selection. 565 | Entries must always include the attributes "name", "content_id", and "content_type"! 566 | 567 | Example for Amazon Echo (Alexa): 568 | ``` 569 | - name: 1Live 570 | content_id: 1Live 571 | content_type: TUNEIN 572 | 573 | - name: Metallica 574 | content_id: metallica 575 | content_type: AMAZON_MUSIC 576 | 577 | - name: Nothing else matters 578 | content_id: play metallica nothing else matters 579 | content_type: custom 580 | ``` 581 | 582 | Example for DLNA Player: 583 | ``` 584 | - name: 1Live 585 | content_id: "https://wdr-1live-live.icecastssl.wdr.de/wdr/1live/live/mp3/128/stream.mp3" 586 | content_type: custom 587 | ``` 588 | 589 |   590 | 591 | ------ 592 | 593 | ## **LOCKS** 594 | Under "devices - lock" all lock entities are listed. 595 | 596 | **Code:** 597 | ``` 598 | shys_m5_dial: 599 | ... 600 | devices: 601 | lock: 602 | - entity: lock.my_lock1 603 | name: Lock 1 604 | ``` 605 | 606 | **entity** 607 | Specifies the lock entity ID from Home Assistant to be controlled. 608 | 609 | **name** 610 | The name of the lock displayed on the screen. 611 | 612 | ## modes (optional) 613 | #### lock_mode 614 | With the lock mode, the state of the lock entity can be controlled. 615 | The lock can be unlocked, locked, and opened using the rotary encoder. 616 | A press on the button usually unlocks or locks the lock depending on the current state. 617 | However, if *open_on_button* is set to *true*, pressing the button will open the lock directly instead of just unlocking it. 618 | 619 | **Code:** 620 | ``` 621 | shys_m5_dial: 622 | ... 623 | devices: 624 | lock: 625 | - entity: lock.my_lock1 626 | name: Lock 1 627 | modes: 628 | lock_mode: 629 | open_on_button: true 630 | rotary_step_width: 10 631 | ``` 632 | 633 | **open_on_button (optional)** *(Default: false)* 634 | Setting to true will directly open the lock when pressing the button instead of just unlocking it. 635 | If the parameter is set to false (or not specified), the lock will only be unlocked but not fully opened. 636 | *Valid values: true / false* 637 | 638 | **rotary_step_width (optional)** *(Default: 1)* 639 | Specifies the general step width by which the value changes when using the rotary encoder per step. 640 | For lock mode of lock, 1 is the default value, regardless of what is set as the general standard in the component. 641 | If a step width of 2 is specified here, turning the encoder will not only unlock the lock but also automatically fully open it. 642 | *Valid values: 1 - 2* 643 | 644 | 645 |   646 | 647 |   648 | 649 | ------ 650 | ------ 651 | 652 | ## Advanced 653 | The following attributes are present, but generally should not need to be changed. 654 | 655 | **send_value_delay** 656 | Specifies the delay in milliseconds to wait after a value change before transmitting the change to Home Assistant. 657 | This is especially important when using the rotary encoder to avoid generating unnecessary API calls. *(Default: 1200)* 658 | 659 | **send_value_lock** 660 | Specifies how long to wait after an API call before the next API call can be made. *(Default: 3000)* 661 | 662 | 663 | ------ 664 | ------ 665 | 666 |   667 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /components/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /components/shys_m5_dial/__init__.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import esphome.codegen as cg 4 | import esphome.config_validation as cv 5 | from esphome.const import CONF_ID, CONF_NAME 6 | 7 | # LIMITS 8 | MAX_DEVICES = 50 9 | 10 | 11 | # PARAMETER 12 | CONF_DEVICES = "devices" 13 | CONF_DEVICE_ENTRY_ID = "entity" 14 | CONF_DEVICE_ENTRY_NAME = "name" 15 | CONF_DEVICE_MODES = "modes" 16 | 17 | CONF_SCREEN_OFF_TIME = "screen_off_time" 18 | CONF_LONG_PRESS_DURATION = "long_press_duration" 19 | CONF_SEND_VALUE_DELAY = "send_value_delay" 20 | CONF_SEND_VALUE_LOCK = "send_value_lock" 21 | CONF_ROTARY_STEP_WIDTH = "rotary_step_width" 22 | CONF_FONT = "font" 23 | CONF_FONT_FACTOR = "font_factor" 24 | 25 | # ALLGEMEINE MODE PARAMETER 26 | CONF_DEVICE_MODE_ENABLE = "enable" 27 | 28 | # LIGHTS 29 | CONF_DEVICE_LIGHTS = "lights" 30 | CONF_DEVICE_LIGHT_RGB_MODE = "rgb_mode" 31 | CONF_DEVICE_LIGHT_DIMM_MODE = "dimm_mode" 32 | CONF_DEVICE_LIGHT_WHITE_MODE = "white_mode" 33 | 34 | # WHITE-MODE PARAMETER 35 | CONF_DEVICE_MODE_WHITE_MIN_KELVIN = "min_kelvin" 36 | CONF_DEVICE_MODE_WHITE_MAX_KELVIN = "max_kelvin" 37 | 38 | 39 | # CLIMATES 40 | CONF_DEVICE_CLIMATES = "climates" 41 | CONF_DEVICE_CLIMATE_TEMP_MODE = "temp_mode" 42 | 43 | # TEMP-MODE PARAMETER 44 | CONF_DEVICE_MODE_TEMP_MIN_TEMP = "min_temperature" 45 | CONF_DEVICE_MODE_TEMP_MAX_TEMP = "max_temperature" 46 | 47 | 48 | # COVER 49 | CONF_DEVICE_COVER = "covers" 50 | CONF_DEVICE_COVER_POSITION_MODE = "position_mode" 51 | 52 | 53 | # SWITCH 54 | CONF_DEVICE_SWITCH = "switches" 55 | 56 | 57 | # FAN 58 | CONF_DEVICE_FAN = "fans" 59 | CONF_DEVICE_FAN_SPEED_MODE = "speed_mode" 60 | CONF_CHANGEABLE_DIRECTION = "changeable_direction" 61 | 62 | 63 | # MEDIA PLAYER 64 | CONF_DEVICE_MEDIA_PLAYER = "media_player" 65 | CONF_DEVICE_MEDIA_PLAYER_PLAY_MODE = "play_mode" 66 | CONF_DEVICE_MEDIA_PLAYER_SOURCE_MODE = "source_mode" 67 | CONF_MEDIA_PLAYER_SOURCES = "sources" 68 | CONF_CONTENT_ID = "content_id" 69 | CONF_CONTENT_TYPE = "content_type" 70 | 71 | # LOCK 72 | CONF_DEVICE_LOCK = "lock" 73 | CONF_DEVICE_LOCK_MODE = "lock_mode" 74 | CONF_DEVICE_LOCK_OPEN_ON_BUTTON = "open_on_button" 75 | 76 | 77 | # DEFAULTS 78 | DEFAULT_NAME = "M5 Stack Dial" 79 | DEFAULT_SCREEN_OFF_TIME = 30000 80 | DEFAULT_LONG_PRESS_DURATION = 1200 81 | DEFAULT_SEND_VALUE_DELAY = 1200 82 | DEFAULT_SEND_VALUE_LOCK = 3000 83 | DEFAULT_ROTARY_STEP_WIDTH = 10 84 | DEFAULT_FONT = "FreeSans12pt7b" 85 | DEFAULT_FONT_FACTOR = 1 86 | DEFAULT_WHITE_MIN_KELVIN = 2000 87 | DEFAULT_WHITE_MAX_KELVIN = 6500 88 | DEFAULT_WHITE_MIN_TEMP = 4 89 | DEFAULT_WHITE_MAX_TEMP = 30 90 | DEFAULT_CLIMATE_ROTARY_STEP_WIDTH = 1 91 | DEFAULT_MEDIA_PLAYER_ROTARY_STEP_WIDTH = 1 92 | DEFAULT_LOCK_ROTARY_STEP_WIDTH = 1 93 | 94 | # ------------------------------------------------------ 95 | 96 | shys_m5_dial_ns = cg.esphome_ns.namespace('shys_m5_dial') 97 | ShysM5Dial = shys_m5_dial_ns.class_('ShysM5Dial', cg.Component) 98 | 99 | 100 | CONFIG_SCHEMA = cv.Schema({ 101 | cv.GenerateID(): cv.declare_id(ShysM5Dial), 102 | cv.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, 103 | 104 | cv.Optional(CONF_SCREEN_OFF_TIME, default=DEFAULT_SCREEN_OFF_TIME): cv.int_range(0, 999999), 105 | cv.Optional(CONF_LONG_PRESS_DURATION, default=DEFAULT_LONG_PRESS_DURATION): cv.int_range(0, 5000), 106 | cv.Optional(CONF_SEND_VALUE_DELAY, default=DEFAULT_SEND_VALUE_DELAY): cv.int_range(0, 999999), 107 | cv.Optional(CONF_SEND_VALUE_LOCK, default=DEFAULT_SEND_VALUE_LOCK): cv.int_range(0, 999999), 108 | cv.Optional(CONF_ROTARY_STEP_WIDTH, default=DEFAULT_ROTARY_STEP_WIDTH): cv.int_range(0, 100), 109 | cv.Optional(CONF_FONT, default=DEFAULT_FONT): cv.string, 110 | cv.Optional(CONF_FONT_FACTOR, default=DEFAULT_FONT_FACTOR): cv.float_range(0.1, 10.0), 111 | 112 | cv.Optional(CONF_DEVICES, default=dict()): cv.All(dict({ 113 | 114 | cv.Optional(CONF_DEVICE_LIGHTS, default=[]): cv.All([dict({ 115 | cv.Required(CONF_DEVICE_ENTRY_ID): cv.string, 116 | cv.Required(CONF_DEVICE_ENTRY_NAME): cv.string, 117 | 118 | cv.Optional(CONF_DEVICE_MODES, default=dict()): cv.All(dict({ 119 | cv.Optional(CONF_DEVICE_LIGHT_RGB_MODE, default=dict()): cv.All(dict({ 120 | cv.Optional(CONF_DEVICE_MODE_ENABLE, default=False): cv.boolean, 121 | cv.Optional(CONF_ROTARY_STEP_WIDTH): cv.int_range(1, 100) 122 | })), 123 | 124 | cv.Optional(CONF_DEVICE_LIGHT_DIMM_MODE, default=dict()): cv.All(dict({ 125 | cv.Optional(CONF_DEVICE_MODE_ENABLE, default=False): cv.boolean, 126 | cv.Optional(CONF_ROTARY_STEP_WIDTH): cv.int_range(1, 100) 127 | })), 128 | 129 | cv.Optional(CONF_DEVICE_LIGHT_WHITE_MODE, default=dict()): cv.All(dict({ 130 | cv.Optional(CONF_DEVICE_MODE_ENABLE, default=False): cv.boolean, 131 | cv.Optional(CONF_ROTARY_STEP_WIDTH): cv.int_range(1, 500), 132 | cv.Optional(CONF_DEVICE_MODE_WHITE_MIN_KELVIN, default=DEFAULT_WHITE_MIN_KELVIN): cv.int_range(1000, 10000), 133 | cv.Optional(CONF_DEVICE_MODE_WHITE_MAX_KELVIN, default=DEFAULT_WHITE_MAX_KELVIN): cv.int_range(1000, 10000) 134 | })) 135 | })) 136 | })]), 137 | 138 | 139 | cv.Optional(CONF_DEVICE_CLIMATES, default=[]): cv.All([dict({ 140 | cv.Required(CONF_DEVICE_ENTRY_ID): cv.string, 141 | cv.Required(CONF_DEVICE_ENTRY_NAME): cv.string, 142 | 143 | cv.Optional(CONF_DEVICE_MODES, default=dict()): cv.All(dict({ 144 | cv.Optional(CONF_DEVICE_CLIMATE_TEMP_MODE, default=dict()): cv.All(dict({ 145 | cv.Optional(CONF_ROTARY_STEP_WIDTH, default=DEFAULT_CLIMATE_ROTARY_STEP_WIDTH): cv.int_range(1, 500), 146 | cv.Optional(CONF_DEVICE_MODE_TEMP_MIN_TEMP, default=DEFAULT_WHITE_MIN_TEMP): cv.int_range(0, 500), 147 | cv.Optional(CONF_DEVICE_MODE_TEMP_MAX_TEMP, default=DEFAULT_WHITE_MAX_TEMP): cv.int_range(0, 500) 148 | })) 149 | })) 150 | })]), 151 | 152 | 153 | cv.Optional(CONF_DEVICE_COVER, default=[]): cv.All([dict({ 154 | cv.Required(CONF_DEVICE_ENTRY_ID): cv.string, 155 | cv.Required(CONF_DEVICE_ENTRY_NAME): cv.string, 156 | 157 | cv.Optional(CONF_DEVICE_MODES, default=dict()): cv.All(dict({ 158 | cv.Optional(CONF_DEVICE_COVER_POSITION_MODE, default=dict()): cv.All(dict({ 159 | cv.Optional(CONF_ROTARY_STEP_WIDTH): cv.int_range(1, 500), 160 | })) 161 | })) 162 | })]), 163 | 164 | 165 | cv.Optional(CONF_DEVICE_SWITCH, default=[]): cv.All([dict({ 166 | cv.Required(CONF_DEVICE_ENTRY_ID): cv.string, 167 | cv.Required(CONF_DEVICE_ENTRY_NAME): cv.string 168 | })]), 169 | 170 | 171 | cv.Optional(CONF_DEVICE_FAN, default=[]): cv.All([dict({ 172 | cv.Required(CONF_DEVICE_ENTRY_ID): cv.string, 173 | cv.Required(CONF_DEVICE_ENTRY_NAME): cv.string, 174 | 175 | cv.Optional(CONF_DEVICE_MODES, default=dict()): cv.All(dict({ 176 | cv.Optional(CONF_DEVICE_FAN_SPEED_MODE, default=dict()): cv.All(dict({ 177 | cv.Optional(CONF_ROTARY_STEP_WIDTH): cv.int_range(1, 100), 178 | cv.Optional(CONF_CHANGEABLE_DIRECTION, default=False): cv.boolean 179 | })) 180 | })) 181 | })]), 182 | 183 | 184 | cv.Optional(CONF_DEVICE_MEDIA_PLAYER, default=[]): cv.All([dict({ 185 | cv.Required(CONF_DEVICE_ENTRY_ID): cv.string, 186 | cv.Required(CONF_DEVICE_ENTRY_NAME): cv.string, 187 | 188 | cv.Optional(CONF_DEVICE_MODES, default=dict()): cv.All(dict({ 189 | cv.Optional(CONF_DEVICE_MEDIA_PLAYER_PLAY_MODE , default=dict()): cv.All(dict({ 190 | cv.Optional(CONF_ROTARY_STEP_WIDTH): cv.int_range(1, 100) 191 | })), 192 | 193 | cv.Optional(CONF_DEVICE_MEDIA_PLAYER_SOURCE_MODE , default=dict()): cv.All(dict({ 194 | cv.Optional(CONF_ROTARY_STEP_WIDTH, default=DEFAULT_MEDIA_PLAYER_ROTARY_STEP_WIDTH): cv.int_range(1, 100), 195 | cv.Optional(CONF_MEDIA_PLAYER_SOURCES, default=[]): cv.All([dict({ 196 | cv.Required(CONF_NAME): cv.string, 197 | cv.Required(CONF_CONTENT_ID): cv.string, 198 | cv.Required(CONF_CONTENT_TYPE): cv.string, 199 | })]) 200 | })), 201 | })) 202 | })]), 203 | 204 | cv.Optional(CONF_DEVICE_LOCK, default=[]): cv.All([dict({ 205 | cv.Required(CONF_DEVICE_ENTRY_ID): cv.string, 206 | cv.Required(CONF_DEVICE_ENTRY_NAME): cv.string, 207 | 208 | cv.Optional(CONF_DEVICE_MODES, default=dict()): cv.All(dict({ 209 | cv.Optional(CONF_DEVICE_LOCK_MODE, default=dict()): cv.All(dict({ 210 | cv.Optional(CONF_ROTARY_STEP_WIDTH, default=DEFAULT_LOCK_ROTARY_STEP_WIDTH): cv.int_range(1, 100), 211 | cv.Optional(CONF_DEVICE_LOCK_OPEN_ON_BUTTON, default=False): cv.boolean 212 | })) 213 | })) 214 | })]) 215 | 216 | 217 | 218 | })) 219 | 220 | 221 | }).extend(cv.COMPONENT_SCHEMA) 222 | 223 | 224 | def to_code(config): 225 | var = cg.new_Pvariable(config[CONF_ID]) 226 | yield cg.register_component(var, config) 227 | 228 | if CONF_SCREEN_OFF_TIME in config: 229 | screenOffTime = config[CONF_SCREEN_OFF_TIME] 230 | cg.add(var.setScreenOffTime(screenOffTime)) 231 | 232 | if CONF_LONG_PRESS_DURATION in config: 233 | longPressDuration = config[CONF_LONG_PRESS_DURATION] 234 | cg.add(var.setLongPressDuration(longPressDuration)) 235 | 236 | if CONF_SEND_VALUE_DELAY in config: 237 | apiSendDelay = config[CONF_SEND_VALUE_DELAY] 238 | cg.add(var.setApiSendDelay(apiSendDelay)) 239 | 240 | if CONF_SEND_VALUE_LOCK in config: 241 | apiSendLock = config[CONF_SEND_VALUE_LOCK] 242 | cg.add(var.setApiSendLock(apiSendLock)) 243 | 244 | if CONF_ROTARY_STEP_WIDTH in config: 245 | rotaryStepWidth = config[CONF_ROTARY_STEP_WIDTH] 246 | cg.add(var.setRotaryStepWidth(rotaryStepWidth)) 247 | 248 | if CONF_FONT in config: 249 | fontname = config[CONF_FONT] 250 | cg.add(var.setFontName(fontname)) 251 | 252 | if CONF_FONT_FACTOR in config: 253 | fontfactor = config[CONF_FONT_FACTOR] 254 | cg.add(var.setFontFactor(fontfactor)) 255 | 256 | if CONF_DEVICES in config: 257 | confDevices = config[CONF_DEVICES] 258 | 259 | if CONF_DEVICE_LIGHTS in confDevices: 260 | confLights = confDevices[CONF_DEVICE_LIGHTS] 261 | for lightEntry in confLights: 262 | cg.add(var.addLight(lightEntry[CONF_DEVICE_ENTRY_ID], 263 | lightEntry[CONF_DEVICE_ENTRY_NAME], 264 | json.dumps(lightEntry[CONF_DEVICE_MODES]) 265 | )) 266 | 267 | if CONF_DEVICE_CLIMATES in confDevices: 268 | confClimates = confDevices[CONF_DEVICE_CLIMATES] 269 | for climateEntry in confClimates: 270 | cg.add(var.addClimate(climateEntry[CONF_DEVICE_ENTRY_ID], 271 | climateEntry[CONF_DEVICE_ENTRY_NAME], 272 | json.dumps(climateEntry[CONF_DEVICE_MODES]) 273 | )) 274 | 275 | if CONF_DEVICE_COVER in confDevices: 276 | confCover = confDevices[CONF_DEVICE_COVER] 277 | for coverEntry in confCover: 278 | cg.add(var.addCover(coverEntry[CONF_DEVICE_ENTRY_ID], 279 | coverEntry[CONF_DEVICE_ENTRY_NAME], 280 | json.dumps(coverEntry[CONF_DEVICE_MODES]) 281 | )) 282 | 283 | 284 | if CONF_DEVICE_SWITCH in confDevices: 285 | confSwitch = confDevices[CONF_DEVICE_SWITCH] 286 | for switchEntry in confSwitch: 287 | cg.add(var.addSwitch(switchEntry[CONF_DEVICE_ENTRY_ID], 288 | switchEntry[CONF_DEVICE_ENTRY_NAME], 289 | "{}") 290 | ) 291 | 292 | 293 | if CONF_DEVICE_FAN in confDevices: 294 | confFan = confDevices[CONF_DEVICE_FAN] 295 | for fanEntry in confFan: 296 | cg.add(var.addFan(fanEntry[CONF_DEVICE_ENTRY_ID], 297 | fanEntry[CONF_DEVICE_ENTRY_NAME], 298 | json.dumps(fanEntry[CONF_DEVICE_MODES])) 299 | ) 300 | 301 | 302 | if CONF_DEVICE_MEDIA_PLAYER in confDevices: 303 | confMediaPlayer = confDevices[CONF_DEVICE_MEDIA_PLAYER] 304 | for mediaPlayerEntry in confMediaPlayer: 305 | cg.add(var.addMediaPlayer(mediaPlayerEntry[CONF_DEVICE_ENTRY_ID], 306 | mediaPlayerEntry[CONF_DEVICE_ENTRY_NAME], 307 | json.dumps(mediaPlayerEntry[CONF_DEVICE_MODES])) 308 | ) 309 | 310 | 311 | if CONF_DEVICE_LOCK in confDevices: 312 | confLock = confDevices[CONF_DEVICE_LOCK] 313 | for lockEntry in confLock: 314 | cg.add(var.addLock(lockEntry[CONF_DEVICE_ENTRY_ID], 315 | lockEntry[CONF_DEVICE_ENTRY_NAME], 316 | json.dumps(lockEntry[CONF_DEVICE_MODES])) 317 | ) 318 | 319 | -------------------------------------------------------------------------------- /components/shys_m5_dial/globals.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include "M5Dial.h" 4 | 5 | #ifndef GLOBALS_H 6 | #define GLOBALS_H 7 | static const char *TOUCH_STATE_NONE = "none"; 8 | static const char *TOUCH_STATE_TOUCH = "touch"; 9 | static const char *TOUCH_STATE_TOUCH_END = "touch_end"; 10 | static const char *TOUCH_STATE_TOUCH_BEGIN = "touch_begin"; 11 | 12 | static const char *TOUCH_STATE_HOLD = "hold"; 13 | static const char *TOUCH_STATE_HOLD_END = "hold_end"; 14 | static const char *TOUCH_STATE_HOLD_BEGIN = "hold_begin"; 15 | 16 | static const char *TOUCH_STATE_FLICK = "flick"; 17 | static const char *TOUCH_STATE_FLICK_END = "flick_end"; 18 | static const char *TOUCH_STATE_FLICK_BEGIN = "flick_begin"; 19 | 20 | static const char *TOUCH_STATE_DRAG = "drag"; 21 | static const char *TOUCH_STATE_DRAG_END = "drag_end"; 22 | static const char *TOUCH_STATE_DRAG_BEGIN = "drag_begin"; 23 | 24 | static const char *TOUCH_STATE_TMP = "___"; 25 | 26 | static const char *TOUCH_STATE_NAME[16] = { 27 | TOUCH_STATE_NONE, TOUCH_STATE_TOUCH, TOUCH_STATE_TOUCH_END, TOUCH_STATE_TOUCH_BEGIN, 28 | TOUCH_STATE_TMP, TOUCH_STATE_HOLD, TOUCH_STATE_HOLD_END, TOUCH_STATE_HOLD_BEGIN, 29 | TOUCH_STATE_TMP, TOUCH_STATE_FLICK, TOUCH_STATE_FLICK_END, TOUCH_STATE_FLICK_BEGIN, 30 | TOUCH_STATE_TMP, TOUCH_STATE_DRAG, TOUCH_STATE_DRAG_END, TOUCH_STATE_DRAG_BEGIN }; 31 | 32 | static const char *TOUCH_SWIPE_NONE = "none"; 33 | static const char *TOUCH_SWIPE_UP = "up"; 34 | static const char *TOUCH_SWIPE_DOWN = "down"; 35 | static const char *TOUCH_SWIPE_LEFT = "left"; 36 | static const char *TOUCH_SWIPE_RIGHT = "right"; 37 | 38 | static const char *ROTARY_LEFT = "left"; 39 | static const char *ROTARY_RIGHT = "right"; 40 | 41 | static const char *BUTTON_SHORT = "short"; 42 | static const char *BUTTON_LONG = "long"; 43 | 44 | static const char *FAN_DIRECTION_FORWARD = "forward"; 45 | static const char *FAN_DIRECTION_REVERSE = "reverse"; 46 | #endif 47 | 48 | 49 | #ifndef GLOBAL_FONTS_H 50 | #define GLOBAL_FONTS_H 51 | static std::map FONT_MAP = { 52 | {"TomThumb", &fonts::TomThumb}, 53 | {"FreeMono9pt7b", &fonts::FreeMono9pt7b}, 54 | {"FreeMono12pt7b", &fonts::FreeMono12pt7b}, 55 | {"FreeMono18pt7b", &fonts::FreeMono18pt7b}, 56 | {"FreeMono24pt7b", &fonts::FreeMono24pt7b}, 57 | {"FreeMonoBold9pt7b", &fonts::FreeMonoBold9pt7b}, 58 | {"FreeMonoBold12pt7b", &fonts::FreeMonoBold12pt7b}, 59 | {"FreeMonoBold18pt7b", &fonts::FreeMonoBold18pt7b}, 60 | {"FreeMonoBold24pt7b", &fonts::FreeMonoBold24pt7b}, 61 | {"FreeMonoOblique9pt7b", &fonts::FreeMonoOblique9pt7b}, 62 | {"FreeMonoOblique12pt7b", &fonts::FreeMonoOblique12pt7b}, 63 | {"FreeMonoOblique18pt7b", &fonts::FreeMonoOblique18pt7b}, 64 | {"FreeMonoOblique24pt7b", &fonts::FreeMonoOblique24pt7b}, 65 | {"FreeMonoBoldOblique9pt7b", &fonts::FreeMonoBoldOblique9pt7b}, 66 | {"FreeMonoBoldOblique12pt7b", &fonts::FreeMonoBoldOblique12pt7b}, 67 | {"FreeMonoBoldOblique18pt7b", &fonts::FreeMonoBoldOblique18pt7b}, 68 | {"FreeMonoBoldOblique24pt7b", &fonts::FreeMonoBoldOblique24pt7b}, 69 | {"FreeSans9pt7b", &fonts::FreeSans9pt7b}, 70 | {"FreeSans12pt7b", &fonts::FreeSans12pt7b}, 71 | {"FreeSans18pt7b", &fonts::FreeSans18pt7b}, 72 | {"FreeSans24pt7b", &fonts::FreeSans24pt7b}, 73 | {"FreeSansBold9pt7b", &fonts::FreeSansBold9pt7b}, 74 | {"FreeSansBold12pt7b", &fonts::FreeSansBold12pt7b}, 75 | {"FreeSansBold18pt7b", &fonts::FreeSansBold18pt7b}, 76 | {"FreeSansBold24pt7b", &fonts::FreeSansBold24pt7b}, 77 | {"FreeSansOblique9pt7b", &fonts::FreeSansOblique9pt7b}, 78 | {"FreeSansOblique12pt7b", &fonts::FreeSansOblique12pt7b}, 79 | {"FreeSansOblique18pt7b", &fonts::FreeSansOblique18pt7b}, 80 | {"FreeSansOblique24pt7b", &fonts::FreeSansOblique24pt7b}, 81 | {"FreeSansBoldOblique9pt7b", &fonts::FreeSansBoldOblique9pt7b}, 82 | {"FreeSansBoldOblique12pt7b", &fonts::FreeSansBoldOblique12pt7b}, 83 | {"FreeSansBoldOblique18pt7b", &fonts::FreeSansBoldOblique18pt7b}, 84 | {"FreeSansBoldOblique24pt7b", &fonts::FreeSansBoldOblique24pt7b}, 85 | {"FreeSerif9pt7b", &fonts::FreeSerif9pt7b}, 86 | {"FreeSerif12pt7b", &fonts::FreeSerif12pt7b}, 87 | {"FreeSerif18pt7b", &fonts::FreeSerif18pt7b}, 88 | {"FreeSerif24pt7b", &fonts::FreeSerif24pt7b}, 89 | {"FreeSerifItalic9pt7b", &fonts::FreeSerifItalic9pt7b}, 90 | {"FreeSerifItalic12pt7b", &fonts::FreeSerifItalic12pt7b}, 91 | {"FreeSerifItalic18pt7b", &fonts::FreeSerifItalic18pt7b}, 92 | {"FreeSerifItalic24pt7b", &fonts::FreeSerifItalic24pt7b}, 93 | {"FreeSerifBold9pt7b", &fonts::FreeSerifBold9pt7b}, 94 | {"FreeSerifBold12pt7b", &fonts::FreeSerifBold12pt7b}, 95 | {"FreeSerifBold18pt7b", &fonts::FreeSerifBold18pt7b}, 96 | {"FreeSerifBold24pt7b", &fonts::FreeSerifBold24pt7b}, 97 | {"FreeSerifBoldItalic9pt7b", &fonts::FreeSerifBoldItalic9pt7b}, 98 | {"FreeSerifBoldItalic12pt7b", &fonts::FreeSerifBoldItalic12pt7b}, 99 | {"FreeSerifBoldItalic18pt7b", &fonts::FreeSerifBoldItalic18pt7b}, 100 | {"FreeSerifBoldItalic24pt7b", &fonts::FreeSerifBoldItalic24pt7b}, 101 | {"Orbitron_Light_24", &fonts::Orbitron_Light_24}, 102 | {"Orbitron_Light_32", &fonts::Orbitron_Light_32}, 103 | {"Roboto_Thin_24", &fonts::Roboto_Thin_24}, 104 | {"Satisfy_24", &fonts::Satisfy_24}, 105 | {"Yellowtail_32", &fonts::Yellowtail_32}, 106 | {"DejaVu9", &fonts::DejaVu9}, 107 | {"DejaVu12", &fonts::DejaVu12}, 108 | {"DejaVu18", &fonts::DejaVu18}, 109 | {"DejaVu24", &fonts::DejaVu24}, 110 | {"DejaVu40", &fonts::DejaVu40}, 111 | {"DejaVu56", &fonts::DejaVu56}, 112 | {"DejaVu72", &fonts::DejaVu72}, 113 | }; 114 | #endif 115 | 116 | 117 | #ifndef M_PI 118 | #define M_PI 3.1415926535 119 | #endif 120 | -------------------------------------------------------------------------------- /components/shys_m5_dial/ha_api.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "esphome.h" 3 | 4 | namespace esphome 5 | { 6 | namespace shys_m5_dial 7 | { 8 | class HaApi { 9 | protected: 10 | std::string colorValue; 11 | esphome::api::HomeassistantServiceResponse resp; 12 | esphome::api::HomeassistantServiceMap resp_kv; 13 | 14 | public: 15 | 16 | void updateEntity(const std::string& entity){ 17 | esphome::api::HomeassistantServiceResponse resp; 18 | esphome::api::HomeassistantServiceMap resp_kv; 19 | 20 | resp.service = "homeassistant.update_entity"; 21 | 22 | resp_kv.key = "entity_id"; 23 | resp_kv.value = entity.c_str(); 24 | resp.data.push_back(resp_kv); 25 | 26 | esphome::api::global_api_server->send_homeassistant_service_call(resp); 27 | 28 | ESP_LOGD("HA_API", "update Entity: %s", entity.c_str()); 29 | } 30 | 31 | // --------------------------------- 32 | // LIGHT 33 | // --------------------------------- 34 | void turnLightOn(const std::string& entity){ 35 | turnLightOn(entity, -1, -1); 36 | } 37 | 38 | void turnLightOn(const std::string& entity, int brightness){ 39 | turnLightOn(entity, brightness, -1); 40 | } 41 | 42 | void turnLightOn(const std::string& entity, int brightness, int color){ 43 | ESP_LOGD("HA_API", "light on values: %s (%i) Color: %i", entity.c_str(), brightness, color); 44 | 45 | resp.service = "light.turn_on"; 46 | 47 | resp_kv.key = "entity_id"; 48 | resp_kv.value = entity.c_str(); 49 | resp.data.push_back(resp_kv); 50 | 51 | if(brightness >= 0){ 52 | resp_kv.key = "brightness_pct"; 53 | resp_kv.value = String(brightness).c_str(); 54 | resp.data.push_back(resp_kv); 55 | } 56 | 57 | if(color >= 0){ 58 | colorValue = str_sprintf("{{(%d,100)|list}}", color); 59 | ESP_LOGD("HA_API", "light on: %s (%i) HS-Color: %s", entity.c_str(), brightness, colorValue.c_str()); 60 | 61 | resp_kv.key = "hs_color"; 62 | resp_kv.value = colorValue.c_str(); 63 | resp.data_template.push_back(resp_kv); 64 | 65 | ESP_LOGI("HA_API", "turn light on: %s (%i) HS-Color: %s", entity.c_str(), brightness, colorValue.c_str()); 66 | } else { 67 | ESP_LOGI("HA_API", "turn light on: %s (%i)", entity.c_str(), brightness); 68 | } 69 | 70 | esphome::api::global_api_server->send_homeassistant_service_call(resp); 71 | } 72 | 73 | 74 | void turnLightOnWhite(const std::string& entity, int kelvin){ 75 | ESP_LOGD("HA_API", "light on values: %s Kelvin: %i", entity.c_str(), kelvin); 76 | 77 | resp.service = "light.turn_on"; 78 | 79 | resp_kv.key = "entity_id"; 80 | resp_kv.value = entity.c_str(); 81 | resp.data.push_back(resp_kv); 82 | 83 | if(kelvin >= 0){ 84 | resp_kv.key = "kelvin"; 85 | resp_kv.value = String(kelvin).c_str(); 86 | resp.data_template.push_back(resp_kv); 87 | 88 | ESP_LOGI("HA_API", "turn light on: %s Kelvin: %s", entity.c_str(), colorValue.c_str()); 89 | } else { 90 | ESP_LOGI("HA_API", "turn light on: %s", entity.c_str()); 91 | } 92 | 93 | esphome::api::global_api_server->send_homeassistant_service_call(resp); 94 | } 95 | 96 | void turnLightOff(const std::string& entity){ 97 | esphome::api::HomeassistantServiceResponse resp; 98 | esphome::api::HomeassistantServiceMap resp_kv; 99 | 100 | resp.service = "light.turn_off"; 101 | 102 | resp_kv.key = "entity_id"; 103 | resp_kv.value = entity.c_str(); 104 | resp.data.push_back(resp_kv); 105 | 106 | esphome::api::global_api_server->send_homeassistant_service_call(resp); 107 | 108 | ESP_LOGI("HA_API", "turn light off: %s", entity.c_str()); 109 | } 110 | 111 | void toggleLight(const std::string& entity){ 112 | esphome::api::HomeassistantServiceResponse resp; 113 | esphome::api::HomeassistantServiceMap resp_kv; 114 | 115 | resp.service = "light.toggle"; 116 | 117 | resp_kv.key = "entity_id"; 118 | resp_kv.value = entity.c_str(); 119 | resp.data.push_back(resp_kv); 120 | 121 | esphome::api::global_api_server->send_homeassistant_service_call(resp); 122 | 123 | ESP_LOGI("HA_API", "toggle light: %s", entity.c_str()); 124 | } 125 | 126 | // --------------------------------- 127 | // CLIMATE 128 | // --------------------------------- 129 | 130 | void turnClimateOn(const std::string& entity) { 131 | esphome::api::HomeassistantServiceResponse resp; 132 | esphome::api::HomeassistantServiceMap resp_kv; 133 | 134 | resp.service = "climate.turn_on"; 135 | 136 | resp_kv.key = "entity_id"; 137 | resp_kv.value = entity.c_str(); 138 | resp.data.push_back(resp_kv); 139 | 140 | esphome::api::global_api_server->send_homeassistant_service_call(resp); 141 | 142 | ESP_LOGI("HA_API", "turn on climate: %s", entity.c_str()); 143 | } 144 | 145 | void turnClimateOff(const std::string& entity) { 146 | esphome::api::HomeassistantServiceResponse resp; 147 | esphome::api::HomeassistantServiceMap resp_kv; 148 | 149 | resp.service = "climate.turn_off"; 150 | 151 | resp_kv.key = "entity_id"; 152 | resp_kv.value = entity.c_str(); 153 | resp.data.push_back(resp_kv); 154 | 155 | esphome::api::global_api_server->send_homeassistant_service_call(resp); 156 | 157 | ESP_LOGI("HA_API", "turn off climate: %s", entity.c_str()); 158 | } 159 | 160 | void setClimateTemperature(const std::string& entity, int temperature) { 161 | esphome::api::HomeassistantServiceResponse resp; 162 | esphome::api::HomeassistantServiceMap resp_kv; 163 | 164 | resp.service = "climate.set_temperature"; 165 | 166 | resp_kv.key = "entity_id"; 167 | resp_kv.value = entity.c_str(); 168 | resp.data.push_back(resp_kv); 169 | 170 | if(temperature >= 0){ 171 | resp_kv.key = "temperature"; 172 | resp_kv.value = String(temperature).c_str(); 173 | resp.data.push_back(resp_kv); 174 | } 175 | 176 | esphome::api::global_api_server->send_homeassistant_service_call(resp); 177 | 178 | ESP_LOGI("HA_API", "set temperature: %i for %s", temperature, entity.c_str()); 179 | } 180 | 181 | 182 | 183 | // --------------------------------- 184 | // COVER 185 | // --------------------------------- 186 | void setCoverPosition(const std::string& entity, int position) { 187 | esphome::api::HomeassistantServiceResponse resp; 188 | esphome::api::HomeassistantServiceMap resp_kv; 189 | 190 | resp.service = "cover.set_cover_position"; 191 | 192 | resp_kv.key = "entity_id"; 193 | resp_kv.value = entity.c_str(); 194 | resp.data.push_back(resp_kv); 195 | 196 | if(position >= 0){ 197 | resp_kv.key = "position"; 198 | resp_kv.value = String(position).c_str(); 199 | resp.data.push_back(resp_kv); 200 | } 201 | 202 | esphome::api::global_api_server->send_homeassistant_service_call(resp); 203 | 204 | ESP_LOGI("HA_API", "cover set position %i for %s", position, entity.c_str()); 205 | } 206 | 207 | 208 | 209 | 210 | // --------------------------------- 211 | // SWITCH 212 | // --------------------------------- 213 | void turnSwitchOn(const std::string& entity){ 214 | esphome::api::HomeassistantServiceResponse resp; 215 | esphome::api::HomeassistantServiceMap resp_kv; 216 | 217 | resp.service = "switch.turn_on"; 218 | 219 | resp_kv.key = "entity_id"; 220 | resp_kv.value = entity.c_str(); 221 | resp.data.push_back(resp_kv); 222 | 223 | esphome::api::global_api_server->send_homeassistant_service_call(resp); 224 | 225 | ESP_LOGI("HA_API", "switch turn on: %s", entity.c_str()); 226 | } 227 | 228 | void turnSwitchOff(const std::string& entity){ 229 | esphome::api::HomeassistantServiceResponse resp; 230 | esphome::api::HomeassistantServiceMap resp_kv; 231 | 232 | resp.service = "switch.turn_off"; 233 | 234 | resp_kv.key = "entity_id"; 235 | resp_kv.value = entity.c_str(); 236 | resp.data.push_back(resp_kv); 237 | 238 | esphome::api::global_api_server->send_homeassistant_service_call(resp); 239 | 240 | ESP_LOGI("HA_API", "switch turn off: %s", entity.c_str()); 241 | } 242 | 243 | void toggleSwitch(const std::string& entity){ 244 | esphome::api::HomeassistantServiceResponse resp; 245 | esphome::api::HomeassistantServiceMap resp_kv; 246 | 247 | resp.service = "switch.toggle"; 248 | 249 | resp_kv.key = "entity_id"; 250 | resp_kv.value = entity.c_str(); 251 | resp.data.push_back(resp_kv); 252 | 253 | esphome::api::global_api_server->send_homeassistant_service_call(resp); 254 | 255 | ESP_LOGI("HA_API", "switch toggle: %s", entity.c_str()); 256 | } 257 | 258 | 259 | 260 | 261 | // --------------------------------- 262 | // FAN 263 | // --------------------------------- 264 | void turnFanOn(const std::string& entity){ 265 | esphome::api::HomeassistantServiceResponse resp; 266 | esphome::api::HomeassistantServiceMap resp_kv; 267 | 268 | resp.service = "fan.turn_on"; 269 | 270 | resp_kv.key = "entity_id"; 271 | resp_kv.value = entity.c_str(); 272 | resp.data.push_back(resp_kv); 273 | 274 | esphome::api::global_api_server->send_homeassistant_service_call(resp); 275 | 276 | ESP_LOGI("HA_API", "fan turn on: %s", entity.c_str()); 277 | } 278 | 279 | void turnFanOff(const std::string& entity){ 280 | esphome::api::HomeassistantServiceResponse resp; 281 | esphome::api::HomeassistantServiceMap resp_kv; 282 | 283 | resp.service = "fan.turn_off"; 284 | 285 | resp_kv.key = "entity_id"; 286 | resp_kv.value = entity.c_str(); 287 | resp.data.push_back(resp_kv); 288 | 289 | esphome::api::global_api_server->send_homeassistant_service_call(resp); 290 | 291 | ESP_LOGI("HA_API", "fan turn off: %s", entity.c_str()); 292 | } 293 | 294 | void toggleFan(const std::string& entity){ 295 | esphome::api::HomeassistantServiceResponse resp; 296 | esphome::api::HomeassistantServiceMap resp_kv; 297 | 298 | resp.service = "fan.toggle"; 299 | 300 | resp_kv.key = "entity_id"; 301 | resp_kv.value = entity.c_str(); 302 | resp.data.push_back(resp_kv); 303 | 304 | esphome::api::global_api_server->send_homeassistant_service_call(resp); 305 | 306 | ESP_LOGI("HA_API", "fan toggle: %s", entity.c_str()); 307 | } 308 | 309 | void setFanDirection(const std::string& entity, const char* direction){ 310 | esphome::api::HomeassistantServiceResponse resp; 311 | esphome::api::HomeassistantServiceMap resp_kv; 312 | 313 | resp.service = "fan.set_direction"; 314 | 315 | resp_kv.key = "entity_id"; 316 | resp_kv.value = entity.c_str(); 317 | resp.data.push_back(resp_kv); 318 | 319 | resp_kv.key = "direction"; 320 | resp_kv.value = direction; 321 | resp.data.push_back(resp_kv); 322 | 323 | esphome::api::global_api_server->send_homeassistant_service_call(resp); 324 | 325 | ESP_LOGI("HA_API", "fan direction: %s for %s", direction, entity.c_str()); 326 | } 327 | 328 | void setFanSpeed(const std::string& entity, int speed){ 329 | esphome::api::HomeassistantServiceResponse resp; 330 | esphome::api::HomeassistantServiceMap resp_kv; 331 | 332 | resp.service = "fan.set_percentage"; 333 | 334 | resp_kv.key = "entity_id"; 335 | resp_kv.value = entity.c_str(); 336 | resp.data.push_back(resp_kv); 337 | 338 | resp_kv.key = "percentage"; 339 | resp_kv.value = String(speed).c_str(); 340 | resp.data.push_back(resp_kv); 341 | 342 | esphome::api::global_api_server->send_homeassistant_service_call(resp); 343 | 344 | ESP_LOGI("HA_API", "fan speed: %s for %s", String(speed).c_str(), entity.c_str()); 345 | } 346 | 347 | 348 | 349 | // --------------------------------- 350 | // MEDIA PLAYER 351 | // --------------------------------- 352 | void setMediaPlayerVolume(const std::string& entity, int volume) { 353 | esphome::api::HomeassistantServiceResponse resp; 354 | esphome::api::HomeassistantServiceMap resp_kv; 355 | 356 | if(volume < 0 || volume > 100){ 357 | return; 358 | } 359 | 360 | resp.service = "media_player.volume_set"; 361 | 362 | resp_kv.key = "entity_id"; 363 | resp_kv.value = entity.c_str(); 364 | resp.data.push_back(resp_kv); 365 | 366 | resp_kv.key = "volume_level"; 367 | resp_kv.value = String((float)volume/100).c_str(); 368 | resp.data.push_back(resp_kv); 369 | 370 | esphome::api::global_api_server->send_homeassistant_service_call(resp); 371 | 372 | ESP_LOGI("HA_API", "media player set volume %i for %s", volume, entity.c_str()); 373 | } 374 | 375 | void stopMediaPlayer(const std::string& entity){ 376 | esphome::api::HomeassistantServiceResponse resp; 377 | esphome::api::HomeassistantServiceMap resp_kv; 378 | 379 | resp.service = "media_player.media_stop"; 380 | 381 | resp_kv.key = "entity_id"; 382 | resp_kv.value = entity.c_str(); 383 | resp.data.push_back(resp_kv); 384 | 385 | esphome::api::global_api_server->send_homeassistant_service_call(resp); 386 | 387 | ESP_LOGI("HA_API", "media player stop: %s", entity.c_str()); 388 | } 389 | 390 | void setNextTrackOnMediaPlayer(const std::string& entity){ 391 | esphome::api::HomeassistantServiceResponse resp; 392 | esphome::api::HomeassistantServiceMap resp_kv; 393 | 394 | resp.service = "media_player.media_next_track"; 395 | 396 | resp_kv.key = "entity_id"; 397 | resp_kv.value = entity.c_str(); 398 | resp.data.push_back(resp_kv); 399 | 400 | esphome::api::global_api_server->send_homeassistant_service_call(resp); 401 | 402 | ESP_LOGI("HA_API", "media player next track: %s", entity.c_str()); 403 | } 404 | 405 | void setPreviousTrackOnMediaPlayer(const std::string& entity){ 406 | esphome::api::HomeassistantServiceResponse resp; 407 | esphome::api::HomeassistantServiceMap resp_kv; 408 | 409 | resp.service = "media_player.media_previous_track"; 410 | 411 | resp_kv.key = "entity_id"; 412 | resp_kv.value = entity.c_str(); 413 | resp.data.push_back(resp_kv); 414 | 415 | esphome::api::global_api_server->send_homeassistant_service_call(resp); 416 | 417 | ESP_LOGI("HA_API", "media player previous track: %s", entity.c_str()); 418 | } 419 | 420 | void playPauseMediaPlayer(const std::string& entity){ 421 | esphome::api::HomeassistantServiceResponse resp; 422 | esphome::api::HomeassistantServiceMap resp_kv; 423 | 424 | resp.service = "media_player.media_play_pause"; 425 | 426 | resp_kv.key = "entity_id"; 427 | resp_kv.value = entity.c_str(); 428 | resp.data.push_back(resp_kv); 429 | 430 | esphome::api::global_api_server->send_homeassistant_service_call(resp); 431 | 432 | ESP_LOGI("HA_API", "play/pause media player: %s", entity.c_str()); 433 | } 434 | 435 | void playMediaOnMediaPlayer(const std::string& entity, const std::string& content_id, const std::string& content_type){ 436 | esphome::api::HomeassistantServiceResponse resp; 437 | esphome::api::HomeassistantServiceMap resp_kv; 438 | 439 | resp.service = "media_player.play_media"; 440 | 441 | resp_kv.key = "entity_id"; 442 | resp_kv.value = entity.c_str(); 443 | resp.data.push_back(resp_kv); 444 | 445 | 446 | resp_kv.key = "media_content_id"; 447 | resp_kv.value = content_id.c_str(); 448 | resp.data.push_back(resp_kv); 449 | 450 | resp_kv.key = "media_content_type"; 451 | resp_kv.value = content_type.c_str(); 452 | resp.data.push_back(resp_kv); 453 | 454 | esphome::api::global_api_server->send_homeassistant_service_call(resp); 455 | 456 | ESP_LOGI("HA_API", "play media %s on player: %s", content_id.c_str(), entity.c_str()); 457 | } 458 | 459 | void refreshMediaPlayer(const std::string& entity){ 460 | esphome::api::HomeassistantServiceResponse resp; 461 | esphome::api::HomeassistantServiceMap resp_kv; 462 | 463 | resp.service = "homeassistant.update_entity"; 464 | 465 | resp_kv.key = "entity_id"; 466 | resp_kv.value = entity.c_str(); 467 | resp.data.push_back(resp_kv); 468 | 469 | esphome::api::global_api_server->send_homeassistant_service_call(resp); 470 | 471 | ESP_LOGI("HA_API", "refresh media player: %s", entity.c_str()); 472 | } 473 | 474 | 475 | 476 | 477 | // --------------------------------- 478 | // LOCK 479 | // --------------------------------- 480 | void openLock(const std::string& entity) { 481 | esphome::api::HomeassistantServiceResponse resp; 482 | esphome::api::HomeassistantServiceMap resp_kv; 483 | 484 | resp.service = "lock.open"; 485 | 486 | resp_kv.key = "entity_id"; 487 | resp_kv.value = entity.c_str(); 488 | resp.data.push_back(resp_kv); 489 | 490 | esphome::api::global_api_server->send_homeassistant_service_call(resp); 491 | ESP_LOGI("HA_API", "open Lock %s", entity.c_str()); 492 | } 493 | 494 | void lockLock(const std::string& entity) { 495 | esphome::api::HomeassistantServiceResponse resp; 496 | esphome::api::HomeassistantServiceMap resp_kv; 497 | 498 | resp.service = "lock.lock"; 499 | 500 | resp_kv.key = "entity_id"; 501 | resp_kv.value = entity.c_str(); 502 | resp.data.push_back(resp_kv); 503 | 504 | esphome::api::global_api_server->send_homeassistant_service_call(resp); 505 | ESP_LOGI("HA_API", "lock Lock %s", entity.c_str()); 506 | } 507 | 508 | void unlockLock(const std::string& entity) { 509 | esphome::api::HomeassistantServiceResponse resp; 510 | esphome::api::HomeassistantServiceMap resp_kv; 511 | 512 | resp.service = "lock.unlock"; 513 | 514 | resp_kv.key = "entity_id"; 515 | resp_kv.value = entity.c_str(); 516 | resp.data.push_back(resp_kv); 517 | 518 | 519 | esphome::api::global_api_server->send_homeassistant_service_call(resp); 520 | ESP_LOGI("HA_API", "unlock Lock %s", entity.c_str()); 521 | } 522 | 523 | }; 524 | 525 | } 526 | } -------------------------------------------------------------------------------- /components/shys_m5_dial/ha_device.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "m5dial_display.h" 4 | 5 | #include "ha_device_mode.h" 6 | #include 7 | 8 | namespace esphome 9 | { 10 | namespace shys_m5_dial 11 | { 12 | class HaDevice { 13 | protected: 14 | int index; 15 | std::string entity; 16 | std::string name; 17 | 18 | static const size_t bufferSize = 1024; 19 | StaticJsonDocument jsonBuffer; 20 | JsonObject modeConfig; 21 | 22 | uint16_t currentModeCount = 0; 23 | uint16_t currentModeIndex = 0; 24 | 25 | int apiSendDelay = 1000; 26 | int apiSendLock = 2000; 27 | 28 | int rotaryStepWidth = 10; 29 | 30 | bool modeChanged = true; 31 | 32 | std::vector deviceModes = {}; 33 | 34 | void addMode(HaDeviceMode* newMode){ 35 | newMode->registerHAListener(); 36 | 37 | newMode->setApiSendDelay(getApiSendDelay()); 38 | newMode->setApiSendLock(getApiSendLock()); 39 | newMode->setRotaryStepWidth(getRotaryStepWidth()); 40 | 41 | this->deviceModes.push_back(newMode); 42 | currentModeCount++; 43 | } 44 | 45 | HaDeviceMode* getCurrentMode(){ 46 | return deviceModes[currentModeIndex]; 47 | } 48 | 49 | void loadModesConfigJson(const std::string& modesJsonString){ 50 | DeserializationError error = deserializeJson(jsonBuffer, modesJsonString); 51 | ESP_LOGW("DEVICE", "Modes JSON: %s", modesJsonString.c_str()); 52 | 53 | if (error) { 54 | ESP_LOGW("DEVICE", "Fehler beim Parsen des JSON: %s", error.c_str()); 55 | return; 56 | } 57 | this->modeConfig = jsonBuffer.as(); 58 | } 59 | 60 | public: 61 | HaDevice(const std::string& entity_id, const std::string& name, const std::string& modesJsonString) : entity(entity_id), name(name) { 62 | loadModesConfigJson(modesJsonString); 63 | } 64 | 65 | virtual void init() = 0; 66 | 67 | std::string getName(){ 68 | return this->name; 69 | } 70 | 71 | std::string getEntityId(){ 72 | return this->entity; 73 | } 74 | 75 | int getValue(){ 76 | return getCurrentMode()->getValue(); 77 | } 78 | 79 | uint16_t getMaxRotary(){ 80 | return getCurrentMode()->getMaxValue(); 81 | } 82 | 83 | void setRotaryStepWidth(int stepWidth){ 84 | this->rotaryStepWidth = stepWidth; 85 | } 86 | int getRotaryStepWidth(){ 87 | return this->rotaryStepWidth; 88 | } 89 | 90 | void setApiSendDelay(int delayInMs){ 91 | this->apiSendDelay = delayInMs; 92 | } 93 | int getApiSendDelay(){ 94 | return this->apiSendDelay; 95 | } 96 | 97 | void setApiSendLock(int delayInMs){ 98 | this->apiSendLock = delayInMs; 99 | } 100 | int getApiSendLock(){ 101 | return this->apiSendLock; 102 | } 103 | 104 | int getCurrentModeIndex(){ 105 | return this->currentModeIndex; 106 | } 107 | 108 | void nextMode(){ 109 | if(currentModeIndex >= currentModeCount-1){ 110 | currentModeIndex = 0; 111 | } else { 112 | currentModeIndex++; 113 | } 114 | modeChanged = true; 115 | } 116 | 117 | void previousMode(){ 118 | if(currentModeIndex >= 1){ 119 | currentModeIndex--; 120 | } else { 121 | currentModeIndex = currentModeCount-1; 122 | } 123 | modeChanged = true; 124 | } 125 | 126 | void refreshDisplay(M5DialDisplay& display, bool deviceChanged){ 127 | getCurrentMode()->refreshDisplay(display, deviceChanged || modeChanged); 128 | modeChanged = false; 129 | } 130 | 131 | bool isDisplayRefreshNeeded(){ 132 | return this->getCurrentMode()->isDisplayRefreshNeeded(); 133 | } 134 | 135 | void updateHomeAssistantValue(){ 136 | getCurrentMode()->updateHomeAssistantValue(); 137 | } 138 | 139 | bool onTouch(M5DialDisplay& display, uint16_t x, uint16_t y){ 140 | ESP_LOGD("DEVICE", "HaDevice.onTouch: %i/%i", x, y); 141 | return getCurrentMode()->onTouch(display, x, y); 142 | } 143 | 144 | bool onSwipe(M5DialDisplay& display, const char * direction){ 145 | ESP_LOGD("DEVICE", "HaDevice.onSwipe: %s", direction); 146 | return false; // getCurrentMode()->onSwipe(display, *this, direction); 147 | } 148 | 149 | bool onRotary(M5DialDisplay& display, const char * direction){ 150 | ESP_LOGD("DEVICE", "HaDevice.onRotary: %s", direction); 151 | return getCurrentMode()->onRotary(display, direction); 152 | } 153 | 154 | bool onButton(M5DialDisplay& display, const char * clickType){ 155 | ESP_LOGD("DEVICE", "HaDevice.onButton: %s", clickType); 156 | return getCurrentMode()->onButton(display, clickType); 157 | } 158 | 159 | void onLoop(){ 160 | getCurrentMode()->onLoop(); 161 | } 162 | 163 | }; 164 | 165 | } 166 | } -------------------------------------------------------------------------------- /components/shys_m5_dial/ha_device_climate.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "ha_device.h" 3 | #include "ha_device_mode_climate_temperature.h" 4 | 5 | namespace esphome 6 | { 7 | namespace shys_m5_dial 8 | { 9 | class HaDeviceClimate: public esphome::shys_m5_dial::HaDevice { 10 | protected: 11 | HaDeviceModeClimateTemperature* modeTemp = new HaDeviceModeClimateTemperature(*this); 12 | 13 | public: 14 | HaDeviceClimate(const std::string& entity_id, const std::string& name, const std::string& modes) : HaDevice(entity_id, name, modes) {} 15 | 16 | void init() override { 17 | ESP_LOGD("HA_DEVICE", "Init Climate: %s", this->getEntityId().c_str()); 18 | 19 | this->addMode(modeTemp); 20 | 21 | if (this->modeConfig.containsKey("temp_mode")) { 22 | JsonObject temp_mode = this->modeConfig["temp_mode"]; 23 | 24 | if (temp_mode.containsKey("rotary_step_width")) { 25 | modeTemp->setRotaryStepWidth(temp_mode["rotary_step_width"].as()); 26 | } 27 | } 28 | } 29 | 30 | }; 31 | 32 | } 33 | } -------------------------------------------------------------------------------- /components/shys_m5_dial/ha_device_cover.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "ha_device.h" 3 | #include "ha_device_mode_cover_position.h" 4 | 5 | namespace esphome 6 | { 7 | namespace shys_m5_dial 8 | { 9 | class HaDeviceCover: public esphome::shys_m5_dial::HaDevice { 10 | protected: 11 | HaDeviceModeCoverPosition* modePosition = new HaDeviceModeCoverPosition(*this); 12 | 13 | public: 14 | HaDeviceCover(const std::string& entity_id, const std::string& name, const std::string& modes) : HaDevice(entity_id, name, modes) {} 15 | 16 | void init() override { 17 | ESP_LOGD("HA_DEVICE", "Init Cover: %s", this->getEntityId().c_str()); 18 | 19 | this->addMode(modePosition); 20 | 21 | if (this->modeConfig.containsKey("position_mode")) { 22 | JsonObject position_mode = this->modeConfig["position_mode"]; 23 | 24 | if (position_mode.containsKey("rotary_step_width")) { 25 | modePosition->setRotaryStepWidth(position_mode["rotary_step_width"].as()); 26 | } 27 | } 28 | } 29 | 30 | }; 31 | 32 | } 33 | } -------------------------------------------------------------------------------- /components/shys_m5_dial/ha_device_fan.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "ha_device.h" 3 | #include "ha_device_mode_fan_speed.h" 4 | 5 | namespace esphome 6 | { 7 | namespace shys_m5_dial 8 | { 9 | class HaDeviceFan: public esphome::shys_m5_dial::HaDevice { 10 | protected: 11 | HaDeviceModeFanSpeed* modeSpeed = new HaDeviceModeFanSpeed(*this); 12 | 13 | public: 14 | HaDeviceFan(const std::string& entity_id, const std::string& name, const std::string& modes) : HaDevice(entity_id, name, modes) {} 15 | 16 | void init() override { 17 | ESP_LOGD("HA_DEVICE", "Init Fan: %s", this->getEntityId().c_str()); 18 | 19 | this->addMode(modeSpeed); 20 | 21 | if (this->modeConfig.containsKey("speed_mode")) { 22 | JsonObject speed_mode = this->modeConfig["speed_mode"]; 23 | 24 | if (speed_mode.containsKey("rotary_step_width")) { 25 | modeSpeed->setRotaryStepWidth(speed_mode["rotary_step_width"].as()); 26 | } 27 | 28 | if (speed_mode.containsKey("changeable_direction")) { 29 | modeSpeed->setChangeableDirection(speed_mode["changeable_direction"].as()); 30 | } 31 | } 32 | } 33 | 34 | }; 35 | 36 | } 37 | } -------------------------------------------------------------------------------- /components/shys_m5_dial/ha_device_light.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "ha_device.h" 3 | #include "ha_device_mode_light_on_off.h" 4 | #include "ha_device_mode_light_brightness.h" 5 | #include "ha_device_mode_light_color.h" 6 | #include "ha_device_mode_light_tunable_white.h" 7 | 8 | namespace esphome 9 | { 10 | namespace shys_m5_dial 11 | { 12 | class HaDeviceLight: public esphome::shys_m5_dial::HaDevice { 13 | protected: 14 | HaDeviceModeLightOnOff* modeOnOff = new HaDeviceModeLightOnOff(*this); 15 | HaDeviceModeLightBrightness* modeBrightness = new HaDeviceModeLightBrightness(*this); 16 | HaDeviceModeLightColor* modeColor = new HaDeviceModeLightColor(*this); 17 | HaDeviceModeLightTunableWhite* modeTunableWhite = new HaDeviceModeLightTunableWhite(*this); 18 | 19 | public: 20 | HaDeviceLight(const std::string& entity_id, const std::string& name, const std::string& modes) : HaDevice(entity_id, name, modes) {} 21 | 22 | void init() override { 23 | ESP_LOGD("HA_DEVICE", "Init Light: %s", this->getEntityId().c_str()); 24 | 25 | bool brightnessModeActive = false; 26 | JsonObject dimm_mode; 27 | if (this->modeConfig.containsKey("dimm_mode")) { 28 | dimm_mode = this->modeConfig["dimm_mode"]; 29 | if (dimm_mode.containsKey("enable") && dimm_mode["enable"].as()) { 30 | brightnessModeActive = true; 31 | } 32 | } 33 | 34 | if(brightnessModeActive){ 35 | ESP_LOGD("HA_DEVICE", "Dimm-Mode enabled (steps: %i)", dimm_mode["rotary_step_width"].as()); 36 | 37 | this->addMode(modeBrightness); 38 | 39 | if (dimm_mode.containsKey("rotary_step_width")) { 40 | modeBrightness->setRotaryStepWidth(dimm_mode["rotary_step_width"].as()); 41 | } 42 | 43 | } else { 44 | this->addMode(modeOnOff); 45 | } 46 | 47 | 48 | if (this->modeConfig.containsKey("rgb_mode")) { 49 | JsonObject rgb_mode = this->modeConfig["rgb_mode"]; 50 | 51 | if (rgb_mode.containsKey("enable") && rgb_mode["enable"].as()) { 52 | ESP_LOGD("HA_DEVICE", "Color-Mode enabled (steps: %i)", rgb_mode["rotary_step_width"].as()); 53 | 54 | this->addMode(modeColor); 55 | 56 | if (rgb_mode.containsKey("rotary_step_width")) { 57 | modeColor->setRotaryStepWidth(rgb_mode["rotary_step_width"].as()); 58 | } 59 | } 60 | } 61 | 62 | 63 | if (this->modeConfig.containsKey("white_mode")) { 64 | JsonObject white_mode = this->modeConfig["white_mode"]; 65 | 66 | if (white_mode.containsKey("enable") && white_mode["enable"].as()) { 67 | ESP_LOGD("HA_DEVICE", "White-Mode enabled (steps: %i)", white_mode["rotary_step_width"].as()); 68 | 69 | this->addMode(modeTunableWhite); 70 | 71 | if (white_mode.containsKey("rotary_step_width")) { 72 | modeTunableWhite->setRotaryStepWidth(white_mode["rotary_step_width"].as()); 73 | } 74 | if (white_mode.containsKey("min_kelvin")) { 75 | modeTunableWhite->setMinValue(white_mode["min_kelvin"].as()); 76 | } 77 | if (white_mode.containsKey("max_kelvin")) { 78 | modeTunableWhite->setMaxValue(white_mode["max_kelvin"].as()); 79 | } 80 | } 81 | } 82 | 83 | } 84 | 85 | }; 86 | 87 | } 88 | } -------------------------------------------------------------------------------- /components/shys_m5_dial/ha_device_lock.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "ha_device.h" 3 | #include "ha_device_mode_lock.h" 4 | 5 | namespace esphome 6 | { 7 | namespace shys_m5_dial 8 | { 9 | class HaDeviceLock: public esphome::shys_m5_dial::HaDevice { 10 | protected: 11 | HaDeviceModeLock* modeLock = new HaDeviceModeLock(*this); 12 | 13 | public: 14 | HaDeviceLock(const std::string& entity_id, const std::string& name, const std::string& modes) : HaDevice(entity_id, name, modes) {} 15 | 16 | void init() override { 17 | ESP_LOGD("HA_DEVICE", "Init Lock: %s", this->getEntityId().c_str()); 18 | 19 | this->addMode(modeLock); 20 | 21 | if (this->modeConfig.containsKey("lock_mode")) { 22 | JsonObject lock_mode = this->modeConfig["lock_mode"]; 23 | 24 | if (lock_mode.containsKey("rotary_step_width")) { 25 | modeLock->setRotaryStepWidth(lock_mode["rotary_step_width"].as()); 26 | } 27 | 28 | if(lock_mode.containsKey("open_on_button")){ 29 | modeLock->setOpenOnButton(lock_mode["open_on_button"].as()); 30 | } 31 | } 32 | } 33 | 34 | }; 35 | 36 | } 37 | } -------------------------------------------------------------------------------- /components/shys_m5_dial/ha_device_mediaplayer.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "ha_device.h" 3 | #include "ha_device_mode_mediaplayer_play.h" 4 | #include "ha_device_mode_mediaplayer_source.h" 5 | 6 | namespace esphome 7 | { 8 | namespace shys_m5_dial 9 | { 10 | class HaDeviceMediaPlayer: public esphome::shys_m5_dial::HaDevice { 11 | protected: 12 | HaDeviceModeMediaPlayerPlay* modePlay = new HaDeviceModeMediaPlayerPlay(*this); 13 | HaDeviceModeMediaPlayerSource* modeSource = new HaDeviceModeMediaPlayerSource(*this); 14 | 15 | public: 16 | HaDeviceMediaPlayer(const std::string& entity_id, const std::string& name, const std::string& modes) : HaDevice(entity_id, name, modes) {} 17 | 18 | void init() override { 19 | ESP_LOGD("HA_DEVICE", "Init Media-Player: %s", this->getEntityId().c_str()); 20 | 21 | this->addMode(modePlay); 22 | 23 | modeSource->loadMediaContents(this->modeConfig); 24 | this->addMode(modeSource); 25 | 26 | if (this->modeConfig.containsKey("play_mode")) { 27 | JsonObject play_mode = this->modeConfig["play_mode"]; 28 | if (play_mode.containsKey("rotary_step_width")) { 29 | modePlay->setRotaryStepWidth(play_mode["rotary_step_width"].as()); 30 | } 31 | } 32 | 33 | if (this->modeConfig.containsKey("source_mode")) { 34 | JsonObject source_mode = this->modeConfig["source_mode"]; 35 | if (source_mode.containsKey("rotary_step_width")) { 36 | modeSource->setRotaryStepWidth(source_mode["rotary_step_width"].as()); 37 | } 38 | } 39 | } 40 | 41 | }; 42 | 43 | } 44 | } -------------------------------------------------------------------------------- /components/shys_m5_dial/ha_device_mode.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "ha_device.h" 4 | #include "ha_api.h" 5 | #include "m5dial_display.h" 6 | #include "M5Dial.h" 7 | 8 | namespace esphome 9 | { 10 | namespace shys_m5_dial 11 | { 12 | class HaDevice; 13 | class HaDeviceMode { 14 | protected: 15 | int value = 0; 16 | int minValue = 0; 17 | int maxValue = 100; 18 | 19 | int rotaryStepWidth = 10; 20 | 21 | int apiSendDelay = 1000; 22 | int apiSendLock = 2000; 23 | 24 | bool currentValueModified = false; 25 | unsigned long lastApiCall = 0; 26 | unsigned long lastValueUpdate = 0; 27 | 28 | bool endlessRotaryValue = false; 29 | 30 | bool displayRefreshNeeded = false; 31 | double lastDisplayRefresh = 0; 32 | 33 | HaApi haApi; 34 | HaDevice& device; 35 | 36 | 37 | HaDevice& getDevice() const { 38 | return this->device; 39 | } 40 | 41 | bool isApiCallNeeded(){ 42 | if ( esphome::millis() - lastApiCall < apiSendLock ) { 43 | return false; 44 | } 45 | 46 | if ( esphome::millis() - lastValueUpdate < apiSendDelay ) { 47 | return false; 48 | } 49 | 50 | return true; 51 | } 52 | 53 | void raiseCurrentValue(){ 54 | int newValue = this->getValue() + rotaryStepWidth; 55 | newValue = getNextToRotaryStepwidth(newValue); 56 | 57 | if(newValue > this->maxValue){ 58 | setValue(endlessRotaryValue?this->minValue:this->maxValue); 59 | } else { 60 | setValue(newValue); 61 | } 62 | } 63 | 64 | void reduceCurrentValue(){ 65 | int newValue = this->getValue() - rotaryStepWidth; 66 | newValue = getNextToRotaryStepwidth(newValue); 67 | 68 | if(newValue >= this->minValue){ 69 | setValue(newValue); 70 | } else { 71 | setValue(endlessRotaryValue?this->maxValue:this->minValue); 72 | } 73 | } 74 | 75 | int getNextToRotaryStepwidth(int val){ 76 | if(rotaryStepWidth > 1){ 77 | int rst = (val % rotaryStepWidth); 78 | if(rst >= (rotaryStepWidth/2)){ 79 | val += rotaryStepWidth-rst; 80 | } else { 81 | val -= rst; 82 | } 83 | } 84 | return val; 85 | } 86 | 87 | uint16_t getValueForXPosition(uint16_t x){ 88 | return map(x, 0, M5Dial.Display.width(), this->minValue, this->maxValue); 89 | } 90 | 91 | uint16_t getValueForYPosition(uint16_t y){ 92 | return this->maxValue - map(y, 0, M5Dial.Display.height(), this->minValue, this->maxValue) + this->minValue; 93 | } 94 | 95 | uint16_t getDisplayPositionX(uint16_t currentValue){ 96 | return map(currentValue, this->minValue, this->maxValue, 0, M5Dial.Display.width()); 97 | } 98 | 99 | uint16_t getDisplayPositionY(uint16_t currentValue){ 100 | return M5Dial.Display.height() - map(currentValue, this->minValue, this->maxValue, 0, M5Dial.Display.height()); 101 | } 102 | 103 | 104 | bool defaultOnTouch(M5DialDisplay& display, uint16_t x, uint16_t y) { 105 | if(y > display.getHeight() * .97){ 106 | y = display.getHeight(); 107 | } else if (y < display.getHeight() * .03){ 108 | y = 0; 109 | } 110 | 111 | uint16_t result = this->getValueForYPosition(y); 112 | result = getNextToRotaryStepwidth(result); 113 | 114 | this->setValue(result); 115 | ESP_LOGD("TOUCH", "Aktuellen Wert auf %i gesetzt", result); 116 | 117 | return true; 118 | } 119 | 120 | bool defaultOnRotary(M5DialDisplay& display, const char * direction) { 121 | if (strcmp(direction, ROTARY_LEFT)==0){ 122 | this->reduceCurrentValue(); 123 | } else if (strcmp(direction, ROTARY_RIGHT)==0){ 124 | this->raiseCurrentValue(); 125 | } 126 | 127 | return true; 128 | } 129 | 130 | virtual void sendValueToHomeAssistant(int value) = 0; 131 | 132 | public: 133 | HaDeviceMode(HaDevice& device) : device(device) {} 134 | 135 | virtual void refreshDisplay(M5DialDisplay& display, bool init) = 0; 136 | virtual void registerHAListener() = 0; 137 | 138 | 139 | virtual bool isDisplayRefreshNeeded(){ 140 | return displayRefreshNeeded; 141 | } 142 | 143 | virtual bool onTouch(M5DialDisplay& display, uint16_t x, uint16_t y) { 144 | return false; 145 | } 146 | 147 | virtual bool onSwipe(M5DialDisplay& display, const char * direction) { 148 | return false; 149 | } 150 | 151 | virtual bool onRotary(M5DialDisplay& display, const char * direction) { 152 | return false; 153 | } 154 | 155 | virtual bool onButton(M5DialDisplay& display, const char * clickType) { 156 | return false; 157 | } 158 | 159 | virtual void onLoop(){} 160 | 161 | 162 | void updateHomeAssistantValue(){ 163 | if(this->isValueModified() && this->isApiCallNeeded() ) { 164 | lastApiCall = esphome::millis(); 165 | 166 | this->sendValueToHomeAssistant(getValue()); 167 | 168 | currentValueModified = false; 169 | } 170 | } 171 | 172 | virtual int getValue(){ 173 | return this->value; 174 | } 175 | 176 | void setValue(int val){ 177 | this->value = val; 178 | this->lastValueUpdate = esphome::millis(); 179 | this->currentValueModified = true; 180 | } 181 | 182 | void setReceivedValue(int val){ 183 | this->value = val; 184 | } 185 | 186 | 187 | int getMinValue(){ 188 | return this->minValue; 189 | } 190 | 191 | void setMinValue(int val){ 192 | this->minValue = val; 193 | } 194 | 195 | int getMaxValue(){ 196 | return this->maxValue; 197 | } 198 | 199 | void setMaxValue(int val){ 200 | this->maxValue = val; 201 | } 202 | 203 | bool isValueModified(){ 204 | return this->currentValueModified; 205 | } 206 | 207 | void setRotaryStepWidth(int stepWidth){ 208 | this->rotaryStepWidth = stepWidth; 209 | } 210 | 211 | void setApiSendDelay(int delayInMs){ 212 | this->apiSendDelay = delayInMs; 213 | } 214 | 215 | void setApiSendLock(int delayInMs){ 216 | this->apiSendLock = delayInMs; 217 | } 218 | }; 219 | } 220 | } -------------------------------------------------------------------------------- /components/shys_m5_dial/ha_device_mode_climate_temperature.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace esphome 4 | { 5 | namespace shys_m5_dial 6 | { 7 | class HaDeviceModeClimateTemperature: public esphome::shys_m5_dial::HaDeviceMode { 8 | protected: 9 | std::string hvac_mode = "none"; 10 | 11 | std::string getHvacMode(){ 12 | return this->hvac_mode; 13 | } 14 | 15 | void setHvacMode(const std::string& newMode){ 16 | this->hvac_mode = newMode; 17 | } 18 | 19 | void sendValueToHomeAssistant(int value) override { 20 | haApi.setClimateTemperature(this->device.getEntityId(), value); 21 | } 22 | 23 | void showTemperatureMenu(M5DialDisplay& display){ 24 | LovyanGFX* gfx = display.getGfx(); 25 | 26 | uint16_t currentValue = getValue(); 27 | 28 | uint16_t height = gfx->height(); 29 | uint16_t width = gfx->width(); 30 | 31 | gfx->setTextColor(MAROON); 32 | gfx->setTextDatum(middle_center); 33 | 34 | gfx->startWrite(); // Secure SPI bus 35 | 36 | gfx->fillRect(0, 0, width, this->getDisplayPositionY(currentValue) , RED); 37 | gfx->fillRect(0, this->getDisplayPositionY(currentValue), width, height, YELLOW); 38 | 39 | display.setFontsize(3); 40 | gfx->drawString(String(currentValue).c_str(), 41 | width / 2, 42 | height / 2 - 30); 43 | 44 | display.setFontsize(1); 45 | gfx->drawString(this->device.getName().c_str(), 46 | width / 2, 47 | height / 2 + 20); 48 | gfx->drawString("Temperature", 49 | width / 2, 50 | height / 2 + 50); 51 | 52 | gfx->endWrite(); // Release SPI bus 53 | } 54 | 55 | public: 56 | HaDeviceModeClimateTemperature(HaDevice& device) : HaDeviceMode(device){ 57 | this->maxValue = 40; 58 | } 59 | 60 | void refreshDisplay(M5DialDisplay& display, bool init) override { 61 | this->showTemperatureMenu(display); 62 | ESP_LOGD("DISPLAY", "Temperature-Modus"); 63 | } 64 | 65 | void registerHAListener() override { 66 | std::string attrNameState = ""; 67 | api::global_api_server->subscribe_home_assistant_state( 68 | this->device.getEntityId().c_str(), 69 | attrNameState, 70 | [this](const std::string &state) { 71 | 72 | if(this->isValueModified()){ 73 | return; 74 | } 75 | 76 | this->setHvacMode(state.c_str()); 77 | ESP_LOGI("HA_API", "Got Mode %s for %s", state.c_str(), this->device.getEntityId().c_str()); 78 | }); 79 | 80 | std::string attrNameTemp = "temperature"; 81 | api::global_api_server->subscribe_home_assistant_state( 82 | this->device.getEntityId().c_str(), 83 | attrNameTemp, 84 | [this](const std::string &state) { 85 | 86 | if(this->isValueModified()){ 87 | return; 88 | } 89 | 90 | auto val = parse_number(state); 91 | 92 | if (!val.has_value()) { 93 | this->setReceivedValue(0); 94 | ESP_LOGD("HA_API", "No Temperature value in %s for %s", state.c_str(), this->device.getEntityId().c_str()); 95 | } else { 96 | this->setReceivedValue(int(val.value())); 97 | ESP_LOGI("HA_API", "Got Temperature value %i for %s", int(val.value()), this->device.getEntityId().c_str()); 98 | } 99 | }); 100 | } 101 | 102 | 103 | bool onTouch(M5DialDisplay& display, uint16_t x, uint16_t y) override { 104 | return defaultOnTouch(display, x, y); 105 | } 106 | 107 | bool onRotary(M5DialDisplay& display, const char * direction) override { 108 | return defaultOnRotary(display, direction); 109 | } 110 | 111 | bool onButton(M5DialDisplay& display, const char * clickType) override { 112 | if (strcmp(clickType, BUTTON_SHORT)==0){ 113 | if(strcmp(this->getHvacMode().c_str(), "off")==0){ 114 | haApi.turnClimateOn(this->device.getEntityId()); 115 | } else { 116 | haApi.turnClimateOff(this->device.getEntityId()); 117 | } 118 | 119 | return true; 120 | } 121 | return false; 122 | } 123 | 124 | }; 125 | } 126 | } -------------------------------------------------------------------------------- /components/shys_m5_dial/ha_device_mode_cover_position.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "ha_device_mode_percentage.h" 3 | 4 | namespace esphome 5 | { 6 | namespace shys_m5_dial 7 | { 8 | class HaDeviceModeCoverPosition: public esphome::shys_m5_dial::HaDeviceModePercentage { 9 | protected: 10 | void sendValueToHomeAssistant(int value) override { 11 | haApi.setCoverPosition(this->device.getEntityId(), value); 12 | } 13 | 14 | public: 15 | HaDeviceModeCoverPosition(HaDevice& device) : HaDeviceModePercentage(device){} 16 | 17 | void registerHAListener() { 18 | std::string attrName = "current_position"; 19 | api::global_api_server->subscribe_home_assistant_state( 20 | this->device.getEntityId().c_str(), 21 | attrName, 22 | [this](const std::string &state) { 23 | if(this->isValueModified()){ 24 | return; 25 | } 26 | auto val = parse_number(state); 27 | if (!val.has_value()) { 28 | this->setReceivedValue(0); 29 | ESP_LOGD("HA_API", "No Position value in %s for %s", state.c_str(), this->device.getEntityId().c_str()); 30 | } else { 31 | this->setReceivedValue(val.value()); 32 | ESP_LOGI("HA_API", "Got Position value %i for %s", val.value(), this->device.getEntityId().c_str()); 33 | } 34 | }); 35 | }; 36 | 37 | bool onButton(M5DialDisplay& display, const char * clickType) override { 38 | if (strcmp(clickType, BUTTON_SHORT)==0){ 39 | if(this->getValue() > 50){ 40 | this->setValue(0); 41 | } else { 42 | this->setValue(100); 43 | } 44 | 45 | return true; 46 | } 47 | return false; 48 | } 49 | }; 50 | } 51 | } -------------------------------------------------------------------------------- /components/shys_m5_dial/ha_device_mode_fan_speed.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "globals.h" 3 | 4 | namespace esphome 5 | { 6 | namespace shys_m5_dial 7 | { 8 | class HaDeviceModeFanSpeed: public esphome::shys_m5_dial::HaDeviceMode { 9 | protected: 10 | bool changeableDirection = false; 11 | const char* direction = FAN_DIRECTION_FORWARD; 12 | 13 | bool stateIsOn = false; 14 | 15 | void changeDirection(){ 16 | if(changeableDirection){ 17 | if (strcmp(direction, FAN_DIRECTION_FORWARD) == 0){ 18 | this->publishDirection(FAN_DIRECTION_REVERSE); 19 | } else { 20 | this->publishDirection(FAN_DIRECTION_FORWARD); 21 | } 22 | } 23 | 24 | } 25 | 26 | void sendValueToHomeAssistant(int value) override { 27 | haApi.setFanSpeed(this->device.getEntityId(), value); 28 | } 29 | 30 | void showFanMenu(M5DialDisplay& display, uint16_t currentValue){ 31 | LovyanGFX* gfx = display.getGfx(); 32 | uint16_t height = gfx->height(); 33 | uint16_t width = gfx->width(); 34 | 35 | gfx->setTextColor(MAROON); 36 | gfx->setTextDatum(middle_center); 37 | 38 | gfx->startWrite(); // Secure SPI bus 39 | 40 | gfx->fillRect(0, 0, width, this->getDisplayPositionY(currentValue), RED); 41 | gfx->fillRect(0, this->getDisplayPositionY(currentValue), width, height, YELLOW); 42 | 43 | display.setFontsize(3); 44 | gfx->drawString((String(currentValue) + "%").c_str(), 45 | width / 2, 46 | height / 2 - 30); 47 | 48 | display.setFontsize(1); 49 | gfx->drawString(this->device.getName().c_str(), 50 | width / 2, 51 | height / 2 + 20); 52 | gfx->drawString(this->changeableDirection?direction:"Speed", 53 | width / 2, 54 | height / 2 + 50); 55 | 56 | gfx->endWrite(); // Release SPI bus 57 | } 58 | 59 | public: 60 | HaDeviceModeFanSpeed(HaDevice& device) : HaDeviceMode(device){} 61 | 62 | void setState(const std::string& newState){ 63 | this->stateIsOn = (strcmp(newState.c_str(), "on") == 0); 64 | } 65 | 66 | int getValue() override { 67 | return this->stateIsOn ? this->value : 0; 68 | } 69 | 70 | 71 | void setChangeableDirection(bool isChangeable){ 72 | this->changeableDirection = isChangeable; 73 | } 74 | 75 | void publishDirection(const std::string& newDirection){ 76 | setDirection(newDirection); 77 | haApi.setFanDirection(this->device.getEntityId(), this->direction); 78 | } 79 | 80 | void setDirection(const std::string& newDirection){ 81 | if (strcmp(newDirection.c_str(), FAN_DIRECTION_FORWARD) == 0){ 82 | this->direction = FAN_DIRECTION_FORWARD; 83 | } else if (strcmp(newDirection.c_str(), FAN_DIRECTION_REVERSE) == 0){ 84 | this->direction = FAN_DIRECTION_REVERSE; 85 | } 86 | 87 | displayRefreshNeeded = true; 88 | 89 | ESP_LOGD("DEVICE", "set direction: %s", this->direction); 90 | } 91 | 92 | void refreshDisplay(M5DialDisplay& display, bool init) override { 93 | ESP_LOGD("DISPLAY", "refresh Display: Speed-Modus"); 94 | this->showFanMenu(display, getValue()); 95 | 96 | this->displayRefreshNeeded = false; 97 | } 98 | 99 | void registerHAListener() override { 100 | std::string attrNameState = ""; 101 | api::global_api_server->subscribe_home_assistant_state( 102 | this->device.getEntityId().c_str(), 103 | attrNameState, 104 | [this](const std::string &state) { 105 | if(this->isValueModified()){ 106 | return; 107 | } 108 | 109 | this->setState(state); 110 | ESP_LOGI("HA_API", "Got State %s for %s", state.c_str(), this->device.getEntityId().c_str()); 111 | }); 112 | 113 | std::string attrNamePercentage = "percentage"; 114 | api::global_api_server->subscribe_home_assistant_state( 115 | this->device.getEntityId().c_str(), 116 | attrNamePercentage, 117 | [this](const std::string &state) { 118 | if(this->isValueModified()){ 119 | return; 120 | } 121 | auto val = parse_number(state); 122 | if (!val.has_value()) { 123 | this->setReceivedValue(0); 124 | ESP_LOGD("HA_API", "No Percentage value in %s for %s", state.c_str(), this->device.getEntityId().c_str()); 125 | } else { 126 | this->setReceivedValue(round((int)val.value())); 127 | ESP_LOGI("HA_API", "Got Percentage value %i for %s", val.value(), this->device.getEntityId().c_str()); 128 | } 129 | }); 130 | 131 | if(this->changeableDirection){ 132 | std::string attrNameDirection = "direction"; 133 | api::global_api_server->subscribe_home_assistant_state( 134 | this->device.getEntityId().c_str(), 135 | attrNameDirection, 136 | [this](const std::string &state) { 137 | if(this->isValueModified()){ 138 | return; 139 | } 140 | 141 | setDirection(state.c_str()); 142 | ESP_LOGI("HA_API", "Got direction value %s for %s", state.c_str(), this->device.getEntityId().c_str()); 143 | }); 144 | } 145 | } 146 | 147 | bool onTouch(M5DialDisplay& display, uint16_t x, uint16_t y) override { 148 | return this->defaultOnTouch(display, x, y); 149 | } 150 | 151 | bool onRotary(M5DialDisplay& display, const char * direction) override { 152 | if(! this->stateIsOn){ 153 | if (strcmp(direction, ROTARY_LEFT)==0){ 154 | this->publishDirection(FAN_DIRECTION_REVERSE); 155 | this->stateIsOn = true; 156 | this->value = 0; 157 | } else if (strcmp(direction, ROTARY_RIGHT)==0){ 158 | this->publishDirection(FAN_DIRECTION_FORWARD); 159 | this->stateIsOn = true; 160 | this->value = 0; 161 | } 162 | } 163 | 164 | if (strcmp(direction, ROTARY_LEFT)==0){ 165 | if(strcmp(this->direction, FAN_DIRECTION_FORWARD)==0){ 166 | this->reduceCurrentValue(); 167 | } else { 168 | this->raiseCurrentValue(); 169 | } 170 | 171 | } else if (strcmp(direction, ROTARY_RIGHT)==0){ 172 | if(strcmp(this->direction, FAN_DIRECTION_FORWARD)==0){ 173 | this->raiseCurrentValue(); 174 | } else { 175 | this->reduceCurrentValue(); 176 | } 177 | } 178 | 179 | if(this->value == 0){ 180 | this->stateIsOn = false; 181 | } 182 | 183 | return true; 184 | } 185 | 186 | bool onButton(M5DialDisplay& display, const char * clickType) override { 187 | if (strcmp(clickType, BUTTON_SHORT)==0){ 188 | haApi.toggleFan(this->device.getEntityId()); 189 | return true; 190 | } else if (strcmp(clickType, BUTTON_LONG)==0){ 191 | this->changeDirection(); 192 | return true; 193 | } 194 | return false; 195 | } 196 | 197 | }; 198 | } 199 | } -------------------------------------------------------------------------------- /components/shys_m5_dial/ha_device_mode_light_brightness.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace esphome 4 | { 5 | namespace shys_m5_dial 6 | { 7 | class HaDeviceModeLightBrightness: public esphome::shys_m5_dial::HaDeviceMode { 8 | protected: 9 | void sendValueToHomeAssistant(int value) override { 10 | haApi.turnLightOn(this->device.getEntityId(), value); 11 | } 12 | 13 | void showBrightnessMenu(M5DialDisplay& display, uint16_t currentValue){ 14 | LovyanGFX* gfx = display.getGfx(); 15 | 16 | uint16_t height = gfx->height(); 17 | uint16_t width = gfx->width(); 18 | 19 | gfx->setTextColor(MAROON); 20 | gfx->setTextDatum(middle_center); 21 | 22 | gfx->startWrite(); // Secure SPI bus 23 | 24 | gfx->fillRect(0, 0, width, this->getDisplayPositionY(currentValue), RED); 25 | gfx->fillRect(0, this->getDisplayPositionY(currentValue), width, height, YELLOW); 26 | 27 | display.setFontsize(3); 28 | gfx->drawString((String(currentValue) + "%").c_str(), 29 | width / 2, 30 | height / 2 - 30); 31 | 32 | display.setFontsize(1); 33 | gfx->drawString(this->device.getName().c_str(), 34 | width / 2, 35 | height / 2 + 20); 36 | gfx->drawString("Brightness", 37 | width / 2, 38 | height / 2 + 50); 39 | 40 | gfx->endWrite(); // Release SPI bus 41 | } 42 | 43 | public: 44 | HaDeviceModeLightBrightness(HaDevice& device) : HaDeviceMode(device){} 45 | 46 | void refreshDisplay(M5DialDisplay& display, bool init) override { 47 | ESP_LOGD("DISPLAY", "refresh Display: Helligkeits-Modus"); 48 | this->showBrightnessMenu(display, getValue()); 49 | } 50 | 51 | void registerHAListener() override { 52 | std::string attrName = "brightness"; 53 | api::global_api_server->subscribe_home_assistant_state( 54 | this->device.getEntityId().c_str(), 55 | attrName, 56 | [this](const std::string &state) { 57 | if(this->isValueModified()){ 58 | return; 59 | } 60 | auto val = parse_number(state); 61 | if (!val.has_value()) { 62 | this->setReceivedValue(0); 63 | ESP_LOGD("HA_API", "No Brightness value in %s for %s", state.c_str(), this->device.getEntityId().c_str()); 64 | } else { 65 | this->setReceivedValue(round((float)val.value()*100/255)); 66 | ESP_LOGI("HA_API", "Got Brightness value %i for %s", val.value(), this->device.getEntityId().c_str()); 67 | } 68 | }); 69 | } 70 | 71 | bool onTouch(M5DialDisplay& display, uint16_t x, uint16_t y) override { 72 | return this->defaultOnTouch(display, x, y); 73 | } 74 | 75 | bool onRotary(M5DialDisplay& display, const char * direction) override { 76 | return this->defaultOnRotary(display, direction); 77 | } 78 | 79 | bool onButton(M5DialDisplay& display, const char * clickType) override { 80 | if (strcmp(clickType, BUTTON_SHORT)==0){ 81 | haApi.toggleLight(this->device.getEntityId()); 82 | return true; 83 | } 84 | return false; 85 | } 86 | 87 | }; 88 | } 89 | } -------------------------------------------------------------------------------- /components/shys_m5_dial/ha_device_mode_light_color.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "m5dial_display.h" 3 | 4 | namespace esphome 5 | { 6 | namespace shys_m5_dial 7 | { 8 | class HaDeviceModeLightColor: public esphome::shys_m5_dial::HaDeviceMode { 9 | protected: 10 | void sendValueToHomeAssistant(int value) override { 11 | haApi.turnLightOn(this->device.getEntityId(), -1, value); 12 | } 13 | 14 | typedef struct rgb { 15 | float r, g, b; 16 | } RGB; 17 | 18 | typedef struct hsl { 19 | float h, s, l; 20 | } HSL; 21 | 22 | typedef struct coord { 23 | float x, y; 24 | } COORD; 25 | 26 | uint32_t getColorByDegree(float degree){ 27 | return hslToRgb(degree/360.0, 1, .5); 28 | } 29 | 30 | uint32_t getComplementaryByDegree(float degree){ 31 | uint32_t complementary = degree; 32 | if(complementary >= 180){ 33 | complementary = complementary - 180; 34 | } else { 35 | complementary = complementary + 180; 36 | } 37 | return getColorByDegree(complementary); 38 | } 39 | 40 | /* 41 | * Converts an HUE to r, g or b. 42 | * returns float in the set [0, 1]. 43 | */ 44 | float hue2rgb(float p, float q, float t) { 45 | if (t < 0) 46 | t += 1; 47 | if (t > 1) 48 | t -= 1; 49 | if (t < 1./6) 50 | return p + (q - p) * 6 * t; 51 | if (t < 1./2) 52 | return q; 53 | if (t < 2./3) 54 | return p + (q - p) * (2./3 - t) * 6; 55 | 56 | return p; 57 | } 58 | 59 | /* 60 | * Convert HSL to RGB. 61 | * http://en.wikipedia.org/wiki/HSL_color_space. 62 | * 63 | * h, s, l [float 0 - 1] 64 | * 65 | * returns color888 66 | */ 67 | uint32_t hslToRgb(float h, float s, float l) { 68 | RGB result; 69 | 70 | if(0 == s) { 71 | result.r = result.g = result.b = l; // achromatic 72 | } 73 | else { 74 | float q = l < 0.5 ? l * (1 + s) : l + s - l * s; 75 | float p = 2 * l - q; 76 | result.r = hue2rgb(p, q, h + 1./3) * 255; 77 | result.g = hue2rgb(p, q, h) * 255; 78 | result.b = hue2rgb(p, q, h - 1./3) * 255; 79 | } 80 | 81 | return M5Dial.Display.color888(result.r, result.g, result.b); 82 | } 83 | 84 | 85 | coord getColorCoord(LovyanGFX* gfx, float radius, float degree){ 86 | coord result; 87 | result.x = radius * sin(degree*M_PI/180) + (gfx->width()/2); 88 | result.y = radius * cos(degree*M_PI/180) + (gfx->height()/2); 89 | return result; 90 | } 91 | 92 | void drawColorCircleLine(LovyanGFX* gfx, float degree, float r1, float r2, uint32_t color) { 93 | uint16_t step = 1; 94 | coord c1 = getColorCoord(gfx, r1, degree); 95 | coord c2 = getColorCoord(gfx, r2, degree-step); 96 | coord c3 = getColorCoord(gfx, r2, degree+step); 97 | 98 | M5Dial.Display.fillTriangle(c1.x, c1.y, c2.x, c2.y, c3.x, c3.y, color); 99 | 100 | c1 = getColorCoord(gfx, r1, degree); 101 | c2 = getColorCoord(gfx, r1, degree-step-step); 102 | c3 = getColorCoord(gfx, r2, degree-step); 103 | M5Dial.Display.fillTriangle(c1.x, c1.y, c2.x, c2.y, c3.x, c3.y, color); 104 | } 105 | 106 | void refreshColorMenu(M5DialDisplay& display){ 107 | LovyanGFX* gfx = display.getGfx(); 108 | 109 | int currentValue = getValue(); 110 | uint32_t complementary_color = getComplementaryByDegree(currentValue); 111 | 112 | int height = gfx->height(); 113 | int width = gfx->width(); 114 | 115 | gfx->setTextColor(complementary_color); 116 | gfx->setTextDatum(middle_center); 117 | 118 | gfx->startWrite(); // Secure SPI bus 119 | gfx->fillCircle(width/2, height/2, 70, getColorByDegree(currentValue)); 120 | 121 | display.setFontsize(1); 122 | gfx->drawString(String(currentValue), 123 | width / 2, 124 | height / 2 - 20); 125 | 126 | display.setFontsize(1); 127 | gfx->drawString(this->device.getName().c_str(), 128 | width / 2, 129 | height / 2 + 20); 130 | gfx->drawString("Color", 131 | width / 2, 132 | height / 2 + 50); 133 | 134 | drawColorCircleLine(gfx, currentValue, 40, 69, complementary_color); 135 | gfx->endWrite(); // Release SPI bus 136 | } 137 | 138 | void showColorMenu(M5DialDisplay& display){ 139 | LovyanGFX* gfx = display.getGfx(); 140 | 141 | int currentValue = getValue(); 142 | uint32_t complementary_color = getComplementaryByDegree(currentValue); 143 | 144 | int height = gfx->height(); 145 | int width = gfx->width(); 146 | 147 | gfx->startWrite(); // Secure SPI bus 148 | 149 | gfx->fillRect(0, 0, width, height, BLACK); 150 | 151 | for (int i=0; i<360; i++){ 152 | drawColorCircleLine(gfx, i, 70.0, 130.0, getColorByDegree(i)); 153 | } 154 | 155 | gfx->endWrite(); // Release SPI bus 156 | 157 | refreshColorMenu(display); 158 | } 159 | 160 | 161 | public: 162 | HaDeviceModeLightColor(HaDevice& device) : HaDeviceMode(device){ 163 | this->maxValue = 360; 164 | this->endlessRotaryValue = true; 165 | } 166 | 167 | void refreshDisplay(M5DialDisplay& display, bool init) override { 168 | ESP_LOGD("DISPLAY", "refresh Display: Farbwahl-Modus"); 169 | if(init){ 170 | showColorMenu(display); 171 | } else { 172 | refreshColorMenu(display); 173 | } 174 | } 175 | 176 | void registerHAListener() override { 177 | std::string attrName = "hs_color"; 178 | api::global_api_server->subscribe_home_assistant_state( 179 | this->device.getEntityId().c_str(), 180 | attrName, 181 | [this](const std::string &state) { 182 | 183 | if(this->isValueModified()){ 184 | return; 185 | } 186 | 187 | std::string colorString = ""; 188 | std::string::size_type pos = state.find(','); 189 | 190 | if (pos != std::string::npos) { 191 | colorString = state.substr(1, pos-1); 192 | } 193 | ESP_LOGD("HA_API", "HS_Color value %s for %s", colorString.c_str(), this->device.getEntityId().c_str()); 194 | 195 | auto val = parse_number(colorString.c_str()); 196 | if (!val.has_value()) { 197 | this->setReceivedValue(0); 198 | ESP_LOGD("HA_API", "No Color value in %s for %s", colorString.c_str(), this->device.getEntityId().c_str()); 199 | } else { 200 | this->setReceivedValue(val.value()); 201 | ESP_LOGI("HA_API", "Got Color value %f for %s", val.value(), this->device.getEntityId().c_str()); 202 | } 203 | }); 204 | } 205 | 206 | bool onTouch(M5DialDisplay& display, uint16_t x, uint16_t y) override { 207 | uint16_t degree = display.getDegByCoord(x, y); 208 | setValue(degree); 209 | ESP_LOGD("TOUCH", "Neuen Farbwert auf %d gesetzt", degree); 210 | 211 | return true; 212 | } 213 | 214 | bool onRotary(M5DialDisplay& display, const char * direction) override { 215 | return this->defaultOnRotary(display, direction); 216 | } 217 | 218 | bool onButton(M5DialDisplay& display, const char * clickType) override { 219 | if (strcmp(clickType, BUTTON_SHORT)==0){ 220 | haApi.toggleLight(this->device.getEntityId()); 221 | return true; 222 | } 223 | return false; 224 | } 225 | 226 | 227 | }; 228 | } 229 | } -------------------------------------------------------------------------------- /components/shys_m5_dial/ha_device_mode_light_on_off.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace esphome 4 | { 5 | namespace shys_m5_dial 6 | { 7 | class HaDeviceModeLightOnOff: public esphome::shys_m5_dial::HaDeviceMode { 8 | protected: 9 | void sendValueToHomeAssistant(int value) override { 10 | if(getValue()<=0){ 11 | haApi.turnLightOff(this->device.getEntityId()); 12 | } else { 13 | haApi.turnLightOn(this->device.getEntityId()); 14 | } 15 | } 16 | 17 | void showOnOffMenu(M5DialDisplay& display){ 18 | LovyanGFX* gfx = display.getGfx(); 19 | 20 | uint16_t currentValue = getValue(); 21 | 22 | uint16_t height = gfx->height(); 23 | uint16_t width = gfx->width(); 24 | 25 | gfx->setTextColor(MAROON); 26 | gfx->setTextDatum(middle_center); 27 | 28 | gfx->startWrite(); // Secure SPI bus 29 | 30 | gfx->fillRect(0, 0, width, height, currentValue>0?YELLOW:RED); 31 | 32 | display.setFontsize(3); 33 | gfx->drawString(currentValue>0?"on":"off", 34 | width / 2, 35 | height / 2 - 30); 36 | 37 | display.setFontsize(1); 38 | gfx->drawString(this->device.getName().c_str(), 39 | width / 2, 40 | height / 2 + 20); 41 | gfx->drawString("On/Off", 42 | width / 2, 43 | height / 2 + 50); 44 | 45 | gfx->endWrite(); // Release SPI bus 46 | } 47 | 48 | public: 49 | HaDeviceModeLightOnOff(HaDevice& device) : HaDeviceMode(device){} 50 | 51 | void refreshDisplay(M5DialDisplay& display, bool init) override { 52 | this->showOnOffMenu(display); 53 | ESP_LOGD("DISPLAY", "An/Aus-Modus"); 54 | } 55 | 56 | void registerHAListener() override { 57 | std::string attrName = ""; 58 | api::global_api_server->subscribe_home_assistant_state( 59 | this->device.getEntityId().c_str(), 60 | attrName, 61 | [this](const std::string &state) { 62 | 63 | if(this->isValueModified()){ 64 | return; 65 | } 66 | 67 | int newState = strcmp("on", state.c_str())==0?1:0; 68 | 69 | this->setReceivedValue(newState); 70 | ESP_LOGI("HA_API", "Got value %s for %s", state.c_str(), this->device.getEntityId().c_str()); 71 | }); 72 | } 73 | 74 | bool onTouch(M5DialDisplay& display, uint16_t x, uint16_t y) override { 75 | haApi.toggleLight(this->device.getEntityId()); 76 | return true; 77 | } 78 | 79 | bool onRotary(M5DialDisplay& display, const char * direction) override { 80 | if(strcmp(direction, ROTARY_LEFT)==0){ 81 | haApi.turnLightOff(this->device.getEntityId()); 82 | } else if(strcmp(direction, ROTARY_RIGHT)==0){ 83 | haApi.turnLightOn(this->device.getEntityId()); 84 | } 85 | 86 | return true; 87 | } 88 | 89 | bool onButton(M5DialDisplay& display, const char * clickType) override { 90 | if (strcmp(clickType, BUTTON_SHORT)==0){ 91 | haApi.toggleLight(this->device.getEntityId()); 92 | return true; 93 | } 94 | return false; 95 | } 96 | 97 | }; 98 | } 99 | } -------------------------------------------------------------------------------- /components/shys_m5_dial/ha_device_mode_light_tunable_white.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | namespace esphome 5 | { 6 | namespace shys_m5_dial 7 | { 8 | class HaDeviceModeLightTunableWhite: public esphome::shys_m5_dial::HaDeviceMode { 9 | protected: 10 | void sendValueToHomeAssistant(int value) override { 11 | haApi.turnLightOnWhite(this->device.getEntityId(), value); 12 | } 13 | 14 | uint32_t colorTemperatureToRGB(int kelvin){ 15 | int temp = kelvin / 100; 16 | int red, green, blue; 17 | 18 | if( temp <= 66 ){ 19 | red = 255; 20 | green = temp; 21 | green = 99.4708025861 * log(green) - 161.1195681661; 22 | 23 | if( temp <= 19){ 24 | blue = 0; 25 | } else { 26 | blue = temp-10; 27 | blue = 138.5177312231 * log(blue) - 305.0447927307; 28 | } 29 | } else { 30 | red = temp - 60; 31 | red = 329.698727446 * pow(red, -0.1332047592); 32 | 33 | green = temp - 60; 34 | green = 288.1221695283 * pow(green, -0.0755148492 ); 35 | 36 | blue = 255; 37 | } 38 | 39 | return M5Dial.Display.color888(clamp(red, 0, 255), clamp(green, 0, 255), clamp(blue, 0, 255)); 40 | } 41 | 42 | 43 | int clamp(int x, int min, int max ) { 44 | if(xmax){ return max; } 46 | return x; 47 | } 48 | 49 | 50 | void showTunableWhiteMenu(M5DialDisplay& display, uint16_t currentValue){ 51 | LovyanGFX* gfx = display.getGfx(); 52 | 53 | uint16_t height = gfx->height(); 54 | uint16_t width = gfx->width(); 55 | 56 | uint16_t ypos = getDisplayPositionY(currentValue); 57 | 58 | gfx->setTextColor(MAROON); 59 | gfx->setTextDatum(middle_center); 60 | 61 | gfx->startWrite(); // Secure SPI bus 62 | 63 | gfx->fillRect(0, 0, width, height, colorTemperatureToRGB(currentValue) ); 64 | 65 | gfx->drawLine(0, ypos, width, ypos, RED ); 66 | 67 | display.setFontsize(3); 68 | gfx->drawString((String(currentValue) + "K").c_str(), 69 | width / 2, 70 | height / 2 - 30); 71 | 72 | display.setFontsize(1); 73 | gfx->drawString(this->device.getName().c_str(), 74 | width / 2, 75 | height / 2 + 20); 76 | gfx->drawString("White", 77 | width / 2, 78 | height / 2 + 50); 79 | 80 | gfx->endWrite(); // Release SPI bus 81 | } 82 | 83 | public: 84 | HaDeviceModeLightTunableWhite(HaDevice& device) : HaDeviceMode(device){ 85 | this->value = 2000; 86 | this->minValue = 2000; 87 | this->maxValue = 6500; 88 | this->endlessRotaryValue = false; 89 | } 90 | 91 | void refreshDisplay(M5DialDisplay& display, bool init) override { 92 | ESP_LOGD("DISPLAY", "refresh Display: Tunable White-Modus"); 93 | this->showTunableWhiteMenu(display, getValue()); 94 | } 95 | 96 | void registerHAListener() override { 97 | std::string attrName = "color_temp_kelvin"; 98 | api::global_api_server->subscribe_home_assistant_state( 99 | this->device.getEntityId().c_str(), 100 | attrName, 101 | [this](const std::string &state) { 102 | 103 | if(this->isValueModified()){ 104 | return; 105 | } 106 | 107 | auto val = parse_number(state); 108 | if (!val.has_value()) { 109 | this->setReceivedValue(0); 110 | ESP_LOGD("HA_API", "No Kelvin value in %s for %s", state.c_str(), this->device.getEntityId().c_str()); 111 | } else { 112 | this->setReceivedValue(round((float)val.value())); 113 | ESP_LOGI("HA_API", "Got Kelvin value %i for %s", val.value(), this->device.getEntityId().c_str()); 114 | } 115 | }); 116 | } 117 | 118 | bool onTouch(M5DialDisplay& display, uint16_t x, uint16_t y) override { 119 | return defaultOnTouch(display, x, y); 120 | } 121 | 122 | bool onRotary(M5DialDisplay& display, const char * direction) override { 123 | return defaultOnRotary(display, direction); 124 | } 125 | 126 | bool onButton(M5DialDisplay& display, const char * clickType) override { 127 | if (strcmp(clickType, BUTTON_SHORT)==0){ 128 | haApi.toggleLight(this->device.getEntityId()); 129 | return true; 130 | } 131 | return false; 132 | } 133 | 134 | }; 135 | } 136 | } -------------------------------------------------------------------------------- /components/shys_m5_dial/ha_device_mode_lock.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "globals.h" 3 | #include "global_image_data.h" 4 | 5 | namespace esphome 6 | { 7 | namespace shys_m5_dial 8 | { 9 | class HaDeviceModeLock: public esphome::shys_m5_dial::HaDeviceMode { 10 | protected: 11 | bool openOnButton = false; 12 | std::string lockState = "unknown"; 13 | 14 | long openingTime = 0; 15 | 16 | 17 | bool isOpenOnButton(){ 18 | return this->openOnButton; 19 | } 20 | 21 | bool isOpen(){ 22 | return strcmp(lockState.c_str(), "open")==0 || strcmp(lockState.c_str(), "opening")==0; 23 | } 24 | 25 | bool isLocked(){ 26 | return strcmp(lockState.c_str(), "locked")==0 || strcmp(lockState.c_str(), "locking")==0; 27 | } 28 | 29 | bool isUnlocked(){ 30 | return strcmp(lockState.c_str(), "unlocked")==0 || strcmp(lockState.c_str(), "unlocking")==0; 31 | } 32 | 33 | std::string getLockState(){ 34 | if(openingTime + 5000 > esphome::millis()){ 35 | return "Opening"; 36 | } 37 | return this->lockState; 38 | } 39 | 40 | void sendValueToHomeAssistant(int value) override { 41 | switch(value){ 42 | case 0: 43 | this->openLock(); 44 | break; 45 | case 1: 46 | haApi.unlockLock(this->device.getEntityId()); 47 | break; 48 | case 2: 49 | haApi.lockLock(this->device.getEntityId()); 50 | break; 51 | } 52 | } 53 | 54 | void showLockStatus(M5DialDisplay& display){ 55 | LovyanGFX* gfx = display.getGfx(); 56 | uint16_t height = gfx->height(); 57 | uint16_t width = gfx->width(); 58 | 59 | gfx->setTextColor(MAROON); 60 | gfx->setTextDatum(middle_center); 61 | 62 | gfx->startWrite(); // Secure SPI bus 63 | 64 | gfx->fillRect(0, 0, width, height, YELLOW); 65 | 66 | display.setFontsize(2); 67 | gfx->drawString(this->getLockState().c_str(), 68 | width / 2, 69 | height / 2 - 15); 70 | 71 | display.setFontsize(1); 72 | gfx->drawString(this->device.getName().c_str(), 73 | width / 2, 74 | height / 2 - 80); 75 | gfx->drawString("Lock", 76 | width / 2, 77 | height / 2 - 60); 78 | 79 | display.drawBitmapTransparent(OPEN_DOOR_IMG, width/2-35, height/2+30, 70, 70, 0xFFFF); 80 | display.setFontsize(.7); 81 | gfx->drawString("Open", 82 | width / 2, 83 | height / 2 +105); 84 | 85 | gfx->endWrite(); // Release SPI bus 86 | } 87 | 88 | void setReceivedLockState(const std::string& newState){ 89 | this->lockState = newState; 90 | 91 | if (this->isLocked()){ 92 | this->setReceivedValue(2); 93 | } else if (this->isUnlocked()){ 94 | this->setReceivedValue(1); 95 | } else if (this->isOpen()){ 96 | this->setReceivedValue(0); 97 | } 98 | this->displayRefreshNeeded = true; 99 | } 100 | 101 | void openLock(){ 102 | haApi.openLock(this->device.getEntityId()); 103 | this->openingTime = esphome::millis(); 104 | } 105 | 106 | public: 107 | HaDeviceModeLock(HaDevice& device) : HaDeviceMode(device){ 108 | setMaxValue(2); 109 | } 110 | 111 | void setOpenOnButton(bool openOnBtn){ 112 | this->openOnButton = openOnBtn; 113 | } 114 | 115 | 116 | void refreshDisplay(M5DialDisplay& display, bool init) override { 117 | ESP_LOGD("DISPLAY", "refresh Display: Lock-Modus"); 118 | this->showLockStatus(display); 119 | 120 | this->displayRefreshNeeded = false; 121 | } 122 | 123 | void registerHAListener() override { 124 | std::string attrNameState = ""; 125 | api::global_api_server->subscribe_home_assistant_state( 126 | this->device.getEntityId().c_str(), 127 | attrNameState, 128 | [this](const std::string &state) { 129 | 130 | if(this->isValueModified()){ 131 | return; 132 | } 133 | 134 | this->setReceivedLockState(state); 135 | ESP_LOGI("HA_API", "Got State %s for %s", state.c_str(), this->device.getEntityId().c_str()); 136 | }); 137 | } 138 | 139 | bool onButton(M5DialDisplay& display, const char * clickType) override { 140 | if (strcmp(clickType, BUTTON_SHORT)==0){ 141 | if(this->isLocked()){ 142 | if(this->isOpenOnButton()){ 143 | this->openLock(); 144 | return true; 145 | } else { 146 | haApi.unlockLock(this->device.getEntityId()); 147 | return true; 148 | } 149 | } else if(this->isUnlocked() || this->isOpen()){ 150 | haApi.lockLock(this->device.getEntityId()); 151 | return true; 152 | } 153 | } 154 | 155 | this->displayRefreshNeeded = true; 156 | 157 | return false; 158 | } 159 | 160 | bool onRotary(M5DialDisplay& display, const char * direction) override { 161 | this->defaultOnRotary(display, direction); 162 | 163 | if(getValue()==0){ 164 | this->openingTime = esphome::millis(); 165 | } 166 | 167 | return true; 168 | } 169 | 170 | bool onTouch(M5DialDisplay& display, uint16_t x, uint16_t y) override { 171 | ESP_LOGI("TOUCH", "Touch %i / %i for %s", x, y, this->device.getEntityId().c_str()); 172 | 173 | LovyanGFX* gfx = display.getGfx(); 174 | uint16_t height = gfx->height(); 175 | uint16_t width = gfx->width(); 176 | 177 | uint16_t minOpenY = height/2+30; 178 | uint16_t maxOpenY = height/2+100; 179 | uint16_t minOpenX = width/2-40; 180 | uint16_t maxOpenX = width/2+40; 181 | 182 | if(y > minOpenY && y < maxOpenY){ 183 | if(x>minOpenX && xopenLock(); 186 | return true; 187 | } 188 | } 189 | 190 | return false; 191 | } 192 | 193 | void onLoop(){ 194 | if(openingTime>0 && openingTime + 7000 < esphome::millis()){ 195 | this->displayRefreshNeeded = true; 196 | openingTime = 0; 197 | } 198 | } 199 | }; 200 | } 201 | } -------------------------------------------------------------------------------- /components/shys_m5_dial/ha_device_mode_mediaplayer_play.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace esphome 4 | { 5 | namespace shys_m5_dial 6 | { 7 | class HaDeviceModeMediaPlayerPlay: public esphome::shys_m5_dial::HaDeviceMode { 8 | protected: 9 | std::string player_state = ""; 10 | 11 | std::string media_title = ""; 12 | std::string media_artist = ""; 13 | std::string media_album_name = ""; 14 | 15 | uint16_t media_duration = 0; 16 | uint16_t media_position = 0; 17 | 18 | unsigned long lastRefresh = 0; 19 | 20 | void sendValueToHomeAssistant(int value) override { 21 | haApi.setMediaPlayerVolume(this->device.getEntityId(), value); 22 | } 23 | 24 | float getMediaPositionPct(){ 25 | float result = 0; 26 | 27 | if(strcmp(this->player_state.c_str(), "playing")==0){ 28 | if(this->media_duration > 0 && this->media_position <= this->media_duration){ 29 | this->media_position/this->media_duration*100; 30 | } 31 | } 32 | 33 | return result; 34 | } 35 | 36 | void showPlayMenu(M5DialDisplay& display){ 37 | LovyanGFX* gfx = display.getGfx(); 38 | uint16_t height = gfx->height(); 39 | uint16_t width = gfx->width(); 40 | 41 | gfx->setTextColor(MAROON); 42 | gfx->setTextDatum(middle_center); 43 | 44 | gfx->startWrite(); // Secure SPI bus 45 | 46 | gfx->fillRect(0, 0, width, height, YELLOW); 47 | 48 | // Round Volume Bar 49 | gfx->fillArc(width / 2, 50 | height / 2, 51 | 115, 52 | 100, 53 | 150, 54 | getValue()==0?150:(((float)240 / 100) * getValue()) + 150, 55 | RED 56 | ); 57 | 58 | gfx->fillArc(width / 2, 59 | height / 2, 60 | 115, 61 | 100, 62 | getValue()==0?150:(((float)240 / 100) * getValue()) + 150, 63 | 390, 64 | ORANGE 65 | ); 66 | 67 | // Percent 68 | display.setFontsize(2); 69 | gfx->drawString((String(getValue()) + "%").c_str(), 70 | width / 2, 71 | height / 2 - 45); 72 | 73 | // Device Name 74 | display.setFontsize(1); 75 | gfx->drawString(this->device.getName().c_str(), 76 | width / 2, 77 | height / 2 - 80); 78 | 79 | 80 | 81 | // Media-Artist/Title 82 | display.setFontsize(.7); 83 | bool displayTitle = ((millis() / 5000) % 2 == 1); 84 | if(displayTitle){ 85 | gfx->drawString(this->media_title.c_str(), 86 | width / 2, 87 | height / 2 + 70); 88 | } else { 89 | gfx->drawString(this->media_artist.c_str(), 90 | width / 2, 91 | height / 2 + 70); 92 | } 93 | 94 | // Position Bar 95 | gfx->fillRect(width/2-40, height/2+90, 80, 5, ORANGE); 96 | gfx->fillRect(width/2-40, height/2+90, ((float)80 /100* getMediaPositionPct()), 5, RED); 97 | 98 | 99 | 100 | if(strcmp(this->player_state.c_str(), "playing") == 0){ 101 | // Pause Button 102 | gfx->fillRect(width/2-20, height/2-20, 15, 40, RED); 103 | gfx->fillRect(width/2+5, height/2-20, 15, 40, RED); 104 | } else { 105 | // Play Button 106 | M5Dial.Display.fillTriangle(width/2-13, height/2-20, width/2-13, height/2+20, width/2+23, height/2, RED); 107 | } 108 | 109 | // FWD 110 | M5Dial.Display.fillTriangle(width/2+50, height/2-20, width/2+50, height/2+20, width/2+75, height/2, RED); 111 | M5Dial.Display.fillTriangle(width/2+65, height/2-20, width/2+65, height/2+20, width/2+95, height/2, RED); 112 | 113 | // PREV 114 | M5Dial.Display.fillTriangle(width/2-50, height/2-20, width/2-50, height/2+20, width/2-75, height/2, RED); 115 | M5Dial.Display.fillTriangle(width/2-65, height/2-20, width/2-65, height/2+20, width/2-95, height/2, RED); 116 | 117 | 118 | gfx->endWrite(); // Release SPI bus 119 | } 120 | 121 | public: 122 | HaDeviceModeMediaPlayerPlay(HaDevice& device) : HaDeviceMode(device){} 123 | 124 | void refreshDisplay(M5DialDisplay& display, bool init) override { 125 | this->showPlayMenu(display); 126 | ESP_LOGD("DISPLAY", "Play-Modus"); 127 | 128 | this->displayRefreshNeeded = false; 129 | } 130 | 131 | void registerHAListener() override { 132 | std::string volAttrName = "volume_level"; 133 | api::global_api_server->subscribe_home_assistant_state( 134 | this->device.getEntityId().c_str(), 135 | volAttrName, 136 | [this](const std::string &state) { 137 | 138 | if(this->isValueModified()){ 139 | return; 140 | } 141 | 142 | ESP_LOGI("HA_API", "Got Volume value %s for %s", state.c_str(), this->device.getEntityId().c_str()); 143 | auto val = parse_number(state); 144 | ESP_LOGI("HA_API", "Parsed Volume value %f for %s", val.value(), this->device.getEntityId().c_str()); 145 | 146 | if (!val.has_value()) { 147 | this->setReceivedValue(0); 148 | ESP_LOGD("HA_API", "No Volume value in %s for %s", state.c_str(), this->device.getEntityId().c_str()); 149 | } else { 150 | this->setReceivedValue(val.value()*100); 151 | ESP_LOGI("HA_API", "Set Volume value %f for %s", val.value(), this->device.getEntityId().c_str()); 152 | 153 | } 154 | }); 155 | 156 | 157 | std::string stateAttrName = ""; 158 | api::global_api_server->subscribe_home_assistant_state( 159 | this->device.getEntityId().c_str(), 160 | stateAttrName, 161 | [this](const std::string &state) { 162 | 163 | if(this->isValueModified()){ 164 | return; 165 | } 166 | 167 | this->player_state = state; 168 | 169 | this->displayRefreshNeeded = true; 170 | ESP_LOGI("HA_API", "Got State %s for %s", state.c_str(), this->device.getEntityId().c_str()); 171 | }); 172 | 173 | std::string titleAttrName = "media_title"; 174 | api::global_api_server->subscribe_home_assistant_state( 175 | this->device.getEntityId().c_str(), 176 | titleAttrName, 177 | [this](const std::string &state) { 178 | 179 | if(this->isValueModified()){ 180 | return; 181 | } 182 | 183 | this->media_title = state; 184 | 185 | this->displayRefreshNeeded = true; 186 | ESP_LOGI("HA_API", "Got Title %s for %s", state.c_str(), this->device.getEntityId().c_str()); 187 | }); 188 | 189 | std::string artistAttrName = "media_artist"; 190 | api::global_api_server->subscribe_home_assistant_state( 191 | this->device.getEntityId().c_str(), 192 | artistAttrName, 193 | [this](const std::string &state) { 194 | 195 | if(this->isValueModified()){ 196 | return; 197 | } 198 | 199 | this->media_artist = state; 200 | 201 | this->displayRefreshNeeded = true; 202 | ESP_LOGI("HA_API", "Got Artist %s for %s", state.c_str(), this->device.getEntityId().c_str()); 203 | }); 204 | 205 | std::string albumAttrName = "media_album_name"; 206 | api::global_api_server->subscribe_home_assistant_state( 207 | this->device.getEntityId().c_str(), 208 | albumAttrName, 209 | [this](const std::string &state) { 210 | 211 | if(this->isValueModified()){ 212 | return; 213 | } 214 | 215 | this->media_album_name = state; 216 | 217 | this->displayRefreshNeeded = true; 218 | ESP_LOGI("HA_API", "Got Album %s for %s", state.c_str(), this->device.getEntityId().c_str()); 219 | }); 220 | 221 | std::string durationAttrName = "media_duration"; 222 | api::global_api_server->subscribe_home_assistant_state( 223 | this->device.getEntityId().c_str(), 224 | durationAttrName, 225 | [this](const std::string &state) { 226 | 227 | if(this->isValueModified()){ 228 | return; 229 | } 230 | 231 | auto val = parse_number(state); 232 | if (!val.has_value()) { 233 | this->media_duration = 0; 234 | ESP_LOGD("HA_API", "No Media-Duration in %s for %s", state.c_str(), this->device.getEntityId().c_str()); 235 | } else { 236 | this->media_duration = (int)val.value(); 237 | ESP_LOGI("HA_API", "Got Media-Duration %i for %s", val.value(), this->device.getEntityId().c_str()); 238 | } 239 | }); 240 | 241 | std::string positionAttrName = "media_position"; 242 | api::global_api_server->subscribe_home_assistant_state( 243 | this->device.getEntityId().c_str(), 244 | positionAttrName, 245 | [this](const std::string &state) { 246 | 247 | if(this->isValueModified()){ 248 | return; 249 | } 250 | 251 | auto val = parse_number(state); 252 | if (!val.has_value()) { 253 | this->media_position = 0; 254 | ESP_LOGD("HA_API", "No Media-Position value in %s for %s", state.c_str(), this->device.getEntityId().c_str()); 255 | } else { 256 | this->media_position = (int)val.value(); 257 | this->displayRefreshNeeded = true; 258 | ESP_LOGI("HA_API", "Got Media-Position value %i for %s", val.value(), this->device.getEntityId().c_str()); 259 | } 260 | }); 261 | 262 | } 263 | 264 | bool onTouch(M5DialDisplay& display, uint16_t x, uint16_t y) override { 265 | ESP_LOGI("TOUCH", "Touch %i / %i for %s", x, y, this->device.getEntityId().c_str()); 266 | 267 | LovyanGFX* gfx = display.getGfx(); 268 | uint16_t height = gfx->height(); 269 | uint16_t width = gfx->width(); 270 | 271 | uint16_t minHeight = height/2-30; 272 | uint16_t maxHeight = height/2+30; 273 | 274 | uint16_t minPrevX = width/2-110; 275 | uint16_t maxPrevX = width/2-50; 276 | 277 | uint16_t minPlayX = width/2-30; 278 | uint16_t maxPlayX = width/2+30; 279 | 280 | uint16_t minNextX = width/2+50; 281 | uint16_t maxNextX = width/2+110; 282 | 283 | if(y > minHeight && y < maxHeight){ 284 | if(x>minPrevX && xdevice.getEntityId()); 287 | return true; 288 | 289 | } else if(x>minPlayX && xdevice.getEntityId()); 292 | return true; 293 | 294 | } else if(x>minNextX && xdevice.getEntityId()); 297 | return true; 298 | } 299 | } 300 | 301 | return false; 302 | } 303 | 304 | bool onRotary(M5DialDisplay& display, const char * direction) override { 305 | this->defaultOnRotary(display, direction); 306 | return true; 307 | } 308 | 309 | bool onButton(M5DialDisplay& display, const char * clickType) override { 310 | if (strcmp(clickType, BUTTON_SHORT)==0){ 311 | haApi.playPauseMediaPlayer(this->device.getEntityId()); 312 | return true; 313 | } 314 | return false; 315 | } 316 | 317 | void onLoop() override { 318 | if(strcmp(this->player_state.c_str(), "playing")==0){ 319 | bool timebasedRefresh = (this->lastRefresh + 2000) < esphome::millis(); 320 | if(timebasedRefresh){ 321 | this->lastRefresh = esphome::millis(); 322 | 323 | haApi.updateEntity(this->device.getEntityId().c_str()); 324 | } 325 | } 326 | } 327 | 328 | }; 329 | } 330 | } -------------------------------------------------------------------------------- /components/shys_m5_dial/ha_device_mode_mediaplayer_source.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace esphome 4 | { 5 | namespace shys_m5_dial 6 | { 7 | class HaDeviceModeMediaPlayerSource: public esphome::shys_m5_dial::HaDeviceMode { 8 | protected: 9 | struct MediaContent { 10 | std::string name; 11 | std::string content_id; 12 | std::string content_type; 13 | }; 14 | 15 | std::vector mediaContents = {}; 16 | 17 | void sendValueToHomeAssistant(int value) override { 18 | // not choose Playlist on change value. 19 | // selection activated by button pressed 20 | } 21 | 22 | void showSourceSelection(M5DialDisplay& display){ 23 | LovyanGFX* gfx = display.getGfx(); 24 | uint16_t height = gfx->height(); 25 | uint16_t width = gfx->width(); 26 | 27 | gfx->setTextColor(MAROON); 28 | gfx->setTextDatum(middle_center); 29 | 30 | gfx->startWrite(); // Secure SPI bus 31 | 32 | gfx->fillRect(0, 0, width, height, YELLOW); 33 | 34 | // Device Name 35 | display.setFontsize(1); 36 | gfx->drawString(this->device.getName().c_str(), 37 | width / 2, 38 | height / 2 - 80); 39 | 40 | if(this->getValue()>0){ 41 | display.setFontsize(.7); 42 | gfx->drawString(this->mediaContents[this->getValue()-1]->name.c_str(), 43 | width / 2, 44 | height / 2-40); 45 | } 46 | 47 | display.setFontsize(1.5); 48 | gfx->drawString(this->mediaContents[this->getValue()]->name.c_str(), 49 | width / 2, 50 | height / 2); 51 | 52 | if(this->getValue()getMaxValue()){ 53 | display.setFontsize(.7); 54 | gfx->drawString(this->mediaContents[this->getValue()+1]->name.c_str(), 55 | width / 2, 56 | height / 2+40); 57 | } 58 | 59 | gfx->endWrite(); // Release SPI bus 60 | } 61 | 62 | public: 63 | HaDeviceModeMediaPlayerSource(HaDevice& device) : HaDeviceMode(device){ 64 | this->setMaxValue(0); 65 | } 66 | 67 | void refreshDisplay(M5DialDisplay& display, bool init) override { 68 | this->showSourceSelection(display); 69 | ESP_LOGD("DISPLAY", "Source-Modus"); 70 | 71 | this->displayRefreshNeeded = false; 72 | } 73 | 74 | void registerHAListener() override { 75 | } 76 | 77 | bool onRotary(M5DialDisplay& display, const char * direction) override { 78 | this->defaultOnRotary(display, direction); 79 | return true; 80 | } 81 | 82 | bool onButton(M5DialDisplay& display, const char * clickType) override { 83 | if (strcmp(clickType, BUTTON_SHORT)==0){ 84 | const std::string content_id = this->mediaContents[this->getValue()]->content_id.c_str(); 85 | const std::string content_type = this->mediaContents[this->getValue()]->content_type.c_str(); 86 | 87 | haApi.playMediaOnMediaPlayer(this->device.getEntityId(), content_id, content_type); 88 | 89 | delay(300); 90 | haApi.updateEntity(this->device.getEntityId().c_str()); 91 | 92 | return true; 93 | } 94 | 95 | return false; 96 | } 97 | 98 | /** 99 | * 100 | */ 101 | void loadMediaContents(JsonObject modeConfig){ 102 | this->mediaContents = {}; 103 | 104 | if (modeConfig.containsKey("source_mode")) { 105 | JsonObject sourceModeConfig = modeConfig["source_mode"]; 106 | 107 | if (sourceModeConfig.containsKey("sources")) { 108 | JsonArray sources = sourceModeConfig["sources"].as(); 109 | 110 | for(JsonObject source : sources){ 111 | MediaContent* content = new MediaContent; 112 | content->name = source["name"].as(); 113 | content->content_id = source["content_id"].as(); 114 | content->content_type = source["content_type"].as(); 115 | 116 | this->mediaContents.push_back(content); 117 | ESP_LOGD("MEDIAPLAYER", "Add Source: %s on %s", content->name.c_str(), this->device.getEntityId().c_str()); 118 | } 119 | this->setMaxValue(this->mediaContents.size()-1); 120 | } 121 | } 122 | } 123 | 124 | }; 125 | } 126 | } -------------------------------------------------------------------------------- /components/shys_m5_dial/ha_device_mode_percentage.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace esphome 4 | { 5 | namespace shys_m5_dial 6 | { 7 | class HaDeviceModePercentage: public esphome::shys_m5_dial::HaDeviceMode { 8 | protected: 9 | void showPercentageMenu(M5DialDisplay& display){ 10 | LovyanGFX* gfx = display.getGfx(); 11 | 12 | uint16_t height = gfx->height(); 13 | uint16_t width = gfx->width(); 14 | 15 | gfx->setTextColor(MAROON); 16 | gfx->setTextDatum(middle_center); 17 | 18 | gfx->startWrite(); // Secure SPI bus 19 | 20 | gfx->fillRect(0, 0, width, this->getDisplayPositionY(getValue()) , RED); 21 | gfx->fillRect(0, this->getDisplayPositionY(getValue()), width, height, YELLOW); 22 | 23 | display.setFontsize(3); 24 | gfx->drawString((String(getValue()) + "%").c_str(), 25 | width / 2, 26 | height / 2 - 30); 27 | 28 | display.setFontsize(1); 29 | gfx->drawString(this->device.getName().c_str(), 30 | width / 2, 31 | height / 2 + 20); 32 | gfx->drawString("Percentage", 33 | width / 2, 34 | height / 2 + 50); 35 | 36 | gfx->endWrite(); // Release SPI bus 37 | } 38 | 39 | public: 40 | HaDeviceModePercentage(HaDevice& device) : HaDeviceMode(device){} 41 | 42 | void refreshDisplay(M5DialDisplay& display, bool init) override { 43 | ESP_LOGD("DISPLAY", "refresh Display: Percentage-Modus"); 44 | showPercentageMenu(display); 45 | } 46 | 47 | bool onTouch(M5DialDisplay& display, uint16_t x, uint16_t y) override { 48 | return this->defaultOnTouch(display, x, y); 49 | } 50 | 51 | bool onRotary(M5DialDisplay& display, const char * direction) override { 52 | return this->defaultOnRotary(display, direction); 53 | } 54 | }; 55 | } 56 | } -------------------------------------------------------------------------------- /components/shys_m5_dial/ha_device_mode_switch_on_off.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace esphome 4 | { 5 | namespace shys_m5_dial 6 | { 7 | class HaDeviceModeSwitchOnOff: public esphome::shys_m5_dial::HaDeviceMode { 8 | protected: 9 | void sendValueToHomeAssistant(int value) override { 10 | if(getValue()<=0){ 11 | haApi.turnSwitchOff(this->device.getEntityId()); 12 | } else { 13 | haApi.turnSwitchOn(this->device.getEntityId()); 14 | } 15 | } 16 | 17 | void showOnOffMenu(M5DialDisplay& display){ 18 | LovyanGFX* gfx = display.getGfx(); 19 | 20 | uint16_t currentValue = getValue(); 21 | 22 | uint16_t height = gfx->height(); 23 | uint16_t width = gfx->width(); 24 | 25 | gfx->setTextColor(MAROON); 26 | gfx->setTextDatum(middle_center); 27 | 28 | gfx->startWrite(); // Secure SPI bus 29 | 30 | gfx->fillRect(0, 0, width, height, currentValue>0?YELLOW:RED); 31 | 32 | display.setFontsize(3); 33 | gfx->drawString(currentValue>0?"on":"off", 34 | width / 2, 35 | height / 2 - 30); 36 | 37 | display.setFontsize(1); 38 | gfx->drawString(this->device.getName().c_str(), 39 | width / 2, 40 | height / 2 + 20); 41 | gfx->drawString("On/Off", 42 | width / 2, 43 | height / 2 + 50); 44 | 45 | gfx->endWrite(); // Release SPI bus 46 | } 47 | 48 | public: 49 | HaDeviceModeSwitchOnOff(HaDevice& device) : HaDeviceMode(device){} 50 | 51 | void refreshDisplay(M5DialDisplay& display, bool init) override { 52 | this->showOnOffMenu(display); 53 | ESP_LOGD("DISPLAY", "An/Aus-Modus"); 54 | } 55 | 56 | void registerHAListener() override { 57 | std::string attrName = ""; 58 | api::global_api_server->subscribe_home_assistant_state( 59 | this->device.getEntityId().c_str(), 60 | attrName, 61 | [this](const std::string &state) { 62 | 63 | if(this->isValueModified()){ 64 | return; 65 | } 66 | 67 | int newState = strcmp("on", state.c_str())==0?1:0; 68 | 69 | this->setReceivedValue(newState); 70 | ESP_LOGI("HA_API", "Got value %s for %s", state.c_str(), this->device.getEntityId().c_str()); 71 | }); 72 | } 73 | 74 | bool onTouch(M5DialDisplay& display, uint16_t x, uint16_t y) override { 75 | haApi.toggleSwitch(this->device.getEntityId()); 76 | return true; 77 | } 78 | 79 | bool onRotary(M5DialDisplay& display, const char * direction) override { 80 | if(strcmp(direction, ROTARY_LEFT)==0){ 81 | haApi.turnSwitchOff(this->device.getEntityId()); 82 | } else if(strcmp(direction, ROTARY_RIGHT)==0){ 83 | haApi.turnSwitchOn(this->device.getEntityId()); 84 | } 85 | 86 | return true; 87 | } 88 | 89 | bool onButton(M5DialDisplay& display, const char * clickType) override { 90 | if (strcmp(clickType, BUTTON_SHORT)==0){ 91 | haApi.toggleSwitch(this->device.getEntityId()); 92 | return true; 93 | } 94 | return false; 95 | } 96 | 97 | }; 98 | } 99 | } -------------------------------------------------------------------------------- /components/shys_m5_dial/ha_device_switch.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "ha_device.h" 3 | #include "ha_device_mode_switch_on_off.h" 4 | 5 | namespace esphome 6 | { 7 | namespace shys_m5_dial 8 | { 9 | class HaDeviceSwitch: public esphome::shys_m5_dial::HaDevice { 10 | protected: 11 | HaDeviceModeSwitchOnOff* modeOnOff = new HaDeviceModeSwitchOnOff(*this); 12 | 13 | public: 14 | HaDeviceSwitch(const std::string& entity_id, const std::string& name, const std::string& modes) : HaDevice(entity_id, name, modes) {} 15 | 16 | void init() override { 17 | ESP_LOGD("HA_DEVICE", "Init Switch: %s", this->getEntityId().c_str()); 18 | 19 | this->addMode(modeOnOff); 20 | } 21 | 22 | }; 23 | 24 | } 25 | } -------------------------------------------------------------------------------- /components/shys_m5_dial/m5dial_display.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "M5Dial.h" 3 | #include "esphome.h" 4 | 5 | namespace esphome 6 | { 7 | namespace shys_m5_dial 8 | { 9 | class M5DialDisplay { 10 | protected: 11 | LovyanGFX* gfx = &M5Dial.Display; 12 | 13 | int timeToScreenOff = 30000; 14 | unsigned long lastEvent = 0; 15 | uint16_t lastMode = -1; 16 | 17 | std::string fontName = "FreeMono12pt7b"; 18 | float fontFactor = 1; 19 | 20 | public: 21 | M5DialDisplay() { 22 | } 23 | 24 | void setTimeToScreenOff(int value){ 25 | this->timeToScreenOff = value; 26 | } 27 | void resetLastEventTimer(){ 28 | lastEvent = esphome::millis(); 29 | } 30 | 31 | uint16_t getHeight(){ 32 | return M5Dial.Display.height(); 33 | } 34 | uint16_t getWidth(){ 35 | return M5Dial.Display.width(); 36 | } 37 | 38 | LovyanGFX* getGfx() { 39 | return gfx; 40 | } 41 | 42 | void setFontName(std::string name){ 43 | this->fontName = name; 44 | } 45 | 46 | void setFontFactor(float factor){ 47 | this->fontFactor = factor; 48 | } 49 | 50 | bool isDisplayOn(){ 51 | return M5Dial.Display.getBrightness() > 0; 52 | } 53 | 54 | void validateTimeout(){ 55 | if (esphome::millis() - lastEvent > timeToScreenOff ) { 56 | if(M5Dial.Display.getBrightness()>0){ 57 | M5Dial.Display.setBrightness(0); 58 | ESP_LOGI("DISPLAY", "Sleep after %d ms", timeToScreenOff); 59 | } 60 | } else if ( M5Dial.Display.getBrightness()<=0 ) { 61 | M5Dial.Display.setBrightness(100); 62 | ESP_LOGI("DISPLAY", "Display on"); 63 | } 64 | } 65 | 66 | void showOffline(){ 67 | uint16_t height = this->getHeight(); 68 | uint16_t width = this->getWidth(); 69 | 70 | gfx->setTextColor(LIGHTGREY); 71 | gfx->setTextDatum(middle_center); 72 | 73 | this->setFontByName(this->fontName); 74 | 75 | gfx->startWrite(); // Secure SPI bus 76 | gfx->fillRect(0, 0, width, height, DARKGREY); 77 | 78 | this->setFontsize(2); 79 | gfx->drawString("OFFLINE", 80 | width / 2, 81 | height / 2); 82 | 83 | gfx->endWrite(); // Release SPI bus 84 | } 85 | 86 | void showDisconnected(){ 87 | uint16_t height = this->getHeight(); 88 | uint16_t width = this->getWidth(); 89 | 90 | gfx->setTextColor(WHITE); 91 | gfx->setTextDatum(middle_center); 92 | 93 | this->setFontByName(this->fontName); 94 | 95 | gfx->startWrite(); // Secure SPI bus 96 | gfx->fillRect(0, 0, width, height, BLUE); 97 | 98 | this->setFontsize(1); 99 | gfx->drawString("DISCONNECTED", 100 | width / 2, 101 | height / 2); 102 | 103 | gfx->endWrite(); // Release SPI bus 104 | } 105 | 106 | void showUnknown(){ 107 | uint16_t height = this->getHeight(); 108 | uint16_t width = this->getWidth(); 109 | 110 | gfx->setTextColor(MAROON); 111 | gfx->setTextDatum(middle_center); 112 | 113 | this->setFontByName(this->fontName); 114 | 115 | gfx->startWrite(); // Secure SPI bus 116 | gfx->fillRect(0, 0, width, height, ORANGE); 117 | 118 | this->setFontsize(2); 119 | 120 | gfx->drawString("UNKNOWN", 121 | width / 2, 122 | height / 2); 123 | 124 | gfx->endWrite(); // Release SPI bus 125 | } 126 | 127 | float getDegByCoord(uint16_t x, uint16_t y){ 128 | float mx = M5Dial.Display.width()/2; 129 | float my = M5Dial.Display.height()/2; 130 | 131 | float angle = atan2(y - my, x - mx) * 180.0 / M_PI; 132 | angle = 360 - fmod((angle + 360.0 - 90), 360.0); 133 | 134 | return angle; 135 | } 136 | 137 | void setFontsize(float size) { 138 | getGfx()->setTextSize(size * this->fontFactor); 139 | } 140 | 141 | int getRowHeight(float fontSize){ 142 | return (int)this->fontFactor * fontSize; 143 | } 144 | 145 | void setFontByName(const std::string& name) { 146 | if (FONT_MAP.find(name) != FONT_MAP.end()) { 147 | this->setFontName(name); 148 | } else { 149 | this->setFontName("FreeMono12pt7b"); 150 | ESP_LOGE("DISPLAY", "Font '%s' not found, using default font: 'FreeMono12pt7b'", name.c_str()); 151 | } 152 | getGfx()->setFont(FONT_MAP[this->fontName]); 153 | } 154 | 155 | void drawBitmap(const uint8_t* bmp, int size, uint8_t x, uint8_t y, uint8_t width, uint8_t height){ 156 | M5Dial.Display.drawJpg(bmp, size, x, y, width, height, 0, 0); 157 | } 158 | 159 | void drawBitmapTransparent(const uint16_t* bmp, uint8_t x, uint8_t y, uint8_t width, uint8_t height, uint32_t transparentColor){ 160 | M5Dial.Display.pushImage(x, y, width, height, bmp, transparentColor); 161 | } 162 | 163 | }; 164 | } 165 | } -------------------------------------------------------------------------------- /components/shys_m5_dial/m5dial_eeprom.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "EEPROM.h" 3 | 4 | #define EEPROM_SIZE 32 5 | 6 | namespace esphome 7 | { 8 | namespace shys_m5_dial 9 | { 10 | class M5DialEEPROM { 11 | protected: 12 | 13 | public: 14 | /** 15 | * Text in EEProm ablegen 16 | */ 17 | void writeStringToEEPROM(int addrOffset, const String &strToWrite){ 18 | EEPROM.begin(EEPROM_SIZE); 19 | 20 | byte len = strToWrite.length(); 21 | EEPROM.write(addrOffset, len); 22 | for (int i = 0; i < len; i++){ 23 | EEPROM.write(addrOffset + 1 + i, strToWrite[i]); 24 | } 25 | EEPROM.commit(); 26 | EEPROM.end(); 27 | } 28 | 29 | /** 30 | * Text aus EEProm auslesen 31 | */ 32 | String readStringFromEEPROM(int addrOffset){ 33 | String retVal = ""; 34 | 35 | EEPROM.begin(EEPROM_SIZE); 36 | 37 | int newStrLen = EEPROM.read(addrOffset); 38 | if(!isnan(newStrLen)){ 39 | char data[newStrLen + 1]; 40 | for (int i = 0; i < newStrLen; i++){ 41 | data[i] = EEPROM.read(addrOffset + 1 + i); 42 | } 43 | data[newStrLen] = '\0'; 44 | 45 | EEPROM.end(); 46 | retVal = String(data); 47 | } 48 | 49 | return retVal; 50 | } 51 | 52 | }; 53 | } 54 | } -------------------------------------------------------------------------------- /components/shys_m5_dial/m5dial_rfid.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | namespace esphome 3 | { 4 | namespace shys_m5_dial 5 | { 6 | class M5DialRfid { 7 | protected: 8 | 9 | public: 10 | 11 | }; 12 | } 13 | } -------------------------------------------------------------------------------- /components/shys_m5_dial/m5dial_rotary.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "M5Dial.h" 3 | 4 | namespace esphome 5 | { 6 | namespace shys_m5_dial 7 | { 8 | class M5DialRotary { 9 | protected: 10 | std::function rotary_left_action; 11 | std::function rotary_right_action; 12 | std::function short_button_press_action; 13 | std::function long_button_press_action; 14 | 15 | int longPressMs = 1500; 16 | 17 | long oldPosition = 0; 18 | bool longPress = false; 19 | 20 | public: 21 | void on_rotary_right(std::function callback){ 22 | ESP_LOGD("DEVICE", "register on_rotary_right Callback"); 23 | this->rotary_right_action = callback; 24 | } 25 | 26 | void on_rotary_left(std::function callback){ 27 | ESP_LOGD("DEVICE", "register on_rotary_left Callback"); 28 | this->rotary_left_action = callback; 29 | } 30 | 31 | void on_short_button_press(std::function callback){ 32 | ESP_LOGD("DEVICE", "register on_short_button_press Callback"); 33 | this->short_button_press_action = callback; 34 | } 35 | 36 | void on_long_button_press(std::function callback){ 37 | ESP_LOGD("DEVICE", "register on_long_button_press Callback"); 38 | this->long_button_press_action = callback; 39 | } 40 | 41 | 42 | /** 43 | * 44 | */ 45 | void setLongPressDuration(int value){ 46 | longPressMs = value; 47 | } 48 | 49 | /** 50 | * 51 | */ 52 | void handleRotary(){ 53 | long newPosition = M5Dial.Encoder.read(); 54 | if (newPosition != this->oldPosition) { 55 | if(newPosition > this->oldPosition){ 56 | ESP_LOGI("DEVICE", "Rotary right"); 57 | this->rotary_right_action(); 58 | } else { 59 | ESP_LOGI("DEVICE", "Rotary left"); 60 | this->rotary_left_action(); 61 | } 62 | 63 | this->oldPosition = newPosition; 64 | } 65 | } 66 | 67 | /** 68 | * 69 | */ 70 | bool handleButtonPress(){ 71 | bool is_event = false; 72 | 73 | if (M5Dial.BtnA.wasPressed()) { 74 | longPress = false; 75 | is_event = true; 76 | } 77 | 78 | if (M5Dial.BtnA.pressedFor(longPressMs)) { 79 | M5Dial.Speaker.tone(4000, 200); 80 | longPress = true; 81 | is_event = true; 82 | } 83 | 84 | if (M5Dial.BtnA.wasReleased()) { 85 | if(longPress){ 86 | this->long_button_press_action(); 87 | ESP_LOGI("DEVICE", "Long press"); 88 | } else { 89 | this->short_button_press_action(); 90 | ESP_LOGI("DEVICE", "Short press"); 91 | } 92 | is_event = true; 93 | } 94 | 95 | return is_event; 96 | } 97 | 98 | }; 99 | } 100 | } -------------------------------------------------------------------------------- /components/shys_m5_dial/m5dial_touch.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "M5Dial.h" 3 | #include "globals.h" 4 | 5 | namespace esphome 6 | { 7 | namespace shys_m5_dial 8 | { 9 | class M5DialTouch { 10 | protected: 11 | std::function touch_action; 12 | std::function swipe_action; 13 | m5::touch_state_t prev_state; 14 | 15 | struct Coord { 16 | int x = -1; 17 | int y = -1; 18 | 19 | void reset(){ 20 | x=-1; 21 | y=-1; 22 | } 23 | }; 24 | 25 | bool swipeInProgress = false; 26 | Coord swipeCoord; 27 | 28 | public: 29 | void on_touch(std::function callback){ 30 | ESP_LOGD("DEVICE", "register on_touch Callback"); 31 | this->touch_action = callback; 32 | } 33 | void on_swipe(std::function callback){ 34 | ESP_LOGD("DEVICE", "register on_swipe Callback"); 35 | this->swipe_action = callback; 36 | } 37 | 38 | void handleTouch(){ 39 | auto t = M5Dial.Touch.getDetail(); 40 | auto count = M5.Touch.getCount(); 41 | if (!count) 42 | { 43 | return; 44 | } 45 | 46 | if (prev_state != t.state) { 47 | prev_state = t.state; 48 | 49 | if(TOUCH_STATE_NAME[t.state] == nullptr){ 50 | return; 51 | } 52 | 53 | if(strcmp(TOUCH_STATE_NAME[t.state], TOUCH_STATE_NONE) != 0 && 54 | strcmp(TOUCH_STATE_NAME[t.state], TOUCH_STATE_TMP) != 0 ){ 55 | ESP_LOGD("TOUCH", "Touch Event: %s ( %i / %i )", TOUCH_STATE_NAME[t.state], t.x, t.y); 56 | } 57 | 58 | if( strcmp(TOUCH_STATE_NAME[t.state], TOUCH_STATE_FLICK_BEGIN) == 0){ 59 | swipeCoord.x = t.x; 60 | swipeCoord.y = t.y; 61 | 62 | M5.Display.waitDisplay(); 63 | } else if( strcmp(TOUCH_STATE_NAME[t.state], TOUCH_STATE_FLICK_END) == 0){ 64 | uint16_t h = swipeCoord.x > t.x ? swipeCoord.x - t.x : t.x - swipeCoord.x; 65 | uint16_t v = swipeCoord.y > t.y ? swipeCoord.y - t.y : t.y - swipeCoord.y; 66 | 67 | const char* swipeDirection; 68 | if(v>h && v>M5Dial.Display.height()/4 ){ 69 | if(swipeCoord.y > t.y){ 70 | ESP_LOGD("TOUCH", "Swipe UP"); 71 | swipeDirection = TOUCH_SWIPE_UP; 72 | } else { 73 | ESP_LOGD("TOUCH", "Swipe DOWN"); 74 | swipeDirection = TOUCH_SWIPE_DOWN; 75 | } 76 | } else if (h>M5Dial.Display.width()/4) { 77 | if(swipeCoord.x < t.x){ 78 | ESP_LOGD("TOUCH", "Swipe RIGHT"); 79 | swipeDirection = TOUCH_SWIPE_RIGHT; 80 | } else { 81 | ESP_LOGD("TOUCH", "Swipe LEFT"); 82 | swipeDirection = TOUCH_SWIPE_LEFT; 83 | } 84 | } else { 85 | ESP_LOGD("TOUCH", "Swipe NONE"); 86 | swipeDirection = TOUCH_SWIPE_NONE; 87 | } 88 | 89 | if(swipeDirection == TOUCH_SWIPE_NONE){ 90 | this->touch_action(t.x, t.y); 91 | ESP_LOGI("TOUCH", "%s: %i / %i", "Touch: ", t.x, t.y); 92 | } else { 93 | this->swipe_action(swipeDirection); 94 | ESP_LOGI("TOUCH", "Swipe: %s", swipeDirection); 95 | } 96 | 97 | } else if( strcmp(TOUCH_STATE_NAME[t.state], TOUCH_STATE_TOUCH_END) == 0){ 98 | this->touch_action(t.x, t.y); 99 | ESP_LOGI("TOUCH", "%s: %i / %i", "Touch: ", t.x, t.y); 100 | } 101 | } 102 | } 103 | 104 | }; 105 | } 106 | } -------------------------------------------------------------------------------- /components/shys_m5_dial/shys_m5_dial.cpp: -------------------------------------------------------------------------------- 1 | #include "esphome/core/log.h" 2 | #include "shys_m5_dial.h" 3 | 4 | namespace esphome 5 | { 6 | namespace shys_m5_dial 7 | { 8 | static const char *TAG = "shys_m5_dial"; 9 | 10 | /** 11 | * @brief SETUP 12 | * 13 | * Initialisierung 14 | */ 15 | void ShysM5Dial::setup() 16 | { 17 | ShysM5Dial::initDevice(); 18 | ESP_LOGI("log", "%s", "M5 is initialized"); 19 | } 20 | 21 | /** 22 | * @brief LOOP 23 | * 24 | * Standard Loop 25 | */ 26 | void ShysM5Dial::loop() 27 | { 28 | M5.delay(1); 29 | M5Dial.update(); 30 | esphome::delay(1); 31 | ShysM5Dial::doLoop(); 32 | esphome::delay(1); 33 | } 34 | 35 | /** 36 | * @brief dump_config 37 | * 38 | * Ausgabe der aktuellen Konfiguration im Log nach Initialisierung 39 | */ 40 | void ShysM5Dial::dump_config() 41 | { 42 | ESP_LOGCONFIG(TAG, "-----------------------------------"); 43 | ESP_LOGCONFIG(TAG, "Shys M5 Dial"); 44 | ESP_LOGCONFIG(TAG, "-----------------------------------"); 45 | } 46 | 47 | } 48 | } -------------------------------------------------------------------------------- /components/shys_m5_dial/shys_m5_dial.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "esphome.h" 3 | #include "esp_log.h" 4 | 5 | #include "globals.h" 6 | #include "ha_api.h" 7 | #include "ha_device.h" 8 | #include "ha_device_light.h" 9 | #include "ha_device_climate.h" 10 | #include "ha_device_cover.h" 11 | #include "ha_device_switch.h" 12 | #include "ha_device_fan.h" 13 | #include "ha_device_mediaplayer.h" 14 | #include "ha_device_lock.h" 15 | 16 | #include "M5Dial.h" 17 | 18 | #define MAX_DEVICE_COUNT 50 19 | 20 | namespace esphome 21 | { 22 | namespace shys_m5_dial 23 | { 24 | class ShysM5Dial : public Component, public esphome::api::CustomAPIDevice 25 | { 26 | protected: 27 | int timeToScreenOff = 30000; 28 | int longPressMs = 1200; 29 | int rotaryStepWidth = 10; 30 | uint16_t displayRefeshPause = 700; 31 | 32 | int apiSendDelay = 1000; // Verzögerung nach Wert-Änderung (um nicht jeden Wert beim drehen des Rades zu senden) 33 | int apiSendLock = 3000; // Wartezeit zwischen einzelnden API-Aufrufen 34 | 35 | // ------------------------------- 36 | 37 | HaDevice* devices[MAX_DEVICE_COUNT]; 38 | int deviceAnzahl = 0; 39 | 40 | int currentDevice = 0; 41 | 42 | int lastDisplayDevice = -1; 43 | float lastDisplayValue = -1; 44 | int lastModeIndex = -1; 45 | 46 | unsigned long lastRotaryEvent = 0; 47 | unsigned long lastReceiveEvent = 0; 48 | unsigned long lastDisplayRefresh = 0; 49 | 50 | int lastLoop = 0; 51 | 52 | bool enableRFID = true; 53 | bool enableEncoder = true; 54 | 55 | M5DialDisplay* m5DialDisplay = new M5DialDisplay(); 56 | M5DialRfid* m5DialRfid = new M5DialRfid(); 57 | M5DialRotary* m5DialRotary = new M5DialRotary(); 58 | M5DialTouch* m5DialTouch = new M5DialTouch(); 59 | M5DialEEPROM* m5DialEEPROM = new M5DialEEPROM(); 60 | 61 | bool startsWith(const char *pre, const char *str){ 62 | return strncmp(pre, str, strlen(pre)) == 0; 63 | } 64 | 65 | int getCurrentValue(){ 66 | return devices[currentDevice]->getValue(); 67 | } 68 | 69 | bool isDisplayRefreshNeeded(){ 70 | if (getCurrentValue() != lastDisplayValue || 71 | currentDevice != lastDisplayDevice || 72 | devices[currentDevice]->getCurrentModeIndex() != lastModeIndex || 73 | devices[currentDevice]->isDisplayRefreshNeeded()){ 74 | return esphome::millis() - lastDisplayRefresh > displayRefeshPause; 75 | } 76 | return false; 77 | } 78 | 79 | void refreshDisplay(bool forceRefresh){ 80 | if(forceRefresh || isDisplayRefreshNeeded()){ 81 | devices[currentDevice]->refreshDisplay(*m5DialDisplay, lastDisplayDevice != currentDevice); 82 | 83 | lastDisplayDevice = currentDevice; 84 | lastModeIndex = devices[currentDevice]->getCurrentModeIndex(); 85 | lastDisplayValue = getCurrentValue(); 86 | } 87 | } 88 | 89 | void nextDevice(){ 90 | if(currentDevice >= deviceAnzahl-1){ 91 | currentDevice = 0; 92 | } else { 93 | currentDevice++; 94 | } 95 | } 96 | 97 | void previousDevice(){ 98 | if(currentDevice >= 1){ 99 | currentDevice--; 100 | } else { 101 | currentDevice = deviceAnzahl-1; 102 | } 103 | } 104 | 105 | /** 106 | * 107 | */ 108 | void addDevice(HaDevice* device){ 109 | if (device != nullptr) { 110 | if(this->deviceAnzahl >= MAX_DEVICE_COUNT-1){ 111 | ESP_LOGE("DEVICE", "EXCEED DEVICE COUNT MAXIMUM: %s can not be added!", device->getName().c_str()); 112 | return; 113 | } 114 | 115 | ESP_LOGD("DEVICE", "New Device: %s", device->getName().c_str()); 116 | 117 | devices[deviceAnzahl] = device; 118 | 119 | devices[deviceAnzahl]->setApiSendDelay(this->apiSendDelay); 120 | devices[deviceAnzahl]->setApiSendLock(this->apiSendLock); 121 | devices[deviceAnzahl]->setRotaryStepWidth(this->rotaryStepWidth); 122 | 123 | devices[deviceAnzahl]->init(); 124 | 125 | deviceAnzahl++; 126 | ESP_LOGD("DEVICE", "Device added"); 127 | } 128 | } 129 | 130 | 131 | public: 132 | void dump_config() override; 133 | void setup() override; 134 | void loop() override; 135 | 136 | ShysM5Dial() : Component() {} 137 | 138 | void setScreenOffTime(int value){ 139 | ESP_LOGI("DEVICE", "setScreenOffTime %i", value); 140 | this->timeToScreenOff = value; 141 | m5DialDisplay->setTimeToScreenOff(value); 142 | } 143 | 144 | void setLongPressDuration(int value){ 145 | ESP_LOGI("DEVICE", "setLongPressDuration %i", value); 146 | this->longPressMs = value; 147 | m5DialRotary->setLongPressDuration(value); 148 | } 149 | 150 | void setApiSendDelay(int delayInMs){ 151 | ESP_LOGI("DEVICE", "setApiSendDelay %i", delayInMs); 152 | this->apiSendDelay = delayInMs; 153 | } 154 | 155 | void setApiSendLock(int delayInMs){ 156 | ESP_LOGI("DEVICE", "setApiSendLock %i", delayInMs); 157 | this->apiSendLock = delayInMs; 158 | } 159 | 160 | void setRotaryStepWidth(int value){ 161 | ESP_LOGI("DEVICE", "setRotaryStepWidth %i", value); 162 | this->rotaryStepWidth = value; 163 | } 164 | 165 | void setFontName(std::string value){ 166 | ESP_LOGI("DEVICE", "setFontName %s", value); 167 | m5DialDisplay->setFontName(value); 168 | } 169 | 170 | void setFontFactor(int value){ 171 | ESP_LOGI("DEVICE", "setFontFactor %i", value); 172 | m5DialDisplay->setFontFactor(value); 173 | } 174 | 175 | 176 | /** 177 | * 178 | */ 179 | void addLight(const std::string& entity_id, const std::string& name, const std::string& modes){ 180 | HaDeviceLight* light = new HaDeviceLight(entity_id, name, modes); 181 | addDevice(light); 182 | } 183 | 184 | 185 | /** 186 | * 187 | */ 188 | void addClimate(const std::string& entity_id, const std::string& name, const std::string& modes){ 189 | HaDeviceClimate* climate = new HaDeviceClimate(entity_id, name, modes); 190 | addDevice(climate); 191 | } 192 | 193 | 194 | /** 195 | * 196 | */ 197 | void addCover(const std::string& entity_id, const std::string& name, const std::string& modes){ 198 | HaDeviceCover* climate = new HaDeviceCover(entity_id, name, modes); 199 | addDevice(climate); 200 | } 201 | 202 | 203 | /** 204 | * 205 | */ 206 | void addSwitch(const std::string& entity_id, const std::string& name, const std::string& modes){ 207 | HaDeviceSwitch* switchDevice = new HaDeviceSwitch(entity_id, name, modes); 208 | addDevice(switchDevice); 209 | } 210 | 211 | 212 | /** 213 | * 214 | */ 215 | void addFan(const std::string& entity_id, const std::string& name, const std::string& modes){ 216 | HaDeviceFan* fan = new HaDeviceFan(entity_id, name, modes); 217 | addDevice(fan); 218 | } 219 | 220 | 221 | /** 222 | * 223 | */ 224 | void addMediaPlayer(const std::string& entity_id, const std::string& name, const std::string& modes){ 225 | HaDeviceMediaPlayer* mediaPlayer = new HaDeviceMediaPlayer(entity_id, name, modes); 226 | addDevice(mediaPlayer); 227 | } 228 | 229 | 230 | void addLock(const std::string& entity_id, const std::string& name, const std::string& modes){ 231 | HaDeviceLock* lock = new HaDeviceLock(entity_id, name, modes); 232 | addDevice(lock); 233 | } 234 | 235 | 236 | /** 237 | * 238 | */ 239 | void initDevice(){ 240 | using std::placeholders::_1; 241 | using std::placeholders::_2; 242 | 243 | ESP_LOGI("DEVICE", "Initialisierung..."); 244 | 245 | auto cfg = M5.config(); 246 | M5Dial.begin(cfg, enableEncoder, enableRFID); 247 | 248 | ESP_LOGI("DEVICE", "Register Callbacks..."); 249 | m5DialRotary->on_rotary_left(std::bind(&esphome::shys_m5_dial::ShysM5Dial::turnRotaryLeft, this)); 250 | m5DialRotary->on_rotary_right(std::bind(&esphome::shys_m5_dial::ShysM5Dial::turnRotaryRight, this)); 251 | m5DialRotary->on_short_button_press(std::bind(&esphome::shys_m5_dial::ShysM5Dial::shortButtonPress, this)); 252 | m5DialRotary->on_long_button_press(std::bind(&esphome::shys_m5_dial::ShysM5Dial::longButtonPress, this)); 253 | 254 | m5DialTouch->on_touch(std::bind(&esphome::shys_m5_dial::ShysM5Dial::touchInput, this, _1, _2)); 255 | m5DialTouch->on_swipe(std::bind(&esphome::shys_m5_dial::ShysM5Dial::touchSwipe, this, _1)); 256 | 257 | this->registerServices(); 258 | } 259 | 260 | 261 | /** 262 | * 263 | */ 264 | void doLoop(){ 265 | if(api::global_api_server->is_connected()){ 266 | m5DialRotary->handleRotary(); 267 | 268 | if (m5DialRotary->handleButtonPress()){ 269 | m5DialDisplay->resetLastEventTimer(); 270 | } 271 | 272 | m5DialTouch->handleTouch(); 273 | m5DialDisplay->validateTimeout(); 274 | 275 | devices[currentDevice]->updateHomeAssistantValue(); 276 | 277 | devices[currentDevice]->onLoop(); 278 | 279 | this->refreshDisplay(false); 280 | lastLoop = 1; 281 | 282 | } else if(network::is_connected()){ 283 | if(lastLoop != 2){ 284 | ESP_LOGD("HA_API", "API is not connected"); 285 | } 286 | m5DialDisplay->showDisconnected(); 287 | esphome::delay(10); 288 | lastLoop = 2; 289 | 290 | } else { 291 | if(lastLoop != 3){ 292 | ESP_LOGD("wifi", "Network is not connected"); 293 | } 294 | m5DialDisplay->showOffline(); 295 | esphome::delay(10); 296 | lastLoop = 3; 297 | } 298 | } 299 | 300 | 301 | /** 302 | * 303 | */ 304 | void registerServices(){ 305 | register_service(&ShysM5Dial::selectDevice, "selectDevice", {"entity_id"}); 306 | } 307 | 308 | /** 309 | * 310 | */ 311 | void selectDevice(std::string entityId){ 312 | bool found = false; 313 | 314 | for(int i=0; igetEntityId().c_str(), entityId.c_str()) == 0 ){ 317 | this->currentDevice = i; 318 | this->m5DialDisplay->resetLastEventTimer(); 319 | 320 | found = true; 321 | ESP_LOGI("SERVICE", "Entity %s selected", entityId.c_str()); 322 | return; 323 | } 324 | } 325 | ESP_LOGW("SERVICE", "Entity-ID %s not found", entityId.c_str()); 326 | } 327 | 328 | /** 329 | * 330 | */ 331 | void turnRotaryLeft(){ 332 | m5DialDisplay->resetLastEventTimer(); 333 | M5Dial.Speaker.tone(5000, 20); 334 | 335 | if(m5DialDisplay->isDisplayOn()){ 336 | devices[currentDevice]->onRotary(*m5DialDisplay, ROTARY_LEFT); 337 | } 338 | 339 | lastRotaryEvent = esphome::millis(); 340 | } 341 | 342 | /** 343 | * 344 | */ 345 | void turnRotaryRight(){ 346 | m5DialDisplay->resetLastEventTimer(); 347 | M5Dial.Speaker.tone(5000, 20); 348 | 349 | if(m5DialDisplay->isDisplayOn()){ 350 | devices[currentDevice]->onRotary(*m5DialDisplay, ROTARY_RIGHT); 351 | } 352 | 353 | lastRotaryEvent = esphome::millis(); 354 | } 355 | 356 | /** 357 | * 358 | */ 359 | void shortButtonPress(){ 360 | m5DialDisplay->resetLastEventTimer(); 361 | M5Dial.Speaker.tone(4000, 20); 362 | if(m5DialDisplay->isDisplayOn()){ 363 | devices[currentDevice]->onButton(*m5DialDisplay, BUTTON_SHORT); 364 | } 365 | } 366 | 367 | /** 368 | * 369 | */ 370 | void longButtonPress(){ 371 | if(m5DialDisplay->isDisplayOn()){ 372 | m5DialDisplay->resetLastEventTimer(); 373 | } 374 | } 375 | 376 | /** 377 | * 378 | */ 379 | void touchInput(uint16_t x, uint16_t y){ 380 | m5DialDisplay->resetLastEventTimer(); 381 | if(m5DialDisplay->isDisplayOn()){ 382 | devices[currentDevice]->onTouch(*m5DialDisplay, x, y); 383 | } 384 | } 385 | 386 | /** 387 | * 388 | */ 389 | void touchSwipe(const char* direction){ 390 | ESP_LOGD("TOUCH", "touchSwipe direction: %s", direction); 391 | m5DialDisplay->resetLastEventTimer(); 392 | if(m5DialDisplay->isDisplayOn()){ 393 | if(! devices[currentDevice]->onSwipe(*m5DialDisplay, direction) ){ 394 | 395 | if(strcmp(direction, TOUCH_SWIPE_LEFT)==0){ 396 | this->previousDevice(); 397 | } else if(strcmp(direction, TOUCH_SWIPE_RIGHT)==0){ 398 | this->nextDevice(); 399 | } else if(strcmp(direction, TOUCH_SWIPE_UP)==0){ 400 | devices[currentDevice]->previousMode(); 401 | } else if(strcmp(direction, TOUCH_SWIPE_DOWN)==0){ 402 | devices[currentDevice]->nextMode(); 403 | } 404 | 405 | } 406 | } 407 | } 408 | 409 | 410 | }; 411 | } 412 | } -------------------------------------------------------------------------------- /shys-m5-dial.yaml: -------------------------------------------------------------------------------- 1 | substitutions: 2 | name: "m5-dial" 3 | wifi_ssid: !secret wifi_ssid 4 | wifi_password: !secret wifi_password 5 | 6 | esphome: 7 | name: ${name} 8 | name_add_mac_suffix: false 9 | project: 10 | name: smarthomeyourself.m5_dial 11 | version: "1.0" 12 | 13 | platformio_options: 14 | lib_deps: 15 | - WiFi 16 | - SPI 17 | - FS 18 | - Wire 19 | - EEPROM 20 | - ArduinoJson 21 | - m5stack/M5Unified@0.1.14 22 | - m5stack/M5Dial@1.0.2 23 | 24 | external_components: 25 | - source: 26 | type: git 27 | url: https://github.com/SmartHome-yourself/m5-dial-for-esphome/ 28 | ref: main 29 | components: [shys_m5_dial] 30 | 31 | dashboard_import: 32 | package_import_url: github://SmartHome-yourself/m5-dial-for-esphome/shys-m5-dial.yaml@main 33 | import_full_config: false 34 | 35 | esp32: 36 | board: esp32-s3-devkitc-1 37 | framework: 38 | type: arduino 39 | 40 | logger: 41 | level: DEBUG 42 | 43 | api: 44 | ota: 45 | platform: esphome 46 | 47 | improv_serial: 48 | captive_portal: 49 | 50 | wifi: 51 | ssid: ${wifi_ssid} 52 | password: ${wifi_password} 53 | ap: 54 | password: "12345678" 55 | --------------------------------------------------------------------------------