├── sample8x32.gif ├── sample8x8.gif ├── images ├── black.gif ├── events.png ├── repair.png ├── sample.png ├── timing.png ├── icon_error.png ├── compile_error.png ├── icons_preview.png ├── duration_select.png └── old_add_screen.png ├── .gitignore ├── .vscode └── c_cpp_properties.json ├── .github └── FUNDING.yml ├── LICENSE ├── components └── ehmtx │ ├── EHMTX_icons.cpp │ ├── EHMTX_screen.cpp │ ├── EHMTX_store.cpp │ ├── EHMTX.h │ ├── EHMTX.cpp │ └── __init__.py ├── CHANGELOG.md ├── fullfeature.yaml ├── UlanziTC001.yaml ├── UlanziTC001_night_mode.yaml └── README.md /sample8x32.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lubeda/EsphoMaTrix/HEAD/sample8x32.gif -------------------------------------------------------------------------------- /sample8x8.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lubeda/EsphoMaTrix/HEAD/sample8x8.gif -------------------------------------------------------------------------------- /images/black.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lubeda/EsphoMaTrix/HEAD/images/black.gif -------------------------------------------------------------------------------- /images/events.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lubeda/EsphoMaTrix/HEAD/images/events.png -------------------------------------------------------------------------------- /images/repair.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lubeda/EsphoMaTrix/HEAD/images/repair.png -------------------------------------------------------------------------------- /images/sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lubeda/EsphoMaTrix/HEAD/images/sample.png -------------------------------------------------------------------------------- /images/timing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lubeda/EsphoMaTrix/HEAD/images/timing.png -------------------------------------------------------------------------------- /images/icon_error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lubeda/EsphoMaTrix/HEAD/images/icon_error.png -------------------------------------------------------------------------------- /images/compile_error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lubeda/EsphoMaTrix/HEAD/images/compile_error.png -------------------------------------------------------------------------------- /images/icons_preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lubeda/EsphoMaTrix/HEAD/images/icons_preview.png -------------------------------------------------------------------------------- /images/duration_select.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lubeda/EsphoMaTrix/HEAD/images/duration_select.png -------------------------------------------------------------------------------- /images/old_add_screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lubeda/EsphoMaTrix/HEAD/images/old_add_screen.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | components/ehmtx/__init__ copy._py 2 | components/ehmtx/__pycache__/__init__.cpython-39.pyc 3 | venv 4 | .esphome/ 5 | _icons/ 6 | _fonts/ 7 | secrets.yaml 8 | esphome.ps1 9 | dev_*.yaml 10 | ehmtx8266.yaml 11 | info.de.md 12 | components/ehmtx/select/__pycache__/__init__.cpython-39.pyc 13 | ehmtx8266-rgb565.yaml 14 | icons.html 15 | components/ehmtx/__init__ copy.py 16 | ehmtx8266-rgb565.yaml.html 17 | ehmtx8266-rgb565-font.yaml 18 | components/ehmtx/EHMTX_icons._cpp 19 | .vscode/settings.json 20 | ehmtx8266-select.yaml 21 | svganimtest.html 22 | servicetest 23 | .DS_Store 24 | -------------------------------------------------------------------------------- /.vscode/c_cpp_properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "name": "Mac", 5 | "includePath": [ 6 | "${workspaceFolder}/**" 7 | ], 8 | "defines": [], 9 | "macFrameworkPath": [ 10 | "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/System/Library/Frameworks" 11 | ], 12 | "compilerPath": "/usr/bin/clang", 13 | "cStandard": "c17", 14 | "cppStandard": "c++98", 15 | "intelliSenseMode": "macos-clang-arm64" 16 | } 17 | ], 18 | "version": 4 19 | } -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | custom: ["https://www.paypal.com/donate/?hosted_button_id=FZDKSLQ46HJTU"] 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022,2023 LuBeDa 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 | -------------------------------------------------------------------------------- /components/ehmtx/EHMTX_icons.cpp: -------------------------------------------------------------------------------- 1 | #include "esphome.h" 2 | 3 | namespace esphome 4 | { 5 | 6 | 7 | EHMTX_Icon::EHMTX_Icon(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, image::ImageType type, std::string icon_name, bool revers, uint16_t frame_duration) 8 | : animation::Animation(data_start, width, height, animation_frame_count, type) 9 | { 10 | this->name = icon_name; 11 | this->reverse = revers; 12 | this->frame_duration = frame_duration; 13 | this->fullscreen = width == 32; 14 | this->counting_up = true; 15 | } 16 | 17 | void EHMTX_Icon::next_frame() 18 | { 19 | if (this->get_animation_frame_count() > 1) 20 | { 21 | if (this->counting_up) 22 | { 23 | if (this->reverse && (this->get_current_frame() == this->get_animation_frame_count() - 2)) 24 | { 25 | this->counting_up = false; 26 | } 27 | Animation::next_frame(); 28 | } 29 | else 30 | { 31 | if (this->get_current_frame() == 1) // this->get_animation_frame_count()) 32 | { 33 | this->counting_up = true; 34 | } 35 | Animation::prev_frame(); 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 2023.4.0 4 | - new: set_screen_color service to set color for a screen (service & action) 5 | - breaking: all settings and parameters are named more consistent 6 | - changed: center small text instead of left aligned 7 | - removed: select component 8 | 9 | ## 2023.3.5 10 | - new: show_seconds indicator top-left corner 11 | - breaking: removed automatic scaling of images 12 | - new: support 8x32 icons without text 13 | - breaking: added status, display_on, display_off as default service => remove these from your YAML 14 | - breaking: added indicator_on/off as default service => remove these from your YAML 15 | - breaking: added alarm_color, text_color, clock_color as default service => remove these from your YAML 16 | - breaking: gauge is also shown while the clock is displayed but without moving the screen to the right 17 | - breaking: show_icons as default service => remove from YAML 18 | 19 | ## 2023.3.4 20 | 21 | - added: option to not display clock/date #53 22 | - added: dynamic set_show_clock 23 | - added: on_next_clock trigger 24 | 25 | ## 2023.3.3 26 | 27 | - fixed: force_screen skips immediately to the selected screen 28 | - added: hold_time configurable 29 | 30 | ## 2023.3.2 31 | 32 | - added: hold_screen for 20 additional seconds 33 | 34 | ## 2023.3.1 35 | 36 | - added: del_screen with wildcards 37 | - changed: maximum icons to 80 38 | - fixed: skip_next 39 | - fixed: show_all_icons on boot 40 | 41 | ## 2023.3.0 42 | 43 | see README.md for features 44 | e.g. Service **display_on** / **display_off** 45 | -------------------------------------------------------------------------------- /fullfeature.yaml: -------------------------------------------------------------------------------- 1 | # esphome config file with all features 2 | substitutions: 3 | devicename: ehmtx8266 4 | ledpin: GPIO02 5 | board: d1_mini 6 | loglevel: DEBUG 7 | 8 | external_components: 9 | - source: 10 | type: git 11 | url: https://github.com/lubeda/EsphoMaTrix 12 | 13 | esphome: 14 | name: $devicename 15 | on_boot: 16 | priority: -100 17 | then: 18 | - ehmtx.text.color: 19 | id: rgb8x32 20 | red: !lambda return 200; 21 | green: !lambda return 100; 22 | blue: !lambda return 50; 23 | - ehmtx.clock.color: 24 | id: rgb8x32 25 | red: !lambda return 150; 26 | green: !lambda return 0; 27 | blue: !lambda return 100; 28 | - ehmtx.today.color: 29 | id: rgb8x32 30 | red: !lambda return 0; 31 | green: !lambda return 100; 32 | blue: !lambda return 0; 33 | - ehmtx.weekday.color: 34 | id: rgb8x32 35 | red: !lambda return 0; 36 | green: !lambda return 0; 37 | blue: !lambda return 100; 38 | - ehmtx.alarm.color: 39 | id: rgb8x32 40 | red: !lambda return 200; 41 | green: !lambda return 150; 42 | blue: !lambda return 30; 43 | 44 | web_server: 45 | port: 80 46 | 47 | esp8266: 48 | board: $board 49 | 50 | font: 51 | - file: monobit.ttf 52 | id: ehmtx_font 53 | size: 16 54 | glyphs: | 55 | !?"%()+*=,-_.:°0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz€@<>/ 56 | 57 | logger: 58 | level: $loglevel 59 | 60 | api: 61 | 62 | ota: 63 | password: !secret ota_password 64 | 65 | wifi: 66 | ssid: !secret wifi_ssid 67 | password: !secret wifi_password 68 | 69 | light: 70 | - platform: neopixelbus 71 | id: ehmtx_light 72 | type: GRB 73 | variant: WS2812 74 | pin: $ledpin 75 | num_leds: 256 76 | color_correct: [30%, 30%, 30%] 77 | name: "$devicename Light" 78 | restore_mode: ALWAYS_OFF 79 | on_turn_on: 80 | lambda: |- 81 | id(ehmtx_display)->set_enabled(false); 82 | on_turn_off: 83 | lambda: |- 84 | id(ehmtx_display)->set_enabled(true); 85 | 86 | time: 87 | - platform: homeassistant 88 | id: ehmtx_time 89 | 90 | display: 91 | - platform: addressable_light 92 | id: ehmtx_display 93 | addressable_light_id: ehmtx_light 94 | width: 32 95 | height: 8 96 | pixel_mapper: |- 97 | if (x % 2 == 0) { 98 | return (x * 8) + y; 99 | } 100 | return (x * 8) + (7 - y); 101 | rotation: 0° 102 | update_interval: 16ms 103 | auto_clear_enabled: true 104 | lambda: |- 105 | id(rgb8x32)->tick(); 106 | id(rgb8x32)->draw(); 107 | 108 | sensor: 109 | - platform: uptime 110 | name: Uptime Sensor 111 | 112 | ehmtx: 113 | id: rgb8x32 114 | time_component: ehmtx_time 115 | matrix_component: ehmtx_display 116 | clock_time: 5 # seconds 117 | screen_time: 8 # seconds 118 | font_id: ehmtx_font 119 | show_dow: true # day of week 120 | icons2html: true # generate html with con overview 121 | brightness: 80 # percent 122 | time_format: "%H:%M" 123 | date_format: "%d.%m." 124 | week_start_monday: true 125 | xoffset: 1 126 | yoffset: 6 127 | scroll_count: 2 128 | scroll_interval: 80 129 | frame_interval: 192 130 | 131 | on_next_screen: # trigger on screen change 132 | lambda: |- 133 | ESP_LOGD("TriggerTest","Iconname: %s",x.c_str()); 134 | ESP_LOGI("TriggerTest","Text: %s",y.c_str()); 135 | 136 | on_next_clock: # trigger on clock display 137 | then: 138 | - ehmtx.clock.color: 139 | id: rgb8x32 140 | red: !lambda return 150; 141 | green: !lambda return rand() % 255; 142 | blue: !lambda return 100; 143 | 144 | icons: 145 | - id: xani 146 | lameid: 6075 147 | - id: xsta 148 | lameid: 11236 149 | - url: https://developer.lametric.com/content/apps/icon_thumbs/48720.gif 150 | pingpong: true 151 | id: pipo 152 | frame_duration: 300 153 | - lameid: 5965 154 | frame_duration: 180 155 | id: d180 156 | - lameid: 5965 157 | frame_duration: 80 158 | id: d080 159 | - id: fullscreen 160 | file: sample8x32.gif 161 | - id: samplegif 162 | file: sample8x8.gif 163 | 164 | -------------------------------------------------------------------------------- /components/ehmtx/EHMTX_screen.cpp: -------------------------------------------------------------------------------- 1 | #include "esphome.h" 2 | 3 | namespace esphome 4 | { 5 | 6 | EHMTX_screen::EHMTX_screen(EHMTX *config) 7 | { 8 | this->config_ = config; 9 | this->endtime = 0; 10 | this->centerx_ = 0; 11 | this->shiftx_ = 0; 12 | this->alarm = false; 13 | } 14 | 15 | bool EHMTX_screen::is_alarm() { return this->alarm; } 16 | 17 | bool EHMTX_screen::del_slot(uint8_t _icon) 18 | { 19 | if (this->icon == _icon) 20 | { 21 | this->endtime = 0; 22 | ESP_LOGD(TAG, "delete screen icon: %d", _icon); 23 | return true; 24 | } 25 | return false; 26 | } 27 | 28 | void EHMTX_screen::reset_shiftx() 29 | { 30 | this->shiftx_ = 0; 31 | } 32 | 33 | void EHMTX_screen::update_screen() 34 | { 35 | if (millis() - this->config_->last_scroll_time >= this->config_->scroll_interval && this->pixels_ > TEXTSTARTOFFSET) 36 | { 37 | this->shiftx_++; 38 | if (this->shiftx_ > this->pixels_ + TEXTSTARTOFFSET) 39 | { 40 | this->shiftx_ = 0; 41 | } 42 | this->config_->last_scroll_time = millis(); 43 | } 44 | if (millis() - this->config_->last_anim_time >= this->config_->icons[this->icon]->frame_duration) 45 | { 46 | this->config_->icons[this->icon]->next_frame(); 47 | this->config_->last_anim_time = millis(); 48 | } 49 | } 50 | 51 | bool EHMTX_screen::active() 52 | { 53 | if (this->endtime > 0) 54 | { 55 | time_t ts = this->config_->clock->now().timestamp; 56 | if (ts < this->endtime) 57 | { 58 | return true; 59 | } 60 | } 61 | return false; 62 | } 63 | 64 | void EHMTX_screen::draw_() 65 | { 66 | int8_t extraoffset = 0; 67 | 68 | if (this->pixels_ > TEXTSTARTOFFSET) 69 | { 70 | extraoffset = TEXTSTARTOFFSET; 71 | } 72 | if (this->config_->show_gauge) 73 | { 74 | extraoffset += 2; 75 | } 76 | 77 | if (!this->config_->icons[this->icon]->fullscreen) 78 | { 79 | if (this->alarm) 80 | { 81 | this->config_->display->print(this->centerx_ + TEXTSCROLLSTART - this->shiftx_ + extraoffset + this->config_->xoffset, this->config_->yoffset, this->config_->font, this->config_->alarm_color, esphome::display::TextAlign::BASELINE_LEFT, 82 | this->text.c_str()); 83 | } 84 | else 85 | { 86 | this->config_->display->print(this->centerx_ + TEXTSCROLLSTART - this->shiftx_ + extraoffset + this->config_->xoffset, this->config_->yoffset, this->config_->font, this->text_color, esphome::display::TextAlign::BASELINE_LEFT, 87 | this->text.c_str()); 88 | } 89 | } 90 | if (this->alarm) 91 | { 92 | this->config_->display->draw_pixel_at(30, 0, this->config_->alarm_color); 93 | this->config_->display->draw_pixel_at(31, 1, this->config_->alarm_color); 94 | this->config_->display->draw_pixel_at(31, 0, this->config_->alarm_color); 95 | } 96 | 97 | if (this->config_->show_gauge) 98 | { 99 | this->config_->draw_gauge(); 100 | this->config_->display->image(2, 0, this->config_->icons[this->icon]); 101 | if (! this->config_->icons[this->icon]->fullscreen) { 102 | this->config_->display->line(10, 0, 10, 7, esphome::display::COLOR_OFF); 103 | } 104 | } 105 | else 106 | { 107 | this->config_->display->line(8, 0, 8, 7, esphome::display::COLOR_OFF); 108 | this->config_->display->image(0, 0, this->config_->icons[this->icon]); 109 | } 110 | } 111 | 112 | void EHMTX_screen::draw() 113 | { 114 | this->draw_(); 115 | this->update_screen(); 116 | } 117 | 118 | void EHMTX_screen::hold_slot(uint8_t _sec) 119 | { 120 | this->endtime += _sec; 121 | ESP_LOGD(TAG, "hold for %d secs", _sec); 122 | } 123 | 124 | void EHMTX_screen::set_text(std::string text, uint8_t icon, uint16_t pixel, uint16_t et,uint16_t show_time) 125 | { 126 | this->text = text; 127 | this->pixels_ = pixel; 128 | 129 | if (pixel < 23) { 130 | this->centerx_ = ceil((22-pixel)/2); 131 | } 132 | 133 | this->shiftx_ = 0; 134 | float display_duration = ceil((this->config_->scroll_count * (TEXTSTARTOFFSET + pixel) * this->config_->scroll_interval) / 1000); 135 | this->screen_time = (display_duration > show_time) ? display_duration : show_time; 136 | ESP_LOGD(TAG, "display length text: %s pixels %d calculated: %d show_time: %d default: %d", text.c_str(), pixel, this->screen_time, show_time, this->config_->screen_time); 137 | this->endtime = this->config_->clock->now().timestamp + et * 60; 138 | this->icon = icon; 139 | } 140 | 141 | void EHMTX_screen::set_text_color(uint8_t icon_id, Color text_color) 142 | { 143 | if (this->icon== icon_id ){ 144 | this->text_color = text_color; 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /components/ehmtx/EHMTX_store.cpp: -------------------------------------------------------------------------------- 1 | #include "esphome.h" 2 | 3 | namespace esphome 4 | { 5 | EHMTX_store::EHMTX_store(EHMTX *config) 6 | { 7 | for (uint8_t i = 0; i < MAXQUEUE; i++) 8 | { 9 | this->slots[i] = new EHMTX_screen(config); 10 | } 11 | this->active_slot = 0; 12 | } 13 | 14 | EHMTX_screen *EHMTX_store::find_free_screen(uint8_t icon) 15 | { 16 | ESP_LOGD(TAG, "findfreeslot for icon: %d", icon); 17 | for (uint8_t i = 0; i < MAXQUEUE; i++) 18 | { 19 | EHMTX_screen *screen = this->slots[i]; 20 | if (screen->icon == icon) 21 | { 22 | return screen; 23 | } 24 | } 25 | 26 | time_t ts = this->clock->now().timestamp; 27 | for (uint8_t i = 0; i < MAXQUEUE; i++) 28 | { 29 | EHMTX_screen *screen = this->slots[i]; 30 | if (screen->endtime <= ts) 31 | { 32 | return screen; 33 | } 34 | } 35 | return this->slots[0]; 36 | } 37 | 38 | void EHMTX_store::set_text_color(uint8_t icon_id, Color c) 39 | { 40 | for (uint8_t i = 0; i < MAXQUEUE; i++) 41 | { 42 | this->slots[i]->set_text_color(icon_id,c); 43 | } 44 | 45 | } 46 | 47 | void EHMTX_store::delete_screen(uint8_t icon) 48 | { 49 | for (uint8_t i = 0; i < MAXQUEUE; i++) 50 | { 51 | this->slots[i]->del_slot(icon); 52 | } 53 | } 54 | 55 | void EHMTX_store::force_next_screen(uint8_t icon_id) 56 | { 57 | if (icon_id < MAXICONS) 58 | { 59 | this->force_screen = icon_id; 60 | this->move_next(); 61 | } 62 | } 63 | 64 | bool EHMTX_store::move_next() 65 | { 66 | if (this->force_screen < MAXICONS) 67 | { 68 | for (uint8_t slot = 0; slot < MAXQUEUE; slot++) 69 | { 70 | EHMTX_screen *screen = this->slots[slot]; 71 | if (screen->active() && screen->icon == this->force_screen) 72 | { 73 | this->force_screen = MAXICONS; 74 | this->active_slot = slot; 75 | return true; 76 | } 77 | } 78 | } 79 | if (this->count_active_screens() == 1) 80 | { 81 | // Find first and only active screen 82 | for (uint8_t slot = 0; slot < MAXQUEUE; slot++) 83 | { 84 | EHMTX_screen *screen = this->slots[slot]; 85 | if (screen->active()) 86 | { 87 | screen->reset_shiftx(); 88 | this->active_slot = slot; 89 | return true; 90 | } 91 | } 92 | } 93 | 94 | // Find active screen between active slot and end of array 95 | for (uint8_t slot = (this->active_slot + 1); slot < MAXQUEUE; slot++) 96 | { 97 | EHMTX_screen *screen = this->slots[slot]; 98 | if (screen->active()) 99 | { 100 | screen->reset_shiftx(); 101 | this->active_slot = slot; 102 | return true; 103 | } 104 | } 105 | 106 | // Find active screen between 0 and active slot 107 | for (uint8_t slot = 0; slot < this->active_slot; slot++) 108 | { 109 | EHMTX_screen *screen = this->slots[slot]; 110 | if (screen->active()) 111 | { 112 | screen->reset_shiftx(); 113 | this->active_slot = slot; 114 | return true; 115 | } 116 | } 117 | 118 | // No active screen found 119 | this->active_slot = 0; 120 | return false; 121 | } 122 | 123 | EHMTX_screen *EHMTX_store::current() 124 | { 125 | return this->slots[this->active_slot]; 126 | } 127 | 128 | void EHMTX_store::hold_current(uint _sec) 129 | { 130 | this->slots[this->active_slot]->hold_slot(_sec); 131 | } 132 | 133 | uint8_t EHMTX_store::count_active_screens() 134 | { 135 | uint8_t count = 0; 136 | for (uint8_t screen = 0; screen < MAXQUEUE; screen++) 137 | { 138 | if (this->slots[screen]->active()) 139 | { 140 | count++; 141 | } 142 | } 143 | return count; 144 | } 145 | 146 | void EHMTX_store::log_status() 147 | { 148 | uint8_t status = 0; 149 | time_t ts = this->clock->now().timestamp; 150 | ESP_LOGI(TAG, "status active slot: %d", this->active_slot); 151 | ESP_LOGI(TAG, "status screen count: %d of %d", this->count_active_screens(), MAXQUEUE); 152 | for (uint8_t i = 0; i < MAXQUEUE; i++) 153 | { 154 | if (this->slots[i]->active()) 155 | { 156 | EHMTX_screen *screen = this->slots[i]; 157 | int td = screen->endtime - ts; 158 | ESP_LOGI(TAG, "status slot %d icon %d text: %s alarm: %d dd: %d sec end: %d sec", i, screen->icon, screen->text.c_str(), screen->alarm, screen->screen_time, td); 159 | } 160 | } 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /UlanziTC001.yaml: -------------------------------------------------------------------------------- 1 | substitutions: 2 | devicename: ulanzi 3 | # Pin definition from https://github.com/aptonline/PixelIt_Ulanzi 4 | battery_pin: GPIO34 5 | ldr_pin: GPIO35 6 | matrix_pin: GPIO32 7 | left_button_pin: GPIO26 8 | mid_button_pin: GPIO27 9 | right_button_pin: GPIO14 10 | buzzer_pin: GPIO15 11 | scl_pin: GPIO22 12 | sda_pin: GPIO21 13 | 14 | external_components: 15 | - source: 16 | type: git 17 | url: https://github.com/lubeda/EsphoMaTrix 18 | refresh: 60s 19 | components: [ ehmtx ] 20 | 21 | esphome: 22 | comment: "Ulanzi TC001 Pixel Clock" 23 | name: $devicename 24 | on_boot: 25 | then: 26 | - ds1307.read_time: 27 | 28 | esp32: 29 | board: esp32dev 30 | 31 | font: 32 | # adapt the filename to your local settings 33 | # public domain font https://www.pentacom.jp/pentacom/bitfontmaker2/gallery/?id=5993 34 | - file: DMDSmall.ttf 35 | id: ehmtx_font 36 | size: 16 37 | glyphs: | 38 | !?"%()+*=,-_.:°0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnÖÄÜöäüopqrstuvwxyz€@<>/ 39 | 40 | globals: 41 | # aab = auto-adjustable brightness 42 | - id: aab_enable 43 | type: "bool" 44 | restore_value: true 45 | initial_value: "true" 46 | - id: aab_add 47 | type: int 48 | initial_value: '10' 49 | - id: aab_max 50 | type: int 51 | initial_value: '220' 52 | - id: aab_min 53 | type: int 54 | initial_value: '20' 55 | 56 | binary_sensor: 57 | - platform: status 58 | name: "$devicename Status" 59 | - platform: gpio 60 | pin: 61 | number: $left_button_pin 62 | inverted: true 63 | name: "$devicename left button" 64 | - platform: gpio 65 | pin: 66 | inverted: true 67 | number: $mid_button_pin 68 | mode: INPUT_PULLUP 69 | name: "$devicename middle button" 70 | - platform: gpio 71 | pin: 72 | number: $right_button_pin 73 | inverted: true 74 | name: "$devicename right button" 75 | 76 | logger: 77 | level: INFO 78 | 79 | # Enable Home Assistant API 80 | api: 81 | services: 82 | - service: alarm 83 | variables: 84 | icon_name: string 85 | text: string 86 | then: 87 | lambda: |- 88 | id(rgb8x32)->add_screen(icon_name,text,7,30,true); 89 | id(rgb8x32)->force_screen(icon_name); 90 | - service: screen 91 | variables: 92 | icon_name: string 93 | text: string 94 | then: 95 | - ehmtx.add.screen: 96 | id: rgb8x32 97 | text: !lambda return text; 98 | icon_name: !lambda return icon_name; 99 | alarm: false 100 | 101 | number: 102 | - platform: template 103 | name: "$devicename brightness" 104 | min_value: 0 105 | max_value: 255 106 | step: 1 107 | lambda: |- 108 | return id(rgb8x32)->get_brightness(); 109 | set_action: 110 | lambda: |- 111 | id(rgb8x32)->set_brightness(x); 112 | 113 | switch: 114 | - platform: template 115 | name: "$devicename Display" 116 | icon: "mdi:power" 117 | restore_mode: ALWAYS_ON 118 | lambda: |- 119 | return id(rgb8x32)->show_display; 120 | turn_on_action: 121 | lambda: |- 122 | id(rgb8x32)->set_display_on(); 123 | turn_off_action: 124 | lambda: |- 125 | id(rgb8x32)->set_display_off(); 126 | - platform: template 127 | name: "Auto-Adjust Brightness" 128 | id: switch_autobrightness 129 | icon: mdi:brightness-auto 130 | restore_mode: RESTORE_DEFAULT_ON 131 | lambda: |- 132 | if (id(aab_enable)) { 133 | return true; 134 | } else { 135 | return false; 136 | } 137 | turn_on_action: 138 | lambda: |- 139 | id(aab_enable) = true; 140 | turn_off_action: 141 | lambda: |- 142 | id(aab_enable) = false; 143 | 144 | 145 | sensor: 146 | - platform: sht3xd 147 | temperature: 148 | name: "$devicename Temperature" 149 | humidity: 150 | name: "$devicename Relative Humidity" 151 | update_interval: 60s 152 | - platform: adc 153 | pin: $battery_pin 154 | name: "$devicename Battery" 155 | id: battery_voltage 156 | update_interval: 10s 157 | device_class: battery 158 | accuracy_decimals: 0 159 | attenuation: auto 160 | filters: 161 | - sliding_window_moving_average: 162 | window_size: 15 163 | send_every: 15 164 | send_first_at: 1 165 | - multiply: 1.6 166 | - lambda: |- 167 | auto r = ((x - 3) / 0.69 * 100.00); 168 | if (r >= 100) return 100; 169 | if (r > 0) return r; 170 | if (r <= 0) return 1; 171 | return 0; 172 | unit_of_measurement: '%' 173 | - platform: adc 174 | id: light_sensor 175 | name: "$devicename Illuminance" 176 | pin: $ldr_pin 177 | device_class: illuminance 178 | update_interval: 10s 179 | attenuation: auto 180 | unit_of_measurement: lx 181 | accuracy_decimals: 0 182 | filters: 183 | - lambda: |- 184 | return (x / 10000.0) * 2000000.0 - 15 ; 185 | on_value: 186 | then: 187 | - lambda: |- 188 | if ( id(aab_enable) ) { 189 | int n = x / 4 + id(aab_add); // new_value 190 | if (n > id(aab_max)) n = id(aab_max); 191 | if (n < id(aab_min)) n = id(aab_min); 192 | int c = id(rgb8x32)->get_brightness(); // current value 193 | int d = (n - c) * 100 / c; // diff in % 194 | if ( abs(d) > 2 ) id(rgb8x32)->set_brightness(n); 195 | } 196 | 197 | ota: 198 | password: !secret ota_password 199 | 200 | wifi: 201 | ssid: !secret wifi_ssid 202 | password: !secret wifi_password 203 | 204 | web_server: 205 | 206 | output: 207 | - platform: ledc 208 | pin: $buzzer_pin 209 | id: rtttl_out 210 | 211 | rtttl: 212 | output: rtttl_out 213 | 214 | i2c: 215 | sda: 21 216 | scl: 22 217 | scan: true 218 | id: bus_a 219 | 220 | light: 221 | - platform: neopixelbus 222 | id: ehmtx_light 223 | type: GRB 224 | variant: WS2812 225 | pin: $matrix_pin 226 | num_leds: 256 227 | color_correct: [30%, 30%, 30%] 228 | name: "$devicename Light" 229 | restore_mode: ALWAYS_OFF 230 | on_turn_on: 231 | lambda: |- 232 | id(ehmtx_display)->set_enabled(false); 233 | on_turn_off: 234 | lambda: |- 235 | id(ehmtx_display)->set_enabled(true); 236 | 237 | time: 238 | - platform: homeassistant 239 | on_time_sync: 240 | then: 241 | ds1307.write_time: 242 | - platform: ds1307 243 | update_interval: never 244 | id: ehmtx_time 245 | 246 | display: 247 | - platform: addressable_light 248 | id: ehmtx_display 249 | addressable_light_id: ehmtx_light 250 | width: 32 251 | height: 8 252 | pixel_mapper: |- 253 | if (y % 2 == 0) { 254 | return (y * 32) + x; 255 | } 256 | return (y * 32) + (31 - x); 257 | rotation: 0° 258 | update_interval: 16ms 259 | auto_clear_enabled: true 260 | lambda: |- 261 | id(rgb8x32)->tick(); 262 | id(rgb8x32)->draw(); 263 | 264 | ehmtx: 265 | id: rgb8x32 266 | clock_time: 6 267 | screen_time: 6 268 | icons2html: true 269 | matrix_component: ehmtx_display 270 | time_component: ehmtx_time 271 | font_id: ehmtx_font 272 | icons: 273 | - id: ha 274 | url: https://github.com/home-assistant/assets/raw/master/logo/logo-small.png 275 | - id: adguard 276 | url: https://github.com/walkxcode/dashboard-icons/blob/529fd23b82927773e54b0197cfa3c8f78864b701/png/adguard-home.png?raw=true 277 | -------------------------------------------------------------------------------- /UlanziTC001_night_mode.yaml: -------------------------------------------------------------------------------- 1 | substitutions: 2 | name: "ulanzi" 3 | room: "MyRoom" 4 | friendly_name: "Ulanzi TC001" 5 | matrix_pin: GPIO32 6 | buzzer_pin: GPIO15 7 | battery_pin: GPIO34 8 | ldr_pin: GPIO35 9 | left_button_pin: GPIO26 10 | mid_button_pin: GPIO27 11 | right_button_pin: GPIO14 12 | scl_pin: GPIO22 13 | sda_pin: GPIO21 14 | 15 | external_components: 16 | - source: 17 | type: git 18 | url: https://github.com/lubeda/EsphoMaTrix 19 | refresh: 60s 20 | components: [ ehmtx ] 21 | 22 | esphome: 23 | comment: "Ulanzi TC001" 24 | name: $name 25 | friendly_name: $friendly_name 26 | on_boot: 27 | then: 28 | - ds1307.read_time: 29 | 30 | esp32: 31 | board: esp32dev 32 | 33 | # WiFi connection 34 | wifi: 35 | ssid: !secret wifi_ssid 36 | password: !secret wifi_password 37 | ap: 38 | ssid: ap_${name} 39 | password: !secret ap_password 40 | ap_timeout: 1min 41 | 42 | 43 | # Unavailable for esp-idf https://github.com/esphome/feature-requests/issues/1649 44 | captive_portal: 45 | 46 | # Enable logging 47 | logger: 48 | 49 | # Enable over-the-air updates 50 | ota: 51 | password: !secret ota_password 52 | id: my_ota 53 | 54 | # Unavailable for esp-idf https://github.com/esphome/feature-requests/issues/1649 55 | # Enable Web server 56 | web_server: 57 | port: 80 58 | auth: 59 | username: admin 60 | password: !secret web_server_password 61 | 62 | font: 63 | # adapt the filename to your local settings 64 | - file: Calcium.ttf 65 | id: ehmtx_font 66 | size: 16 67 | glyphs: | 68 | !?"%‰()+*=,-_.:°µ²³0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnÖÄÜöäüopqrstuvwxyz€$@<>/ 69 | 70 | globals: 71 | # aab = auto-adjustable brightness 72 | - id: aab_enable 73 | type: "bool" 74 | restore_value: true 75 | initial_value: "true" 76 | - id: aab_add 77 | type: int 78 | initial_value: '10' 79 | - id: aab_max 80 | type: int 81 | initial_value: '220' 82 | - id: aab_min 83 | type: int 84 | initial_value: '40' 85 | - id: night_enable 86 | type: "bool" 87 | restore_value: true 88 | initial_value: "false" 89 | 90 | ehmtx: 91 | id: rgb8x32 92 | time_component: ehmtx_time 93 | matrix_component: ehmtx_display 94 | clock_time: 8 # duration to display the clock after this time the date is display until next "show_screen" 95 | clock_interval: 20 # show the clock at least each x seconds 96 | screen_time: 15 # duration to display a screen or a clock/date sequence 97 | date_format: "%d.%m" # defaults "%d.%m." (use "%m.%d." for the US) 98 | time_format: "%H:%M" # defaults "%H:%M" (use "%I:%M%p" for the US) 99 | show_dow: false # draw the day indicator on the bottom of the screen, defaults to true 100 | show_date: true # show the date for show_screen - show_clock seconds otherwise only shows the clock for show_screen seconds, defaults to true 101 | week_start_monday: true # default monday is first day of week, false = sunday 102 | yoffset: 8 # the text is aligned BASELINE_LEFT, the baseline defaults to 6 103 | xoffset: 1 # the text is aligned BASELINE_LEFT, the left defaults to 1 104 | scroll_interval: 80 # the interval in ms to scroll the text (default=80), should be a multiple of the update_interval from the display (default: 16ms) 105 | frame_interval: 192 # the interval in ms to display the next anim frame (default=192), should be a multiple of the update_interval from the display (default: 16ms) 106 | font_id: ehmtx_font 107 | icons: 108 | - id: ha 109 | lameid: 7956 110 | - id: tempc 111 | lameid: 2422 112 | - id: plug 113 | lameid: 403 114 | - id: humidity 115 | lameid: 51764 116 | - id: co2 117 | lameid: 30662 118 | - id: weather_clear_night 119 | lameid: 53383 120 | - id: weather_cloudy 121 | lameid: 53384 122 | - id: weather_fog 123 | lameid: 12196 124 | - id: weather_hail 125 | lameid: 53385 126 | - id: weather_lightning 127 | lameid: 50231 128 | - id: weather_lightning_rainy 129 | lameid: 49299 130 | - id: weather_partlycloudy 131 | lameid: 53802 132 | - id: weather_pouring 133 | lameid: 49300 134 | - id: weather_rainy 135 | lameid: 2284 136 | - id: weather_snowy 137 | lameid: 2289 138 | - id: weather_snowy_rainy 139 | lameid: 49301 140 | - id: weather_sunny 141 | lameid: 1246 142 | - id: weather_windy 143 | lameid: 17076 144 | - id: weather_windy_variant 145 | lameid: 15618 146 | - id: weather_exceptional 147 | lameid: 5464 148 | - id: washing_machine_ready 149 | lameid: 26673 150 | - id: dryer_ready 151 | lameid: 48497 152 | - id: dishwasher_ready 153 | lameid: 47488 154 | - id: door_ringing 155 | lameid: 24800 156 | - id: fire 157 | lameid: 24873 158 | # on_next_clock: 159 | # lambda: |- 160 | # id(rgb8x32)->set_clock_color(235, 0, 0); 161 | # id(rgb8x32)->set_brightness(30); 162 | 163 | binary_sensor: 164 | - platform: status 165 | name: "Status" 166 | - platform: gpio 167 | pin: 168 | number: $left_button_pin 169 | inverted: true 170 | name: "Left button" 171 | on_press: 172 | then: 173 | - number.decrement: screen_brightness 174 | - platform: gpio 175 | pin: 176 | inverted: true 177 | number: $mid_button_pin 178 | mode: INPUT_PULLUP 179 | name: "Middle button" 180 | on_press: 181 | then: 182 | - switch.toggle: displaycontrol 183 | - platform: gpio 184 | pin: 185 | number: $right_button_pin 186 | inverted: true 187 | name: "Right button" 188 | on_press: 189 | then: 190 | - number.increment: screen_brightness 191 | # example to switch to next screen 192 | # lambda: |- 193 | # id(rgb8x32)->skip_screen(); 194 | 195 | # Enable Home Assistant API 196 | api: 197 | encryption: 198 | key: !secret api_password 199 | services: 200 | - service: alarm 201 | variables: 202 | icon_name: string 203 | text: string 204 | then: 205 | lambda: |- 206 | if (!id(night_enable)) { 207 | id(rgb8x32)->add_screen(icon_name,text,7,20,true); 208 | id(rgb8x32)->force_screen(icon_name); 209 | } 210 | - service: add_screen_respect_night_mode 211 | variables: 212 | icon_name: string 213 | text: string 214 | lifetime: int 215 | screen_time: int 216 | alarm: bool 217 | then: 218 | lambda: |- 219 | if (!id(night_enable)) { 220 | id(rgb8x32)->add_screen(icon_name,text,lifetime,screen_time,alarm); 221 | } 222 | - service: brightness 223 | variables: 224 | brightness: int 225 | then: 226 | lambda: |- 227 | id(rgb8x32)->set_brightness(brightness); 228 | - service: icons 229 | then: 230 | lambda: |- 231 | id(rgb8x32)->show_all_icons(); 232 | - service: skip_screen 233 | then: 234 | lambda: |- 235 | id(rgb8x32)->skip_screen(); 236 | - service: tuneplay 237 | variables: 238 | tune: string 239 | then: 240 | - rtttl.play: 241 | rtttl: !lambda 'return tune;' 242 | 243 | number: 244 | - platform: template 245 | name: "Brightness" 246 | id: screen_brightness 247 | min_value: 0 248 | max_value: 255 249 | update_interval: 1s 250 | step: 1 251 | lambda: |- 252 | return id(rgb8x32)->get_brightness(); 253 | set_action: 254 | lambda: |- 255 | id(rgb8x32)->set_brightness(x); 256 | 257 | switch: 258 | - platform: template 259 | name: "Display" 260 | id: displaycontrol 261 | icon: "mdi:power" 262 | restore_mode: ALWAYS_ON 263 | lambda: |- 264 | return id(rgb8x32)->show_display; 265 | turn_on_action: 266 | lambda: |- 267 | id(rgb8x32)->set_display_on(); 268 | turn_off_action: 269 | lambda: |- 270 | id(rgb8x32)->set_display_off(); 271 | - platform: template 272 | name: "Auto-Adjust Brightness" 273 | id: switch_autobrightness 274 | icon: mdi:brightness-auto 275 | restore_mode: RESTORE_DEFAULT_ON 276 | lambda: |- 277 | if (id(aab_enable)) { 278 | return true; 279 | } else { 280 | return false; 281 | } 282 | turn_on_action: 283 | lambda: |- 284 | id(aab_enable) = true; 285 | turn_off_action: 286 | lambda: |- 287 | id(aab_enable) = false; 288 | - platform: template 289 | name: "Night mode" 290 | id: switch_night_mode 291 | icon: mdi:sleep 292 | restore_mode: RESTORE_DEFAULT_OFF 293 | lambda: |- 294 | if (id(night_enable)) { 295 | return true; 296 | } else { 297 | return false; 298 | } 299 | turn_on_action: 300 | lambda: |- 301 | id(night_enable) = true; 302 | id(rgb8x32)->del_screen("*"); 303 | id(rgb8x32)->set_show_date(false); 304 | id(rgb8x32)->set_brightness(30); 305 | id(rgb8x32)->set_clock_color(235, 0, 0); 306 | turn_off_action: 307 | lambda: |- 308 | id(night_enable) = false; 309 | id(rgb8x32)->set_show_date(true); 310 | id(rgb8x32)->set_clock_color(255, 255, 255); 311 | sensor: 312 | - platform: sht3xd 313 | temperature: 314 | name: "Temperature" 315 | humidity: 316 | name: "Humidity" 317 | update_interval: 60s 318 | - platform: adc 319 | id: light_sensor 320 | name: "Illuminance" 321 | pin: $ldr_pin 322 | device_class: illuminance 323 | update_interval: 10s 324 | attenuation: auto 325 | unit_of_measurement: lx 326 | accuracy_decimals: 0 327 | filters: 328 | - lambda: |- 329 | return (x / 10000.0) * 2000000.0 - 15 ; 330 | on_value: 331 | then: 332 | - lambda: |- 333 | if ( id(aab_enable) && !id(night_enable) ) { 334 | int n = x / 4 + id(aab_add); // new_value 335 | if (n > id(aab_max)) n = id(aab_max); 336 | if (n < id(aab_min)) n = id(aab_min); 337 | int c = id(rgb8x32)->get_brightness(); // current value 338 | int d = (n - c) * 100 / c; // diff in % 339 | if ( abs(d) > 2 ) id(rgb8x32)->set_brightness(n); 340 | } 341 | 342 | output: 343 | - platform: ledc 344 | pin: $buzzer_pin 345 | id: rtttl_out 346 | 347 | rtttl: 348 | output: rtttl_out 349 | 350 | i2c: 351 | sda: 21 352 | scl: 22 353 | scan: true 354 | id: bus_a 355 | 356 | light: 357 | - platform: neopixelbus 358 | id: ehmtx_light 359 | type: GRB 360 | variant: WS2812 361 | pin: $matrix_pin 362 | num_leds: 256 363 | color_correct: [30%, 30%, 30%] 364 | name: "Light" 365 | restore_mode: ALWAYS_OFF 366 | on_turn_on: 367 | lambda: |- 368 | id(ehmtx_display)->set_enabled(false); 369 | on_turn_off: 370 | lambda: |- 371 | id(ehmtx_display)->set_enabled(true); 372 | 373 | time: 374 | - platform: homeassistant 375 | on_time_sync: 376 | then: 377 | ds1307.write_time: 378 | - platform: ds1307 379 | update_interval: never 380 | id: ehmtx_time 381 | 382 | display: 383 | - platform: addressable_light 384 | id: ehmtx_display 385 | addressable_light_id: ehmtx_light 386 | width: 32 387 | height: 8 388 | pixel_mapper: |- 389 | if (y % 2 == 0) { 390 | return (y * 32) + x; 391 | } 392 | return (y * 32) + (31 - x); 393 | rotation: 0° 394 | update_interval: 16ms 395 | auto_clear_enabled: true 396 | lambda: |- 397 | id(rgb8x32)->tick(); 398 | id(rgb8x32)->draw(); 399 | 400 | -------------------------------------------------------------------------------- /components/ehmtx/EHMTX.h: -------------------------------------------------------------------------------- 1 | #ifndef EHMTX_H 2 | #define EHMTX_H 3 | 4 | #include "esphome.h" 5 | #include "esphome/components/time/real_time_clock.h" 6 | 7 | 8 | const uint8_t MAXQUEUE = 24; 9 | const uint8_t MAXICONS = 90; 10 | const uint8_t TEXTSCROLLSTART = 8; 11 | const uint8_t TEXTSTARTOFFSET = (32 - 8); 12 | 13 | const uint16_t TICKINTERVAL = 1000; // each 1000ms 14 | static const char *const EHMTX_VERSION = "Version: 2023.4.0"; 15 | static const char *const TAG = "EHMTX"; 16 | 17 | namespace esphome 18 | { 19 | class EHMTX_screen; 20 | class EHMTX_store; 21 | class EHMTX_Icon; 22 | class EHMTXNextScreenTrigger; 23 | class EHMTXNextClockTrigger; 24 | 25 | class EHMTX : public PollingComponent, public api::CustomAPIDevice { 26 | protected: 27 | float get_setup_priority() const override { return esphome::setup_priority::AFTER_CONNECTION; } 28 | uint8_t brightness_; 29 | bool week_starts_monday; 30 | bool show_day_of_week; 31 | std::string time_fmt; 32 | std::string date_fmt; 33 | Color indicator_color; 34 | Color clock_color; 35 | Color today_color; 36 | Color weekday_color; 37 | EHMTX_store *store; 38 | std::vector on_next_screen_triggers_; 39 | std::vector on_next_clock_triggers_; 40 | void internal_add_screen(uint8_t icon, std::string text, uint16_t lifetime,uint16_t show_time, bool alarm); 41 | 42 | public: 43 | EHMTX(); 44 | Color text_color, alarm_color, gauge_color; 45 | void dump_config(); 46 | bool show_screen; 47 | bool show_indicator; 48 | bool show_gauge; 49 | bool show_date; 50 | uint8_t gauge_value; 51 | uint8_t scroll_count; 52 | bool show_icons; 53 | void force_screen(std::string name); 54 | EHMTX_Icon *icons[MAXICONS]; 55 | EHMTX_screen *icon_screen; 56 | void add_icon(EHMTX_Icon *icon); 57 | bool show_display; 58 | bool has_active_screen; 59 | addressable_light::AddressableLightDisplay *display; 60 | esphome::time::RealTimeClock *clock; 61 | display::BaseFont *font; 62 | int8_t yoffset, xoffset; 63 | uint8_t find_icon(std::string name); 64 | bool string_has_ending(std::string const &fullString, std::string const &ending); 65 | bool show_seconds; 66 | //uint16_t duration; // in minutes how long is a screen valid 67 | uint16_t scroll_interval; // ms to between scrollsteps 68 | uint16_t frame_interval; // ms to next_frame() 69 | uint16_t clock_time; // seconds display of screen_time - clock_time = date_time 70 | uint16_t hold_time; // seconds display of screen_time to extend 71 | uint16_t clock_interval; // seconds display of screen_time - clock_time = date_time 72 | uint16_t screen_time; // seconds display of screen 73 | uint8_t icon_count; // max iconnumber -1 74 | unsigned long last_scroll_time; 75 | unsigned long last_anim_time; 76 | time_t last_clock_time = 0; // starttime clock display 77 | time_t next_action_time = 0; // when is the next screen change 78 | void draw_day_of_week(); 79 | void show_all_icons(); 80 | void tick(); 81 | void draw(); 82 | void get_status(); 83 | void skip_screen(); 84 | void hold_screen(); 85 | std::string get_current(); 86 | void set_display(addressable_light::AddressableLightDisplay *disp); 87 | void set_screen_time(uint16_t t); 88 | void set_clock_time(uint16_t t); 89 | void set_hold_time(uint16_t t); 90 | void set_clock_interval(uint16_t t); 91 | void set_show_day_of_week(bool b); 92 | void set_show_seconds(bool b); 93 | void set_show_date(bool b); 94 | void set_font_offset(int8_t x, int8_t y); 95 | void set_week_start(bool b); 96 | void set_brightness(int b); // int because of register_service! 97 | uint8_t get_brightness(); 98 | void add_screen(std::string icon, std::string text, int duration, int showt_time, bool alarm); 99 | void del_screen(std::string iname); 100 | void set_clock(esphome::time::RealTimeClock *clock); 101 | void set_font(display::BaseFont *font); 102 | void set_frame_interval(uint16_t interval); 103 | void set_scroll_interval(uint16_t interval); 104 | void set_scroll_count(uint8_t count); 105 | void set_duration(uint8_t d); 106 | void set_indicator_off(); 107 | void set_time_format(std::string s); 108 | void set_date_format(std::string s); 109 | void set_indicator_on(int r, int g, int b); 110 | void set_gauge_off(); 111 | void set_gauge_value(int v); // int because of register_service 112 | void set_gauge_color(int r, int g, int b); 113 | void set_text_color(int r, int g, int b); 114 | void set_clock_color(int r, int g, int b); 115 | void set_today_color(int r, int g, int b); 116 | void set_weekday_color(int r, int g, int b); 117 | void set_alarm_color(int r, int g, int b); 118 | void set_screen_color(std::string icon_name,int r, int g, int b); 119 | void set_icon_count(uint8_t ic); 120 | void draw_clock(); 121 | void draw_gauge(); 122 | void add_on_next_screen_trigger(EHMTXNextScreenTrigger *t) { this->on_next_screen_triggers_.push_back(t); } 123 | void add_on_next_clock_trigger(EHMTXNextClockTrigger *t) { this->on_next_clock_triggers_.push_back(t); } 124 | void setup(); 125 | void update(); 126 | void set_display_on(); 127 | void set_display_off(); 128 | }; 129 | 130 | class EHMTX_store 131 | { 132 | protected: 133 | EHMTX_screen *slots[MAXQUEUE]; 134 | uint8_t active_slot; 135 | uint8_t force_screen; 136 | uint8_t count_active_screens(); 137 | 138 | public: 139 | EHMTX_store(EHMTX *config); 140 | void force_next_screen(uint8_t icon_id); 141 | esphome::time::RealTimeClock *clock; 142 | EHMTX_screen *find_free_screen(uint8_t icon); 143 | void delete_screen(uint8_t icon); 144 | bool move_next(); 145 | void hold_current(uint _sec); 146 | EHMTX_screen *current(); 147 | void set_text_color(uint8_t icon_id, Color c); 148 | void log_status(); 149 | }; 150 | 151 | class EHMTX_screen 152 | { 153 | protected: 154 | uint16_t shiftx_; 155 | uint16_t pixels_; 156 | uint8_t centerx_; 157 | EHMTX *config_; 158 | 159 | public: 160 | uint16_t screen_time; 161 | bool alarm; 162 | time_t endtime; 163 | uint8_t icon; 164 | Color text_color; 165 | std::string text; 166 | 167 | EHMTX_screen(EHMTX *config); 168 | 169 | bool active(); 170 | bool is_alarm(); 171 | void draw(); 172 | void draw_(); 173 | bool isfree(); 174 | void reset_shiftx(); 175 | bool update_slot(uint8_t _icon); 176 | void update_screen(); 177 | bool del_slot(uint8_t _icon); 178 | void hold_slot(uint8_t _sec); 179 | void set_text(std::string text, uint8_t icon, uint16_t pixel, uint16_t et, uint16_t st); 180 | void set_text_color(uint8_t icon_id,Color text_color); 181 | }; 182 | 183 | class EHMTXNextScreenTrigger : public Trigger 184 | { 185 | public: 186 | explicit EHMTXNextScreenTrigger(EHMTX *parent) { parent->add_on_next_screen_trigger(this); } 187 | void process(std::string, std::string); 188 | }; 189 | 190 | class EHMTXNextClockTrigger : public Trigger<> 191 | { 192 | public: 193 | explicit EHMTXNextClockTrigger(EHMTX *parent) { parent->add_on_next_clock_trigger(this); } 194 | void process(); 195 | }; 196 | 197 | template 198 | class SetBrightnessAction : public Action 199 | { 200 | public: 201 | SetBrightnessAction(EHMTX *parent) : parent_(parent) {} 202 | TEMPLATABLE_VALUE(uint8_t, brightness) 203 | 204 | void play(Ts... x) override 205 | { 206 | auto brightness = this->brightness_.value(x...); 207 | 208 | this->parent_->set_brightness(brightness); 209 | } 210 | 211 | protected: 212 | EHMTX *parent_; 213 | }; 214 | 215 | template 216 | class SetScreenColorAction : public Action 217 | { 218 | public: 219 | SetScreenColorAction(EHMTX *parent) : parent_(parent) {} 220 | 221 | TEMPLATABLE_VALUE(std::string, icon) 222 | TEMPLATABLE_VALUE(uint8_t, red) 223 | TEMPLATABLE_VALUE(uint8_t, green) 224 | TEMPLATABLE_VALUE(uint8_t, blue) 225 | 226 | void play(Ts... x) override 227 | { 228 | auto icon = this->icon_.value(x...); 229 | 230 | this->parent_->set_screen_color(icon,this->red_.value(x...) ,this->blue_.value(x...),this->green_.value(x...)); 231 | } 232 | 233 | protected: 234 | EHMTX *parent_; 235 | }; 236 | 237 | template 238 | class AddScreenAction : public Action 239 | { 240 | public: 241 | AddScreenAction(EHMTX *parent) : parent_(parent) {} 242 | TEMPLATABLE_VALUE(std::string, icon) 243 | TEMPLATABLE_VALUE(std::string, text) 244 | TEMPLATABLE_VALUE(uint8_t, lifetime) 245 | TEMPLATABLE_VALUE(uint16_t, screen_time) 246 | TEMPLATABLE_VALUE(bool, alarm) 247 | 248 | void play(Ts... x) override 249 | { 250 | auto icon = this->icon_.value(x...); 251 | auto text = this->text_.value(x...); 252 | auto lifetime = this->lifetime_.value(x...); 253 | auto screen_time = this->screen_time_.value(x...); 254 | auto alarm = this->alarm_.value(x...); 255 | 256 | this->parent_->add_screen(icon, text, lifetime, screen_time, alarm); 257 | } 258 | 259 | protected: 260 | EHMTX *parent_; 261 | }; 262 | 263 | template 264 | class SetIndicatorOn : public Action 265 | { 266 | public: 267 | SetIndicatorOn(EHMTX *parent) : parent_(parent) {} 268 | TEMPLATABLE_VALUE(uint8_t, red) 269 | TEMPLATABLE_VALUE(uint8_t, green) 270 | TEMPLATABLE_VALUE(uint8_t, blue) 271 | 272 | void play(Ts... x) override 273 | { 274 | this->parent_->set_indicator_on(this->red_.value(x...), this->green_.value(x...), this->blue_.value(x...)); 275 | } 276 | 277 | protected: 278 | EHMTX *parent_; 279 | }; 280 | 281 | template 282 | class SetClockColor : public Action 283 | { 284 | public: 285 | SetClockColor(EHMTX *parent) : parent_(parent) {} 286 | TEMPLATABLE_VALUE(uint8_t, red) 287 | TEMPLATABLE_VALUE(uint8_t, green) 288 | TEMPLATABLE_VALUE(uint8_t, blue) 289 | 290 | void play(Ts... x) override 291 | { 292 | this->parent_->set_clock_color(this->red_.value(x...), this->green_.value(x...), this->blue_.value(x...)); 293 | } 294 | 295 | protected: 296 | EHMTX *parent_; 297 | }; 298 | 299 | template 300 | class SetAlarmColor : public Action 301 | { 302 | public: 303 | SetAlarmColor(EHMTX *parent) : parent_(parent) {} 304 | TEMPLATABLE_VALUE(uint8_t, red) 305 | TEMPLATABLE_VALUE(uint8_t, green) 306 | TEMPLATABLE_VALUE(uint8_t, blue) 307 | 308 | void play(Ts... x) override 309 | { 310 | this->parent_->set_alarm_color(this->red_.value(x...), this->green_.value(x...), this->blue_.value(x...)); 311 | } 312 | 313 | protected: 314 | EHMTX *parent_; 315 | }; 316 | 317 | template 318 | class SetTodayColor : public Action 319 | { 320 | public: 321 | SetTodayColor(EHMTX *parent) : parent_(parent) {} 322 | TEMPLATABLE_VALUE(uint8_t, red) 323 | TEMPLATABLE_VALUE(uint8_t, green) 324 | TEMPLATABLE_VALUE(uint8_t, blue) 325 | 326 | void play(Ts... x) override 327 | { 328 | this->parent_->set_today_color(this->red_.value(x...), this->green_.value(x...), this->blue_.value(x...)); 329 | } 330 | 331 | protected: 332 | EHMTX *parent_; 333 | }; 334 | 335 | template 336 | class SetShowDate : public Action 337 | { 338 | public: 339 | SetShowDate(EHMTX *parent) : parent_(parent) {} 340 | TEMPLATABLE_VALUE(uint8_t, flag) 341 | 342 | void play(Ts... x) override 343 | { 344 | this->parent_->set_show_date(this->flag_.value(x...)); 345 | } 346 | 347 | protected: 348 | EHMTX *parent_; 349 | }; 350 | 351 | template 352 | class SetShowDayOfWeek : public Action 353 | { 354 | public: 355 | SetShowDayOfWeek(EHMTX *parent) : parent_(parent) {} 356 | TEMPLATABLE_VALUE(uint8_t, flag) 357 | 358 | void play(Ts... x) override 359 | { 360 | this->parent_->set_show_day_of_week(this->flag_.value(x...)); 361 | } 362 | 363 | protected: 364 | EHMTX *parent_; 365 | }; 366 | 367 | template 368 | class SetTextColor : public Action 369 | { 370 | public: 371 | SetTextColor(EHMTX *parent) : parent_(parent) {} 372 | TEMPLATABLE_VALUE(uint8_t, red) 373 | TEMPLATABLE_VALUE(uint8_t, green) 374 | TEMPLATABLE_VALUE(uint8_t, blue) 375 | 376 | void play(Ts... x) override 377 | { 378 | this->parent_->set_text_color(this->red_.value(x...), this->green_.value(x...), this->blue_.value(x...)); 379 | } 380 | 381 | protected: 382 | EHMTX *parent_; 383 | }; 384 | 385 | template 386 | class SetWeekdayColor : public Action 387 | { 388 | public: 389 | SetWeekdayColor(EHMTX *parent) : parent_(parent) {} 390 | TEMPLATABLE_VALUE(uint8_t, red) 391 | TEMPLATABLE_VALUE(uint8_t, green) 392 | TEMPLATABLE_VALUE(uint8_t, blue) 393 | 394 | void play(Ts... x) override 395 | { 396 | this->parent_->set_weekday_color(this->red_.value(x...), this->green_.value(x...), this->blue_.value(x...)); 397 | } 398 | 399 | protected: 400 | EHMTX *parent_; 401 | }; 402 | 403 | template 404 | class SetIndicatorOff : public Action 405 | { 406 | public: 407 | SetIndicatorOff(EHMTX *parent) : parent_(parent) {} 408 | 409 | void play(Ts... x) override 410 | { 411 | this->parent_->set_indicator_off(); 412 | } 413 | 414 | protected: 415 | EHMTX *parent_; 416 | }; 417 | 418 | template 419 | class SetDisplayOn : public Action 420 | { 421 | public: 422 | SetDisplayOn(EHMTX *parent) : parent_(parent) {} 423 | 424 | void play(Ts... x) override 425 | { 426 | this->parent_->set_display_on(); 427 | } 428 | 429 | protected: 430 | EHMTX *parent_; 431 | }; 432 | 433 | template 434 | class SetDisplayOff : public Action 435 | { 436 | public: 437 | SetDisplayOff(EHMTX *parent) : parent_(parent) {} 438 | 439 | void play(Ts... x) override 440 | { 441 | this->parent_->set_display_off(); 442 | } 443 | 444 | protected: 445 | EHMTX *parent_; 446 | }; 447 | 448 | template 449 | class DeleteScreen : public Action 450 | { 451 | public: 452 | DeleteScreen(EHMTX *parent) : parent_(parent) {} 453 | TEMPLATABLE_VALUE(std::string, icon) 454 | 455 | void play(Ts... x) override 456 | { 457 | this->parent_->del_screen(this->icon_.value(x...)); 458 | } 459 | 460 | protected: 461 | EHMTX *parent_; 462 | }; 463 | 464 | template 465 | class ForceScreen : public Action 466 | { 467 | public: 468 | ForceScreen(EHMTX *parent) : parent_(parent) {} 469 | TEMPLATABLE_VALUE(std::string, icon) 470 | 471 | void play(Ts... x) override 472 | { 473 | this->parent_->force_screen(this->icon_.value(x...)); 474 | } 475 | 476 | protected: 477 | EHMTX *parent_; 478 | }; 479 | 480 | class EHMTX_Icon : public animation::Animation 481 | { 482 | protected: 483 | bool counting_up; 484 | 485 | public: 486 | EHMTX_Icon(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, image::ImageType type, std::string icon_name, bool revers, uint16_t frame_duration); 487 | std::string name; 488 | uint16_t frame_duration; 489 | bool fullscreen; 490 | void next_frame(); 491 | bool reverse; 492 | }; 493 | 494 | } 495 | 496 | #endif 497 | -------------------------------------------------------------------------------- /components/ehmtx/EHMTX.cpp: -------------------------------------------------------------------------------- 1 | #include "esphome.h" 2 | 3 | namespace esphome 4 | { 5 | EHMTX::EHMTX() : PollingComponent(TICKINTERVAL) 6 | { 7 | this->store = new EHMTX_store(this); 8 | this->icon_screen = new EHMTX_screen(this); 9 | this->show_screen = false; 10 | this->show_gauge = false; 11 | this->text_color = Color(240, 240, 240); 12 | this->today_color = Color(240, 240, 240); 13 | this->weekday_color = Color(100, 100, 100); 14 | this->clock_color = Color(240, 240, 240); 15 | this->alarm_color = Color(200, 50, 50); 16 | this->gauge_color = Color(100, 100, 200); 17 | this->gauge_value = 0; 18 | this->show_indicator = false; 19 | this->icon_count = 0; 20 | this->last_clock_time = 0; 21 | this->show_icons = false; 22 | this->show_display = true; 23 | } 24 | 25 | void EHMTX::force_screen(std::string name) 26 | { 27 | uint8_t icon_id = this->find_icon(name); 28 | if (icon_id < MAXICONS) 29 | { 30 | this->store->force_next_screen(icon_id); 31 | this->next_action_time = this->clock->now().timestamp + this->screen_time; 32 | ESP_LOGD(TAG, "force next screen: %s for %d sec", name.c_str(),this->screen_time); 33 | } 34 | } 35 | 36 | void EHMTX::set_screen_color(std::string icon_name,int r,int g,int b) 37 | { 38 | if (this->string_has_ending(icon_name, "*")) 39 | { 40 | // remove the * 41 | std::string comparename = icon_name.substr(0, icon_name.length() - 1); 42 | 43 | // iterate through the icons, comparing start only 44 | for (uint8_t icon_id = 0; icon_id < this->icon_count; icon_id++) 45 | { 46 | std::string iconname = this->icons[icon_id]->name.c_str(); 47 | if (iconname.rfind(comparename, 0) == 0) 48 | { 49 | ESP_LOGD(TAG, "set screen color: icon %d r: %d g: %d b: %d",icon_id,r,g,b); 50 | this->store->set_text_color(icon_id,Color(r,g,b)); 51 | } 52 | } 53 | } 54 | else 55 | { 56 | uint8_t icon_id = this->find_icon(icon_name.c_str()); 57 | ESP_LOGD(TAG, "set screen color: icon %d r: %d g: %d b: %d",icon_id,r,g,b); 58 | this->store->set_text_color(icon_id,Color(r,g,b)); 59 | } 60 | } 61 | 62 | void EHMTX::set_time_format(std::string s) 63 | { 64 | this->time_fmt = s; 65 | } 66 | 67 | void EHMTX::set_date_format(std::string s) 68 | { 69 | this->date_fmt = s; 70 | } 71 | 72 | void EHMTX::set_indicator_on(int r, int g, int b) 73 | { 74 | this->indicator_color = Color((uint8_t)r & 248, (uint8_t)g & 252, (uint8_t)b & 248); 75 | this->show_indicator = true; 76 | ESP_LOGD(TAG, "indicator r: %d g: %d b: %d", r, g, b); 77 | } 78 | 79 | void EHMTX::set_indicator_off() 80 | { 81 | this->show_indicator = false; 82 | ESP_LOGD(TAG, "indicator off"); 83 | } 84 | 85 | void EHMTX::set_display_off() 86 | { 87 | this->show_display = false; 88 | ESP_LOGD(TAG, "display off"); 89 | } 90 | 91 | void EHMTX::set_display_on() 92 | { 93 | this->show_display = true; 94 | ESP_LOGD(TAG, "display on"); 95 | } 96 | 97 | void EHMTX::set_today_color(int r, int g, int b) 98 | { 99 | this->today_color = Color((uint8_t)r & 248, (uint8_t)g & 252, (uint8_t)b & 248); 100 | ESP_LOGD("EHMTX", "today color r: %d g: %d b: %d", r, g, b); 101 | } 102 | 103 | void EHMTX::set_weekday_color(int r, int g, int b) 104 | { 105 | this->weekday_color = Color((uint8_t)r & 248, (uint8_t)g & 252, (uint8_t)b & 248); 106 | ESP_LOGD("EHMTX", "weekday color: %d g: %d b: %d", r, g, b); 107 | } 108 | 109 | void EHMTX::set_clock_color(int r, int g, int b) 110 | { 111 | this->clock_color = Color((uint8_t)r & 248, (uint8_t)g & 252, (uint8_t)b & 248); 112 | ESP_LOGD("EHMTX", "clock color r: %d g: %d b: %d", r, g, b); 113 | } 114 | 115 | void EHMTX::set_gauge_color(int r, int g, int b) 116 | { 117 | this->gauge_color = Color((uint8_t)r & 248, (uint8_t)g & 252, (uint8_t)b & 248); 118 | ESP_LOGD(TAG, "gauge color r: %d g: %d b: %d", r, g, b); 119 | } 120 | 121 | void EHMTX::set_alarm_color(int r, int g, int b) 122 | { 123 | this->alarm_color = Color((uint8_t)r & 248, (uint8_t)g & 252, (uint8_t)b & 248); 124 | ESP_LOGD(TAG, "alarm color r: %d g: %d b: %d", r, g, b); 125 | } 126 | 127 | void EHMTX::set_text_color(int r, int g, int b) 128 | { 129 | this->text_color = Color((uint8_t)r & 248, (uint8_t)g & 252, (uint8_t)b & 248); 130 | ESP_LOGD(TAG, "text color r: %d g: %d b: %d", r, g, b); 131 | } 132 | 133 | bool EHMTX::string_has_ending(std::string const &fullString, std::string const &ending) 134 | { 135 | if (fullString.length() >= ending.length()) 136 | { 137 | return (0 == fullString.compare(fullString.length() - ending.length(), ending.length(), ending)); 138 | } 139 | else 140 | { 141 | return false; 142 | } 143 | } 144 | 145 | uint8_t EHMTX::find_icon(std::string name) 146 | { 147 | for (uint8_t i = 0; i < this->icon_count; i++) 148 | { 149 | if (strcmp(this->icons[i]->name.c_str(), name.c_str()) == 0) 150 | { 151 | ESP_LOGD(TAG, "icon: %s found id: %d", name.c_str(), i); 152 | return i; 153 | } 154 | } 155 | ESP_LOGD(TAG, "icon: %s not found", name.c_str()); 156 | return MAXICONS; 157 | } 158 | 159 | void EHMTX::set_gauge_off() 160 | { 161 | this->show_gauge = false; 162 | ESP_LOGD(TAG, "gauge off"); 163 | } 164 | 165 | void EHMTX::set_gauge_value(int percent) 166 | { 167 | this->show_gauge = false; 168 | if (percent <= 100) 169 | { 170 | this->show_gauge = true; 171 | this->gauge_value = percent; // (uint8_t)(100 - percent) * 7 / 100; 172 | ESP_LOGD(TAG, "set gauge value: %d", percent); 173 | } 174 | } 175 | 176 | void EHMTX::draw_clock() 177 | { 178 | if (this->clock->now().timestamp > 6000) // valid time 179 | { 180 | time_t ts = this->clock->now().timestamp; 181 | if (!this->show_date or ((this->next_action_time - ts) < this->clock_time)) 182 | { 183 | this->display->strftime(this->xoffset + 15, this->yoffset, this->font, this->clock_color, display::TextAlign::BASELINE_CENTER, this->time_fmt.c_str(), 184 | this->clock->now()); 185 | if ((this->clock->now().second % 2 == 0) && this->show_seconds) 186 | { 187 | this->display->draw_pixel_at(0, 0, this->clock_color); 188 | } 189 | } 190 | else 191 | { 192 | this->display->strftime(this->xoffset + 15, this->yoffset, this->font, this->clock_color, display::TextAlign::BASELINE_CENTER, this->date_fmt.c_str(), 193 | this->clock->now()); 194 | } 195 | this->draw_day_of_week(); 196 | } 197 | else 198 | { 199 | this->display->print(this->xoffset + 15, this->yoffset, this->font, this->alarm_color, display::TextAlign::BASELINE_CENTER, "!t!"); 200 | } 201 | } 202 | 203 | void EHMTX::draw_gauge() 204 | { 205 | 206 | if (this->show_gauge) 207 | { 208 | this->display->line(0, 7, 0, 0, esphome::display::COLOR_OFF); 209 | this->display->line(1, 7, 1, 0, esphome::display::COLOR_OFF); 210 | if (this->gauge_value > 11) 211 | { 212 | uint8_t height = 7 - (int)(this->gauge_value/12.5); 213 | this->display->line(0, 7, 0, height, this->gauge_color); 214 | } 215 | } 216 | } 217 | 218 | void EHMTX::setup() 219 | { 220 | register_service(&EHMTX::get_status, "status"); 221 | register_service(&EHMTX::set_display_on, "display_on"); 222 | register_service(&EHMTX::set_display_off, "display_off"); 223 | register_service(&EHMTX::show_all_icons, "show_icons"); 224 | register_service(&EHMTX::hold_screen, "hold_screen"); 225 | register_service(&EHMTX::set_indicator_on, "indicator_on", {"r", "g", "b"}); 226 | register_service(&EHMTX::set_indicator_off, "indicator_off"); 227 | register_service(&EHMTX::set_gauge_off, "gauge_off"); 228 | register_service(&EHMTX::set_alarm_color, "alarm_color", {"r", "g", "b"}); 229 | register_service(&EHMTX::set_text_color, "text_color", {"r", "g", "b"}); 230 | register_service(&EHMTX::set_clock_color, "clock_color", {"r", "g", "b"}); 231 | register_service(&EHMTX::set_today_color, "today_color", {"r", "g", "b"}); 232 | register_service(&EHMTX::set_gauge_color, "gauge_color", {"r", "g", "b"}); 233 | register_service(&EHMTX::set_weekday_color, "weekday_color", {"r", "g", "b"}); 234 | register_service(&EHMTX::set_screen_color, "set_screen_color", {"icon_name","r", "g", "b"}); 235 | register_service(&EHMTX::add_screen, "add_screen", {"icon_name", "text", "lifetime","screen_time", "alarm"}); 236 | register_service(&EHMTX::force_screen, "force_screen", {"icon_name"}); 237 | register_service(&EHMTX::del_screen, "del_screen", {"icon_name"}); 238 | register_service(&EHMTX::set_gauge_value, "gauge_value", {"percent"}); 239 | register_service(&EHMTX::set_brightness, "brightness", {"value"}); 240 | } 241 | 242 | void EHMTX::update() // called from polling component 243 | { 244 | } 245 | 246 | void EHMTX::tick() 247 | { 248 | time_t ts = this->clock->now().timestamp; 249 | 250 | if (ts > this->next_action_time) 251 | { 252 | if (this->show_icons) 253 | { 254 | this->next_action_time = ts + this->screen_time; 255 | uint8_t i = this->icon_screen->icon; 256 | ++i; 257 | if (i < this->icon_count) 258 | { 259 | int x, y, w, h; 260 | this->display->get_text_bounds(0, 0, this->icons[i]->name.c_str(), this->font, display::TextAlign::LEFT, &x, &y, &w, &h); 261 | this->icon_screen->set_text(this->icons[i]->name, i, w, 1,1); 262 | ESP_LOGD(TAG, "show all icons icon: %d name: %s", i, this->icons[i]->name.c_str()); 263 | } 264 | else 265 | { 266 | this->show_icons = false; 267 | ESP_LOGD(TAG, "show all icons done"); 268 | } 269 | } 270 | else 271 | { 272 | if (this->clock_time == 0) 273 | { 274 | this->has_active_screen = this->store->move_next(); 275 | this->show_screen = true; 276 | } 277 | else 278 | { 279 | this->show_screen = false; 280 | 281 | if (!(ts - this->last_clock_time > this->clock_interval)) // force clock if last time more the 60s old 282 | { 283 | this->has_active_screen = this->store->move_next(); 284 | if (this->has_active_screen) 285 | { 286 | this->show_screen = true; 287 | } 288 | } 289 | } 290 | 291 | if (this->show_screen == false) 292 | { 293 | ESP_LOGD(TAG, "next action: show clock/date for %d/%d sec", this->clock_time, this->screen_time - this->clock_time); 294 | for (auto *t : on_next_clock_triggers_) 295 | { 296 | t->process(); 297 | } 298 | this->last_clock_time = ts; 299 | this->next_action_time = ts + this->screen_time; 300 | } 301 | else 302 | { 303 | if (this->has_active_screen) 304 | { 305 | ESP_LOGD(TAG, "next action: show screen \"%s\" for %d sec", this->icons[this->store->current()->icon]->name.c_str(), this->store->current()->screen_time); 306 | this->next_action_time = ts + this->store->current()->screen_time; 307 | for (auto *t : on_next_screen_triggers_) 308 | { 309 | t->process(this->icons[this->store->current()->icon]->name, this->store->current()->text); 310 | } 311 | } 312 | else 313 | { 314 | this->next_action_time = ts; 315 | } 316 | } 317 | } 318 | } 319 | } 320 | 321 | void EHMTX::set_screen_time(uint16_t t) 322 | { 323 | this->screen_time = t; 324 | } 325 | 326 | void EHMTX::skip_screen() 327 | { 328 | this->store->move_next(); 329 | } 330 | 331 | void EHMTX::hold_screen() 332 | { 333 | this->next_action_time += this->hold_time; 334 | this->store->hold_current(this->hold_time); 335 | } 336 | 337 | void EHMTX::get_status() 338 | { 339 | time_t ts = this->clock->now().timestamp; 340 | ESP_LOGI(TAG, "status time: %d.%d.%d %02d:%02d", this->clock->now().day_of_month, 341 | this->clock->now().month, this->clock->now().year, 342 | this->clock->now().hour, this->clock->now().minute); 343 | ESP_LOGI(TAG, "status brightness: %d (0..255)", this->brightness_); 344 | ESP_LOGI(TAG, "status date format: %s", this->date_fmt.c_str()); 345 | ESP_LOGI(TAG, "status time format: %s", this->time_fmt.c_str()); 346 | ESP_LOGI(TAG, "status text_color: RGB(%d,%d,%d)", this->text_color.r, this->text_color.g, this->text_color.b); 347 | ESP_LOGI(TAG, "status alarm_color: RGB(%d,%d,%d)", this->alarm_color.r, this->alarm_color.g, this->alarm_color.b); 348 | if (this->show_indicator) 349 | { 350 | ESP_LOGI(TAG, "status indicator on"); 351 | } 352 | else 353 | { 354 | ESP_LOGI(TAG, "status indicator off"); 355 | } 356 | if (this->show_display) 357 | { 358 | ESP_LOGI(TAG, "status display on"); 359 | } 360 | else 361 | { 362 | ESP_LOGI(TAG, "status display off"); 363 | } 364 | 365 | this->store->log_status(); 366 | 367 | for (uint8_t i = 0; i < this->icon_count; i++) 368 | { 369 | ESP_LOGI(TAG, "status icon: %d name: %s", i, this->icons[i]->name.c_str()); 370 | } 371 | } 372 | 373 | void EHMTX::set_font(display::BaseFont *font) 374 | { 375 | this->font = font; 376 | } 377 | 378 | void EHMTX::set_frame_interval(uint16_t fi) 379 | { 380 | this->frame_interval = fi; 381 | } 382 | 383 | void EHMTX::set_scroll_interval(uint16_t si) 384 | { 385 | this->scroll_interval = si; 386 | } 387 | 388 | void EHMTX::del_screen(std::string icon_name) 389 | { 390 | // if has ending of * 391 | if (this->string_has_ending(icon_name, "*")) 392 | { 393 | // remove the * 394 | std::string comparename = icon_name.substr(0, icon_name.length() - 1); 395 | 396 | // iterate through the icons, comparing start only 397 | for (uint8_t icon_id = 0; icon_id < this->icon_count; icon_id++) 398 | { 399 | std::string iconname = this->icons[icon_id]->name.c_str(); 400 | if (iconname.rfind(comparename, 0) == 0) 401 | { 402 | this->store->delete_screen(icon_id); 403 | this->next_action_time = this->clock->now().timestamp; 404 | } 405 | } 406 | } 407 | else 408 | { 409 | uint8_t icon_id = this->find_icon(icon_name.c_str()); 410 | this->store->delete_screen(icon_id); 411 | this->next_action_time = this->clock->now().timestamp; 412 | } 413 | } 414 | 415 | void EHMTX::add_screen(std::string iconname, std::string text, int lifetime,int show_time, bool alarm) 416 | { 417 | uint8_t icon = this->find_icon(iconname.c_str()); 418 | this->internal_add_screen(icon, text, lifetime,show_time,alarm); 419 | ESP_LOGD(TAG, "add_screen icon: %d iconname: %s text: %s lifetime: %d screen_time: %d alarm: %d", icon, iconname.c_str(), text.c_str(), lifetime,show_time, alarm); 420 | } 421 | 422 | void EHMTX::internal_add_screen(uint8_t icon, std::string text, uint16_t lifetime,uint16_t show_time , bool alarm = false) 423 | { 424 | if (icon >= this->icon_count) 425 | { 426 | ESP_LOGD(TAG, "icon %d not found => default: 0", icon); 427 | icon = 0; 428 | } 429 | EHMTX_screen *screen = this->store->find_free_screen(icon); 430 | 431 | int x, y, w, h; 432 | this->display->get_text_bounds(0, 0, text.c_str(), this->font, display::TextAlign::LEFT, &x, &y, &w, &h); 433 | screen->alarm = alarm; 434 | screen->set_text(text, icon, w, lifetime, show_time); 435 | screen->text_color= this->text_color; 436 | } 437 | 438 | void EHMTX::set_show_date(bool b) 439 | { 440 | this->show_date = b; 441 | if (b) 442 | { 443 | ESP_LOGI(TAG, "show date"); 444 | } 445 | else 446 | { 447 | ESP_LOGI(TAG, "don't show date"); 448 | } 449 | } 450 | 451 | void EHMTX::set_show_seconds(bool b) 452 | { 453 | this->show_seconds = b; 454 | if (b) 455 | { 456 | ESP_LOGI(TAG, "show seconds"); 457 | } 458 | else 459 | { 460 | ESP_LOGI(TAG, "don't show seconds"); 461 | } 462 | } 463 | 464 | void EHMTX::set_show_day_of_week(bool b) 465 | { 466 | this->show_day_of_week = b; 467 | if (b) 468 | { 469 | ESP_LOGI(TAG, "show day of week"); 470 | } 471 | else 472 | { 473 | ESP_LOGI(TAG, "don't show day of week"); 474 | } 475 | } 476 | 477 | void EHMTX::set_week_start(bool b) 478 | { 479 | this->week_starts_monday = b; 480 | if (b) 481 | { 482 | ESP_LOGI(TAG, "weekstart: monday"); 483 | } 484 | else 485 | { 486 | ESP_LOGI(TAG, "weekstart: sunday"); 487 | } 488 | } 489 | 490 | void EHMTX::set_brightness(int value) 491 | { 492 | if (value < 256) 493 | { 494 | this->brightness_ = value; 495 | float br = (float)value / (float)255; 496 | ESP_LOGI(TAG, "set_brightness %d => %.2f %%", value, 100 * br); 497 | this->display->get_light()->set_correction(br,br,br); 498 | } 499 | } 500 | 501 | uint8_t EHMTX::get_brightness() 502 | { 503 | return this->brightness_; 504 | } 505 | 506 | std::string EHMTX::get_current() 507 | { 508 | return this->icons[this->store->current()->icon]->name; 509 | } 510 | 511 | void EHMTX::set_clock_time(uint16_t t) 512 | { 513 | this->clock_time = t; 514 | } 515 | 516 | void EHMTX::set_hold_time(uint16_t t) 517 | { 518 | this->hold_time = t; 519 | } 520 | 521 | void EHMTX::set_scroll_count(uint8_t c) 522 | { 523 | this->scroll_count = c; 524 | } 525 | 526 | void EHMTX::set_clock_interval(uint16_t t) 527 | { 528 | this->clock_interval = t; 529 | } 530 | 531 | void EHMTX::set_display(addressable_light::AddressableLightDisplay *disp) 532 | { 533 | this->display = disp; 534 | } 535 | 536 | void EHMTX::set_clock(time::RealTimeClock *clock) 537 | { 538 | this->clock = clock; 539 | this->store->clock = clock; 540 | } 541 | 542 | void EHMTX::draw_day_of_week() 543 | { 544 | if (this->show_day_of_week) 545 | { 546 | auto dow = this->clock->now().day_of_week - 1; // SUN = 0 547 | for (uint8_t i = 0; i <= 6; i++) 548 | { 549 | if (((!this->week_starts_monday) && (dow == i)) || 550 | ((this->week_starts_monday) && ((dow == (i + 1)) || ((dow == 0 && i == 6))))) 551 | { 552 | this->display->line(2 + i * 4, 7, i * 4 + 4, 7, this->today_color); 553 | } 554 | else 555 | { 556 | this->display->line(2 + i * 4, 7, i * 4 + 4, 7, this->weekday_color); 557 | } 558 | } 559 | } 560 | }; 561 | 562 | void EHMTX::set_font_offset(int8_t x, int8_t y) 563 | { 564 | this->xoffset = x; 565 | this->yoffset = y; 566 | } 567 | 568 | void EHMTX::dump_config() 569 | { 570 | ESP_LOGCONFIG(TAG, "EspHoMatriX %s", EHMTX_VERSION); 571 | ESP_LOGCONFIG(TAG, "Icons: %d of %d", this->icon_count, MAXICONS); 572 | ESP_LOGCONFIG(TAG, "Font offset: x=%d y=%d", this->xoffset, this->yoffset); 573 | ESP_LOGCONFIG(TAG, "Max screens: %d", MAXQUEUE); 574 | ESP_LOGCONFIG(TAG, "Date format: %s", this->date_fmt.c_str()); 575 | ESP_LOGCONFIG(TAG, "Time format: %s", this->time_fmt.c_str()); 576 | ESP_LOGCONFIG(TAG, "Interval (ms) scroll: %d frame: %d", this->scroll_interval, this->frame_interval); 577 | ESP_LOGCONFIG(TAG, "Displaytime (s) clock: %d screen: %d", this->clock_time, this->screen_time); 578 | if (this->show_day_of_week) 579 | { 580 | ESP_LOGCONFIG(TAG, "show day of week"); 581 | } 582 | if (this->show_date) 583 | { 584 | ESP_LOGCONFIG(TAG, "show date"); 585 | } 586 | if (this->week_starts_monday) 587 | { 588 | ESP_LOGCONFIG(TAG, "weekstart: monday"); 589 | } 590 | else 591 | { 592 | ESP_LOGCONFIG(TAG, "weekstart: sunday"); 593 | } 594 | } 595 | 596 | void EHMTX::add_icon(EHMTX_Icon *icon) 597 | { 598 | this->icons[this->icon_count] = icon; 599 | ESP_LOGD(TAG, "add_icon no.: %d name: %s frame_duration: %d ms", this->icon_count, icon->name.c_str(), icon->frame_duration); 600 | this->icon_count++; 601 | } 602 | 603 | void EHMTX::show_all_icons() 604 | { 605 | int x, y, w, h; 606 | ESP_LOGD(TAG, "show all icons icon: %s", this->icons[0]->name.c_str()); 607 | this->display->get_text_bounds(0, 0, this->icons[0]->name.c_str(), this->font, display::TextAlign::LEFT, &x, &y, &w, &h); 608 | this->icon_screen->set_text(this->icons[0]->name, 0, w, 1,1); 609 | this->show_icons = true; 610 | } 611 | 612 | void EHMTX::draw() 613 | { 614 | if (this->show_display) 615 | { 616 | if (this->show_icons) 617 | { 618 | this->icon_screen->draw(); 619 | } 620 | else 621 | { 622 | if (this->show_screen) 623 | { 624 | if (this->has_active_screen) 625 | { 626 | this->store->current()->draw(); 627 | } 628 | } 629 | else 630 | { 631 | this->draw_clock(); 632 | this->draw_gauge(); 633 | } 634 | } 635 | 636 | if (this->show_indicator) 637 | { 638 | this->display->line(31, 5, 29, 7, this->indicator_color); 639 | this->display->draw_pixel_at(30, 7, this->indicator_color); 640 | this->display->draw_pixel_at(31, 6, this->indicator_color); 641 | this->display->draw_pixel_at(31, 7, this->indicator_color); 642 | } 643 | } 644 | } 645 | 646 | void EHMTXNextScreenTrigger::process(std::string iconname, std::string text) 647 | { 648 | this->trigger(iconname, text); 649 | } 650 | 651 | void EHMTXNextClockTrigger::process() 652 | { 653 | this->trigger(); 654 | } 655 | 656 | } 657 | -------------------------------------------------------------------------------- /components/ehmtx/__init__.py: -------------------------------------------------------------------------------- 1 | from argparse import Namespace 2 | import logging 3 | import io 4 | import requests 5 | 6 | from esphome import core, automation 7 | from esphome.components import display, font, time 8 | import esphome.components.image as espImage 9 | import esphome.config_validation as cv 10 | import esphome.codegen as cg 11 | from esphome.const import CONF_BLUE, CONF_GREEN, CONF_RED, CONF_FILE, CONF_ID, CONF_BRIGHTNESS, CONF_RAW_DATA_ID, CONF_TIME, CONF_TRIGGER_ID 12 | from esphome.core import CORE, HexInt 13 | from esphome.cpp_generator import RawExpression 14 | 15 | _LOGGER = logging.getLogger(__name__) 16 | 17 | DEPENDENCIES = ["display", "light", "api"] 18 | AUTO_LOAD = ["ehmtx"] 19 | IMAGE_TYPE_RGB565 = 4 20 | MAXFRAMES = 110 21 | MAXICONS = 90 22 | ICONWIDTH = 8 23 | ICONHEIGHT = 8 24 | ICONBUFFERSIZE = ICONWIDTH * ICONHEIGHT * 4 25 | SVG_ICONSTART = '' 26 | SVG_FULLSCREENSTART = '' 27 | SVG_END = "" 28 | 29 | logging.warning(f"") 30 | logging.warning(f"If you are upgrading EsphoMaTrix from a version before 2023.4.0,") 31 | logging.warning(f"you should read the section https://github.com/lubeda/EsphoMaTrix/#how-to-update for tipps.") 32 | logging.warning(f"This version of EsphoMaTrix works only on esphome > 2023.7.0") 33 | logging.warning(f"") 34 | 35 | def rgb565_svg(x,y,r,g,b): 36 | return f"> 2)},{(g << 2) | (g >> 4)},{(b << 3) | (b >> 2)});\" x=\"{x*10}\" y=\"{y*10}\" width=\"10\" height=\"10\"/>" 37 | 38 | ehmtx_ns = cg.esphome_ns.namespace("esphome") 39 | EHMTX_ = ehmtx_ns.class_("EHMTX", cg.Component) 40 | Icons_ = ehmtx_ns.class_("EHMTX_Icon") 41 | 42 | NextScreenTrigger = ehmtx_ns.class_( 43 | "EHMTXNextScreenTrigger", automation.Trigger.template(cg.std_string) 44 | ) 45 | 46 | NextClockTrigger = ehmtx_ns.class_( 47 | "EHMTXNextClockTrigger", automation.Trigger.template(cg.std_string) 48 | ) 49 | 50 | CONF_CLOCKTIME = "clock_time" 51 | CONF_CLOCKINTERVAL = "clock_interval" 52 | CONF_SCREENTIME = "screen_time" 53 | CONF_EHMTX = "ehmtx" 54 | CONF_URL = "url" 55 | CONF_FLAG = "flag" 56 | CONF_TIMECOMPONENT = "time_component" 57 | CONF_LAMEID = "lameid" 58 | CONF_LIFETIME = "lifetime" 59 | CONF_ICONS = "icons" 60 | CONF_SHOWDOW = "show_dow" 61 | CONF_SHOWDATE = "show_date" 62 | CONF_FRAMEDURATION = "frame_duration" 63 | CONF_HOLD_TIME = "hold_time" 64 | CONF_SCROLLCOUNT = "scroll_count" 65 | CONF_MATRIXCOMPONENT = "matrix_component" 66 | CONF_HTML = "icons2html" 67 | CONF_SCROLLINTERVAL = "scroll_interval" 68 | CONF_FRAMEINTERVAL = "frame_interval" 69 | CONF_FONT_ID = "font_id" 70 | CONF_YOFFSET = "yoffset" 71 | CONF_XOFFSET = "xoffset" 72 | CONF_PINGPONG = "pingpong" 73 | CONF_TIME_FORMAT = "time_format" 74 | CONF_DATE_FORMAT = "date_format" 75 | CONF_ON_NEXT_SCREEN = "on_next_screen" 76 | CONF_ON_NEXT_CLOCK = "on_next_clock" 77 | CONF_SHOW_SECONDS = "show_seconds" 78 | CONF_WEEK_START_MONDAY = "week_start_monday" 79 | CONF_ICON = "icon_name" 80 | CONF_TEXT = "text" 81 | CONF_ALARM = "alarm" 82 | 83 | EHMTX_SCHEMA = cv.Schema({ 84 | cv.Required(CONF_ID): cv.declare_id(EHMTX_), 85 | cv.Required(CONF_TIMECOMPONENT): cv.use_id(time), 86 | cv.Required(CONF_MATRIXCOMPONENT): cv.use_id(display), 87 | cv.Required(CONF_FONT_ID): cv.use_id(font), 88 | cv.Optional( 89 | CONF_CLOCKTIME, default="5" 90 | ): cv.templatable(cv.positive_int), 91 | cv.Optional( 92 | CONF_CLOCKINTERVAL, default="60" 93 | ): cv.templatable(cv.positive_int), 94 | cv.Optional( 95 | CONF_YOFFSET, default="6" 96 | ): cv.templatable(cv.int_range(min=-32, max=32)), 97 | cv.Optional( 98 | CONF_HTML, default=False 99 | ): cv.boolean, 100 | cv.Optional( 101 | CONF_SHOW_SECONDS, default=False 102 | ): cv.boolean, 103 | cv.Optional( 104 | CONF_SHOWDATE, default=True 105 | ): cv.boolean, 106 | cv.Optional( 107 | CONF_WEEK_START_MONDAY, default=True 108 | ): cv.boolean, 109 | cv.Optional( 110 | CONF_SHOWDOW, default=True 111 | ): cv.boolean, 112 | cv.Optional( 113 | CONF_SHOWDATE, default=True 114 | ): cv.boolean, 115 | cv.Optional( 116 | CONF_TIME_FORMAT, default="%H:%M" 117 | ): cv.string, 118 | cv.Optional( 119 | CONF_DATE_FORMAT, default="%d.%m." 120 | ): cv.string, 121 | cv.Optional( 122 | CONF_XOFFSET, default="1" 123 | ): cv.templatable(cv.int_range(min=-32, max=32)), 124 | cv.Optional( 125 | CONF_HOLD_TIME, default="20" 126 | ): cv.templatable(cv.int_range(min=0, max=3600)), 127 | cv.Optional(CONF_SCROLLINTERVAL, default="80" 128 | ): cv.templatable(cv.positive_int), 129 | cv.Optional(CONF_SCROLLCOUNT, default="2" 130 | ): cv.templatable(cv.positive_int), 131 | cv.Optional( 132 | CONF_FRAMEINTERVAL, default="192" 133 | ): cv.templatable(cv.positive_int), 134 | cv.Optional( 135 | CONF_SCREENTIME, default="8" 136 | ): cv.templatable(cv.positive_int), 137 | cv.Optional(CONF_BRIGHTNESS, default=80): cv.templatable(cv.int_range(min=0, max=255)), 138 | cv.Optional(CONF_ON_NEXT_SCREEN): automation.validate_automation( 139 | { 140 | cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(NextScreenTrigger), 141 | } 142 | ), 143 | cv.Optional(CONF_ON_NEXT_CLOCK): automation.validate_automation( 144 | { 145 | cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(NextClockTrigger), 146 | } 147 | ), 148 | cv.Required(CONF_ICONS): cv.All( 149 | cv.ensure_list( 150 | { 151 | cv.Required(CONF_ID): cv.declare_id(Icons_), 152 | 153 | cv.Exclusive(CONF_FILE,"uri"): cv.file_, 154 | cv.Exclusive(CONF_URL,"uri"): cv.url, 155 | cv.Exclusive(CONF_LAMEID,"uri"): cv.string, 156 | cv.Optional( 157 | CONF_FRAMEDURATION, default="0" 158 | ): cv.templatable(cv.positive_int), 159 | cv.Optional( 160 | CONF_PINGPONG, default=False 161 | ): cv.boolean, 162 | cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8), 163 | } 164 | ), 165 | cv.Length(max=MAXICONS), 166 | )}) 167 | 168 | CONFIG_SCHEMA = cv.All(font.validate_pillow_installed, EHMTX_SCHEMA) 169 | 170 | ADD_SCREEN_ACTION_SCHEMA = cv.Schema( 171 | { 172 | cv.GenerateID(): cv.use_id(EHMTX_), 173 | cv.Required(CONF_ICON): cv.templatable(cv.string), 174 | cv.Optional(CONF_TEXT, default = ""): cv.templatable(cv.string), 175 | cv.Optional(CONF_LIFETIME, default = 5): cv.templatable(cv.positive_int), 176 | cv.Optional(CONF_SCREENTIME, default = 10): cv.templatable(cv.positive_int), 177 | cv.Optional(CONF_ALARM, default=False): cv.templatable(cv.boolean), 178 | } 179 | ) 180 | 181 | AddScreenAction = ehmtx_ns.class_("AddScreenAction", automation.Action) 182 | 183 | @automation.register_action( 184 | "ehmtx.add.screen", AddScreenAction, ADD_SCREEN_ACTION_SCHEMA 185 | ) 186 | async def ehmtx_add_screen_action_to_code(config, action_id, template_arg, args): 187 | paren = await cg.get_variable(config[CONF_ID]) 188 | var = cg.new_Pvariable(action_id, template_arg, paren) 189 | 190 | template_ = await cg.templatable(config[CONF_ICON], args, cg.std_string) 191 | cg.add(var.set_icon(template_)) 192 | 193 | template_ = await cg.templatable(config[CONF_TEXT], args, cg.std_string) 194 | cg.add(var.set_text(template_)) 195 | 196 | template_ = await cg.templatable(config[CONF_LIFETIME], args, cv.positive_int) 197 | cg.add(var.set_lifetime(template_)) 198 | 199 | template_ = await cg.templatable(config[CONF_SCREENTIME], args, cv.positive_int) 200 | cg.add(var.set_screen_time(template_)) 201 | 202 | template_ = await cg.templatable(config[CONF_ALARM], args, bool) 203 | cg.add(var.set_alarm(template_)) 204 | return var 205 | 206 | SET_BRIGHTNESS_ACTION_SCHEMA = cv.Schema( 207 | { 208 | cv.GenerateID(): cv.use_id(EHMTX_), 209 | cv.Optional(CONF_BRIGHTNESS, default=80): cv.templatable(cv.int_range(min=0, max=255)), 210 | } 211 | ) 212 | 213 | SetBrightnessAction = ehmtx_ns.class_("SetBrightnessAction", automation.Action) 214 | 215 | @automation.register_action( 216 | "ehmtx.set.brightness", SetBrightnessAction, SET_BRIGHTNESS_ACTION_SCHEMA 217 | ) 218 | async def ehmtx_set_brightness_action_to_code(config, action_id, template_arg, args): 219 | paren = await cg.get_variable(config[CONF_ID]) 220 | var = cg.new_Pvariable(action_id, template_arg, paren) 221 | template_ = await cg.templatable(config[CONF_BRIGHTNESS], args, cg.int_) 222 | cg.add(var.set_brightness(template_)) 223 | 224 | return var 225 | 226 | SET_SCREEN_COLOR_ACTION_SCHEMA = cv.Schema( 227 | { 228 | cv.GenerateID(): cv.use_id(EHMTX_), 229 | cv.Required(CONF_ICON): cv.templatable(cv.string), 230 | cv.Optional(CONF_RED,default=80): cv.templatable(cv.uint8_t,), 231 | cv.Optional(CONF_BLUE,default=80): cv.templatable(cv.uint8_t,), 232 | cv.Optional(CONF_GREEN,default=80): cv.templatable(cv.uint8_t,), 233 | } 234 | ) 235 | 236 | SetScreenColorAction = ehmtx_ns.class_("SetScreenColorAction", automation.Action) 237 | 238 | @automation.register_action( 239 | "ehmtx.screen.color", SetScreenColorAction, SET_SCREEN_COLOR_ACTION_SCHEMA 240 | ) 241 | async def ehmtx_set_screen_color_action_to_code(config, action_id, template_arg, args): 242 | paren = await cg.get_variable(config[CONF_ID]) 243 | 244 | var = cg.new_Pvariable(action_id, template_arg, paren) 245 | 246 | template_ = await cg.templatable(config[CONF_ICON], args, cg.std_string) 247 | cg.add(var.set_icon(template_)) 248 | template_ = await cg.templatable(config[CONF_RED], args, cg.int_) 249 | cg.add(var.set_red(template_)) 250 | template_ = await cg.templatable(config[CONF_GREEN], args, cg.int_) 251 | cg.add(var.set_green(template_)) 252 | template_ = await cg.templatable(config[CONF_BLUE], args, cg.int_) 253 | cg.add(var.set_blue(template_)) 254 | 255 | return var 256 | 257 | SET_COLOR_ACTION_SCHEMA = cv.Schema( 258 | { 259 | cv.GenerateID(): cv.use_id(EHMTX_), 260 | cv.Optional(CONF_RED,default=80): cv.templatable(cv.uint8_t,), 261 | cv.Optional(CONF_BLUE,default=80): cv.templatable(cv.uint8_t,), 262 | cv.Optional(CONF_GREEN,default=80): cv.templatable(cv.uint8_t,), 263 | } 264 | ) 265 | 266 | SetClockColorAction = ehmtx_ns.class_("SetClockColor", automation.Action) 267 | 268 | @automation.register_action( 269 | "ehmtx.clock.color", SetClockColorAction, SET_COLOR_ACTION_SCHEMA 270 | ) 271 | async def ehmtx_set_clock_color_action_to_code(config, action_id, template_arg, args): 272 | paren = await cg.get_variable(config[CONF_ID]) 273 | 274 | var = cg.new_Pvariable(action_id, template_arg, paren) 275 | template_ = await cg.templatable(config[CONF_RED], args, cg.int_) 276 | cg.add(var.set_red(template_)) 277 | template_ = await cg.templatable(config[CONF_GREEN], args, cg.int_) 278 | cg.add(var.set_green(template_)) 279 | template_ = await cg.templatable(config[CONF_BLUE], args, cg.int_) 280 | cg.add(var.set_blue(template_)) 281 | 282 | return var 283 | 284 | SetTextColorAction = ehmtx_ns.class_("SetTextColor", automation.Action) 285 | 286 | @automation.register_action( 287 | "ehmtx.text.color", SetTextColorAction, SET_COLOR_ACTION_SCHEMA 288 | ) 289 | async def ehmtx_set_text_color_action_to_code(config, action_id, template_arg, args): 290 | paren = await cg.get_variable(config[CONF_ID]) 291 | 292 | var = cg.new_Pvariable(action_id, template_arg, paren) 293 | template_ = await cg.templatable(config[CONF_RED], args, cg.int_) 294 | cg.add(var.set_red(template_)) 295 | template_ = await cg.templatable(config[CONF_GREEN], args, cg.int_) 296 | cg.add(var.set_green(template_)) 297 | template_ = await cg.templatable(config[CONF_BLUE], args, cg.int_) 298 | cg.add(var.set_blue(template_)) 299 | 300 | return var 301 | 302 | SetAlarmColorAction = ehmtx_ns.class_("SetAlarmColor", automation.Action) 303 | 304 | @automation.register_action( 305 | "ehmtx.alarm.color", SetAlarmColorAction, SET_COLOR_ACTION_SCHEMA 306 | ) 307 | async def ehmtx_set_alarm_color_action_to_code(config, action_id, template_arg, args): 308 | paren = await cg.get_variable(config[CONF_ID]) 309 | 310 | var = cg.new_Pvariable(action_id, template_arg, paren) 311 | template_ = await cg.templatable(config[CONF_RED], args, cg.int_) 312 | cg.add(var.set_red(template_)) 313 | template_ = await cg.templatable(config[CONF_GREEN], args, cg.int_) 314 | cg.add(var.set_green(template_)) 315 | template_ = await cg.templatable(config[CONF_BLUE], args, cg.int_) 316 | cg.add(var.set_blue(template_)) 317 | 318 | return var 319 | 320 | SET_FLAG_ACTION_SCHEMA = cv.Schema( 321 | { 322 | cv.GenerateID(): cv.use_id(EHMTX_), 323 | cv.Optional(CONF_FLAG,default=True): cv.templatable(cv.boolean), 324 | } 325 | ) 326 | 327 | SetShowDateAction = ehmtx_ns.class_("SetShowDate", automation.Action) 328 | 329 | @automation.register_action( 330 | "ehmtx.show.date", SetShowDateAction, SET_FLAG_ACTION_SCHEMA 331 | ) 332 | async def ehmtx_show_date_action_to_code(config, action_id, template_arg, args): 333 | paren = await cg.get_variable(config[CONF_ID]) 334 | 335 | var = cg.new_Pvariable(action_id, template_arg, paren) 336 | template_ = await cg.templatable(config[CONF_FLAG], args, cg.bool_) 337 | cg.add(var.set_flag(template_)) 338 | 339 | return var 340 | 341 | SetShowDayOfWeekAction = ehmtx_ns.class_("SetShowDayOfWeek", automation.Action) 342 | 343 | @automation.register_action( 344 | "ehmtx.show.dayofweek", SetShowDayOfWeekAction, SET_FLAG_ACTION_SCHEMA 345 | ) 346 | 347 | async def ehmtx_show_dayofweek_action_to_code(config, action_id, template_arg, args): 348 | paren = await cg.get_variable(config[CONF_ID]) 349 | 350 | var = cg.new_Pvariable(action_id, template_arg, paren) 351 | template_ = await cg.templatable(config[CONF_FLAG], args, cg.bool_) 352 | cg.add(var.set_flag(template_)) 353 | 354 | return var 355 | 356 | SetTodayColorAction = ehmtx_ns.class_("SetTodayColor", automation.Action) 357 | 358 | @automation.register_action( 359 | "ehmtx.today.color", SetTodayColorAction, SET_COLOR_ACTION_SCHEMA 360 | ) 361 | async def ehmtx_set_today_color_action_to_code(config, action_id, template_arg, args): 362 | paren = await cg.get_variable(config[CONF_ID]) 363 | 364 | var = cg.new_Pvariable(action_id, template_arg, paren) 365 | template_ = await cg.templatable(config[CONF_RED], args, cg.int_) 366 | cg.add(var.set_red(template_)) 367 | template_ = await cg.templatable(config[CONF_GREEN], args, cg.int_) 368 | cg.add(var.set_green(template_)) 369 | template_ = await cg.templatable(config[CONF_BLUE], args, cg.int_) 370 | cg.add(var.set_blue(template_)) 371 | return var 372 | 373 | SetWeekdayColorAction = ehmtx_ns.class_("SetWeekdayColor", automation.Action) 374 | 375 | @automation.register_action( 376 | "ehmtx.weekday.color", SetWeekdayColorAction, SET_COLOR_ACTION_SCHEMA 377 | ) 378 | 379 | async def ehmtx_set_week_color_action_to_code(config, action_id, template_arg, args): 380 | paren = await cg.get_variable(config[CONF_ID]) 381 | 382 | var = cg.new_Pvariable(action_id, template_arg, paren) 383 | template_ = await cg.templatable(config[CONF_RED], args, cg.int_) 384 | cg.add(var.set_red(template_)) 385 | template_ = await cg.templatable(config[CONF_GREEN], args, cg.int_) 386 | cg.add(var.set_green(template_)) 387 | template_ = await cg.templatable(config[CONF_BLUE], args, cg.int_) 388 | cg.add(var.set_blue(template_)) 389 | 390 | return var 391 | 392 | SetIndicatorOnAction = ehmtx_ns.class_("SetIndicatorOn", automation.Action) 393 | 394 | @automation.register_action( 395 | "ehmtx.indicator.on", SetIndicatorOnAction, SET_COLOR_ACTION_SCHEMA 396 | ) 397 | async def ehmtx_set_indicator_on_action_to_code(config, action_id, template_arg, args): 398 | paren = await cg.get_variable(config[CONF_ID]) 399 | 400 | var = cg.new_Pvariable(action_id, template_arg, paren) 401 | template_ = await cg.templatable(config[CONF_RED], args, cg.int_) 402 | cg.add(var.set_red(template_)) 403 | template_ = await cg.templatable(config[CONF_GREEN], args, cg.int_) 404 | cg.add(var.set_green(template_)) 405 | template_ = await cg.templatable(config[CONF_BLUE], args, cg.int_) 406 | cg.add(var.set_blue(template_)) 407 | 408 | return var 409 | 410 | DELETE_SCREEN_ACTION_SCHEMA = cv.Schema( 411 | { 412 | cv.GenerateID(): cv.use_id(EHMTX_), 413 | cv.Required(CONF_ICON): cv.templatable(cv.string), 414 | } 415 | ) 416 | 417 | DeleteScreenAction = ehmtx_ns.class_("DeleteScreen", automation.Action) 418 | 419 | @automation.register_action( 420 | "ehmtx.delete.screen", DeleteScreenAction, DELETE_SCREEN_ACTION_SCHEMA 421 | ) 422 | async def ehmtx_delete_screen_action_to_code(config, action_id, template_arg, args): 423 | paren = await cg.get_variable(config[CONF_ID]) 424 | 425 | var = cg.new_Pvariable(action_id, template_arg, paren) 426 | template_ = await cg.templatable(config[CONF_ICON], args, cg.std_string) 427 | cg.add(var.set_icon(template_)) 428 | 429 | return var 430 | 431 | ForceScreenAction = ehmtx_ns.class_("ForceScreen", automation.Action) 432 | 433 | @automation.register_action( 434 | "ehmtx.force.screen", ForceScreenAction, DELETE_SCREEN_ACTION_SCHEMA 435 | ) 436 | async def ehmtx_force_screen_action_to_code(config, action_id, template_arg, args): 437 | paren = await cg.get_variable(config[CONF_ID]) 438 | 439 | var = cg.new_Pvariable(action_id, template_arg, paren) 440 | template_ = await cg.templatable(config[CONF_ICON], args, cg.std_string) 441 | cg.add(var.set_icon(template_)) 442 | 443 | return var 444 | 445 | SetIndicatorOffAction = ehmtx_ns.class_("SetIndicatorOff", automation.Action) 446 | 447 | INDICATOR_OFF_ACTION_SCHEMA = cv.Schema( 448 | { 449 | cv.GenerateID(): cv.use_id(EHMTX_), 450 | } 451 | ) 452 | 453 | @automation.register_action( 454 | "ehmtx.indicator.off", SetIndicatorOffAction, INDICATOR_OFF_ACTION_SCHEMA 455 | ) 456 | async def ehmtx_set_indicator_off_action_to_code(config, action_id, template_arg, args): 457 | paren = await cg.get_variable(config[CONF_ID]) 458 | var = cg.new_Pvariable(action_id, template_arg, paren) 459 | 460 | return var 461 | 462 | SetDisplayOnAction = ehmtx_ns.class_("SetDisplayOn", automation.Action) 463 | 464 | DISPLAY_ON_ACTION_SCHEMA = cv.Schema( 465 | { 466 | cv.GenerateID(): cv.use_id(EHMTX_), 467 | } 468 | ) 469 | 470 | @automation.register_action( 471 | "ehmtx.display.on", SetDisplayOnAction, DISPLAY_ON_ACTION_SCHEMA 472 | ) 473 | async def ehmtx_set_display_on_action_to_code(config, action_id, template_arg, args): 474 | paren = await cg.get_variable(config[CONF_ID]) 475 | var = cg.new_Pvariable(action_id, template_arg, paren) 476 | 477 | return var 478 | 479 | SetDisplayOffAction = ehmtx_ns.class_("SetDisplayOff", automation.Action) 480 | 481 | DISPLAY_OFF_ACTION_SCHEMA = cv.Schema( 482 | { 483 | cv.GenerateID(): cv.use_id(EHMTX_), 484 | } 485 | ) 486 | @automation.register_action( 487 | "ehmtx.display.off", SetDisplayOffAction, DISPLAY_OFF_ACTION_SCHEMA 488 | ) 489 | async def ehmtx_set_display_off_action_to_code(config, action_id, template_arg, args): 490 | paren = await cg.get_variable(config[CONF_ID]) 491 | var = cg.new_Pvariable(action_id, template_arg, paren) 492 | 493 | return var 494 | 495 | CODEOWNERS = ["@lubeda"] 496 | 497 | async def to_code(config): 498 | 499 | from PIL import Image, ImageSequence 500 | 501 | def openImageFile(path): 502 | try: 503 | return Image.open(path) 504 | except Exception as e: 505 | raise core.EsphomeError(f" ICONS: Could not load image file {path}: {e}") 506 | 507 | def thumbnails(frames): 508 | for frame in frames: 509 | thumbnail = frame.copy() 510 | thumbnail.thumbnail((32,8), Image.ANTIALIAS) 511 | yield thumbnail 512 | 513 | var = cg.new_Pvariable(config[CONF_ID]) 514 | html_string = F"{CORE.config_path}" 515 | html_string += '''\ 516 | \ 519 | ''' 520 | for conf in config[CONF_ICONS]: 521 | 522 | if CONF_FILE in conf: 523 | path = CORE.relative_config_path(conf[CONF_FILE]) 524 | try: 525 | image = openImageFile(path) 526 | except Exception as e: 527 | raise core.EsphomeError(f" ICONS: Could not load image file {path}: {e}") 528 | elif CONF_LAMEID in conf: 529 | r = requests.get("https://developer.lametric.com/content/apps/icon_thumbs/" + conf[CONF_LAMEID], timeout=4.0) 530 | if r.status_code != requests.codes.ok: 531 | raise core.EsphomeError(f" ICONS: Could not download image file {conf[CONF_LAMEID]}: {conf[CONF_ID]}") 532 | image = Image.open(io.BytesIO(r.content)) 533 | elif CONF_URL in conf: 534 | r = requests.get(conf[CONF_URL], timeout=4.0) 535 | if r.status_code != requests.codes.ok: 536 | raise core.EsphomeError(f" ICONS: Could not download image file {conf[CONF_URL]}: {conf[CONF_ID]}") 537 | image = Image.open(io.BytesIO(r.content)) 538 | 539 | width, height = image.size 540 | 541 | if hasattr(image, 'n_frames'): 542 | frames = min(image.n_frames, MAXFRAMES) 543 | else: 544 | frames = 1 545 | 546 | if ((width != 4*ICONWIDTH) or (width != ICONWIDTH)) and (height != ICONHEIGHT): 547 | logging.warning(f" icon wrong size valid 8x8 or 8x32: {conf[CONF_ID]} skipped!") 548 | else: 549 | if (conf[CONF_FRAMEDURATION] == 0): 550 | try: 551 | duration = image.info['duration'] 552 | except: 553 | duration = config[CONF_FRAMEINTERVAL] 554 | else: 555 | duration = conf[CONF_FRAMEDURATION] 556 | 557 | html_string += F"
{conf[CONF_ID]} - ({duration} ms):
" 558 | 559 | pos = 0 560 | frameIndex = 0 561 | html_string += f"
" 562 | data = [0 for _ in range(ICONBUFFERSIZE * 2 * frames)] 563 | for frameIndex in range(frames): 564 | 565 | image.seek(frameIndex) 566 | frame = image.convert("RGB") 567 | pixels = list(frame.getdata()) 568 | width, height = image.size 569 | if width == 8: 570 | html_string += SVG_ICONSTART 571 | else: 572 | html_string += SVG_FULLSCREENSTART 573 | i = 0 574 | for pix in pixels: 575 | R = pix[0] >> 3 576 | G = pix[1] >> 2 577 | B = pix[2] >> 3 578 | x = (i % width) 579 | y = i//width 580 | i +=1 581 | rgb = (R << 11) | (G << 5) | B 582 | html_string += rgb565_svg(x,y,R,G,B) 583 | data[pos] = rgb >> 8 584 | pos += 1 585 | data[pos] = rgb & 255 586 | pos += 1 587 | html_string += SVG_END 588 | html_string += f"
" 589 | 590 | rhs = [HexInt(x) for x in data] 591 | 592 | prog_arr = cg.progmem_array(conf[CONF_RAW_DATA_ID], rhs) 593 | 594 | cg.new_Pvariable( 595 | conf[CONF_ID], 596 | prog_arr, 597 | width, 598 | height, 599 | frames, 600 | espImage.IMAGE_TYPE["RGB565"], 601 | str(conf[CONF_ID]), 602 | conf[CONF_PINGPONG], 603 | duration, 604 | ) 605 | 606 | cg.add(var.add_icon(RawExpression(str(conf[CONF_ID])))) 607 | 608 | html_string += "" 609 | 610 | if config[CONF_HTML]: 611 | try: 612 | htmlfn = CORE.config_path.replace(".yaml","") + ".html" 613 | with open(htmlfn, 'w') as f: 614 | f.truncate() 615 | f.write(html_string) 616 | f.close() 617 | logging.info(f"EsphoMaTrix: wrote html-file with icon preview: {htmlfn}") 618 | 619 | except: 620 | logging.warning(f"EsphoMaTrix: Error writing HTML file: {htmlfn}") 621 | 622 | disp = await cg.get_variable(config[CONF_MATRIXCOMPONENT]) 623 | cg.add(var.set_display(disp)) 624 | 625 | f = await cg.get_variable(config[CONF_FONT_ID]) 626 | cg.add(var.set_font(f)) 627 | 628 | ehmtxtime = await cg.get_variable(config[CONF_TIMECOMPONENT]) 629 | cg.add(var.set_clock(ehmtxtime)) 630 | 631 | cg.add(var.set_clock_time(config[CONF_CLOCKTIME])) 632 | cg.add(var.set_clock_interval(config[CONF_CLOCKINTERVAL])) 633 | cg.add(var.set_brightness(config[CONF_BRIGHTNESS])) 634 | cg.add(var.set_screen_time(config[CONF_SCREENTIME])) 635 | cg.add(var.set_scroll_interval(config[CONF_SCROLLINTERVAL])) 636 | cg.add(var.set_scroll_count(config[CONF_SCROLLCOUNT])) 637 | cg.add(var.set_frame_interval(config[CONF_FRAMEINTERVAL])) 638 | cg.add(var.set_week_start(config[CONF_WEEK_START_MONDAY])) 639 | cg.add(var.set_time_format(config[CONF_TIME_FORMAT])) 640 | cg.add(var.set_date_format(config[CONF_DATE_FORMAT])) 641 | cg.add(var.set_show_day_of_week(config[CONF_SHOWDOW])) 642 | cg.add(var.set_hold_time(config[CONF_HOLD_TIME])) 643 | cg.add(var.set_show_date(config[CONF_SHOWDATE])) 644 | cg.add(var.set_show_seconds(config[CONF_SHOW_SECONDS])) 645 | cg.add(var.set_font_offset(config[CONF_XOFFSET], config[CONF_YOFFSET])) 646 | 647 | for conf in config.get(CONF_ON_NEXT_SCREEN, []): 648 | trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) 649 | await automation.build_automation(trigger, [(cg.std_string, "x"), (cg.std_string, "y")], conf) 650 | 651 | for conf in config.get(CONF_ON_NEXT_CLOCK, []): 652 | trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) 653 | await automation.build_automation(trigger, [], conf) 654 | 655 | await cg.register_component(var, config) 656 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Important news 4 | 5 | **Take a look at the new version of the improved [EspHoMaTriXv2](https://github.com/lubeda/EspHoMaTriXv2/)!!!** 6 | 7 | This version is read-only!!!! 8 | 9 | # EspHoMaTriX (ehmtx) 10 | 11 | 12 | ### Breaking changes in esphome 2023.7.0 13 | 14 | Since the update ehmtx only compiles if you add some code to your yaml. See here: [https://github.com/lubeda/EspHoMaTriXv2/issues/62#issuecomment-1643052894](https://github.com/lubeda/EspHoMaTriXv2/issues/62#issuecomment-1643052894) 15 | 16 | ## Important information 17 | 18 | If you like my work, please donate me a star on GitHub and consider [sponsoring](https://www.paypal.com/donate/?hosted_button_id=FZDKSLQ46HJTU) me!! 19 | 20 | ## Introduction 21 | 22 | A simple DIY status display, built with a flexible 8x32 RGB LED panel implemented with [esphome.io](https://esphome.io) 23 | ![sample image](./images/sample.png) 24 | 25 | There are some “RGB-matrix” status displays/clocks out there, the commercial one from LaMetric and some excellent DIY-alternatives. 26 | 27 | - [LaMetric](https://lametric.com/en-US/) commercial ~ €199 28 | - [Ulanzi TC001](https://www.aliexpress.com/item/1005005008682055.html) commercial ~ €50 29 | - [Awtrix](https://awtrixdocs.blueforcer.de/#/) (project has been discontinued after more than 4 years now in August 2022) 30 | - [PixelIt](https://github.com/pixelit-project/PixelIt) (project is under active development) 31 | - [Awtrix-Light](https://github.com/Blueforcer/awtrix-light) From the developer of Awtrix, optimized for the Ulanzi TC001 Hardware 32 | The other DIY  solutions have their pros and cons. I tried some and used AwTrix for a long time. But the cons are so big (in my opinion) that I started an esphome.io variant targeted to an optimized Home Assistant integration. The main reason, for me, is the Home Assistant integration! 33 | There is a little hype around the Ulanzi TC001 pixel clock. This hardware can be used with **EspHoMaTriX** (with some limitations). You can connect the device and flash it via USB-C. As a starting point, you can use the [``UlanziTC001.yaml``](https://github.com/lubeda/EsphoMaTrix/blob/main/UlanziTC001.yaml). Yet, the LDR and battery sensor are not perfectly supported. For another use of the hardware, see [PixelIT_Ulanzi](https://github.com/aptonline/PixelIt_Ulanzi) or [AWTRIX-LIGHT](https://github.com/Blueforcer/awtrix-light) firmware. 34 | 35 | ### ehmtx in the media 36 | See this German tutorial video with information on setting up your display [RGB-LED Status Display für Home Assistant mit ESPHome | ESPHoMaTrix](https://www.youtube.com/watch?v=DTd9vAhet9A). 37 | Another german tutorial video focused at the Ulanzi [Smarte Pixel Clock über Home Assistant steuern - Entitäten / Icons und mehr in der Ulanzi](https://www.youtube.com/watch?v=LgaT0mNbl34) 38 | See this [nice article](https://blakadder.com/esphome-pixel-clock/) about EsphoMaTrix on an Ulanzi TC001 from [blakadder](https://github.com/blakadder). 39 | Short video on Instagram [@blak_adder](https://www.instagram.com/reel/CpYVByRIaSI) 40 | https://www.instagram.com/reel/CpYVByRIaSI 41 | See these English discussions: 42 | [Share your projects](https://community.home-assistant.io/t/esphomatrix-a-simple-clock-status-display/425325) 43 | [ESPHOME](https://community.home-assistant.io/t/a-simple-diy-status-display-with-an-8x32-rgb-led/379051) 44 | Or in German: 45 | [Showroom](https://community.simon42.com/t/8x32-pixel-uhr-mit-homeassistant-anbindung/1076) 46 | ### State 47 | It is a working solution with core functionality coded. Advanced features, like automatic brightness control, can be done with esphome actions and automations. The possibilities are endless. 48 | ### Features 49 | Based on a 8x32 RGB flexible matrix, it displays a clock, the date and up to 24 other 'screens' provided by Home Assistant. Each screen (value/text) can be associated with a 8x8 bit RGB icon or GIF animation (see [installation](#installation)). The values/text can be updated or deleted from the display queue. Each screen has a lifetime, if not refreshed in its lifetime, it will disappear. When a screen is active, it is displayed so that the text can scroll twice (`scroll_count`) or even longer for `screen_time` seconds. 50 | You can use the [fullfeature.yaml](https://github.com/lubeda/EsphoMaTrix/blob/main/fullfeature.yaml) as a sample for an ESP8266. As mentioned, you have to edit to your needs. So check font, icons, board and the GPIO pins for your display. 51 | The file [ulanziTC001.yaml](https://github.com/lubeda/EsphoMaTrix/blob/main/ulanziTC001.yaml) uses the functions ehmtx provides, optimized for the Ulanzi Hardware. 52 | See it in action on [YouTube](https://www.youtube.com/watch?v=ZyaFj7ArIdY) (no sound but subtitles). 53 | ## Installation 54 | ### **EsphoMaTrix** custom component 55 | **EsphoMaTrix** is a custom component, you have to include it in your YAML configuration. To always use the newest features, you should use the repo, to use a stable version you copy a working version to your esphome installation. 56 | #### local use 57 | If you download the components-folder from the repo and install it in your esphome you have a stable installation. But if there are new features, you won't see them. If needed, customize the YAML to your folder structure. 58 | ```yaml 59 | external_components: 60 |    - source: 61 |        type: local 62 |        path: components # e.g. /config/esphome/components 63 | ``` 64 | #### use from repo 65 | Use the GitHub repo as a component. Esphome refreshes the external components “only” once a day, perhaps you have to refresh it manually. In this mode, there may be breaking changes, so read the changelog and check the logs while installing the firmware. 66 | ```yaml 67 | external_components: 68 |   - source: 69 |       type: git 70 |       url: https://github.com/lubeda/EsphoMaTrix 71 |       ref: main # optional select a special branch or tag 72 | ``` 73 | ### Addressable_light component 74 | The **EsphoMaTrix** component requires a 8x32 pixel addressable_light, it is referenced by the ID `matrix_component`. 75 | See the default [options](https://esphome.io/components/display/index.html) 76 | There are some different matrices-types on the market, to adapt them to **EspHoMaTriX** you have to find the proper pixel mapper. If there is garbage on your display, try the other `pixel_mapper`. Here are the most common types for flexible 8x32 matrices: 77 | #### Type 1 78 | under the display tag, specify this pixel mapper: 79 | ```yaml 80 | display: 81 |   - platform: addressable_light 82 |     ..... 83 |     pixel_mapper: |- 84 |       if (x % 2 == 0) { 85 |         return (x * 8) + y; 86 |       } 87 |       return (x * 8) + (7 - y); 88 |     ..... 89 | ``` 90 | #### Type 2 (e.g., Ulanzi TC001) 91 | Under the display tag, specify this pixel mapper: 92 | ```yaml 93 | display: 94 |   - platform: addressable_light 95 |     ..... 96 |     pixel_mapper: |- 97 |       if (y % 2 == 0) { 98 |         return (y * 32) + x; 99 |       } 100 |       return (y * 32) + (31 - x); 101 |     ..... 102 | ``` 103 | You have to configure this `lambda` under the `display:` section to use the **EsphoMaTrix** component 104 | ```yaml 105 | display: 106 |   - platform: addressable_light 107 |     id: ehmtx_display 108 |     ..... 109 |     auto_clear_enabled: true 110 |     lambda: |- 111 |       id(rgb8x32)->tick(); 112 |       id(rgb8x32)->draw(); 113 | ``` 114 | ### Light component 115 | The light component is used by the addressable_light component and referenced by ID under `addressable_light_id:`. 116 | To use the light component directly from home assistant, add the sample lambdas```on_turn_on``` and ```on_turn_off``` to the light component. 117 | 118 | ***Sample*** 119 | 120 | ```yaml 121 | light: 122 |   - platform: neopixelbus 123 |     id: ehmtx_light 124 |     .... 125 |     on_turn_on: 126 |       lambda: |- 127 |          id(rgb8x32)->set_enabled(false); 128 |     on_turn_off: 129 |        lambda: |- 130 |          id(rgb8x32)->set_enabled(true); 131 | ``` 132 | To hide the light component from home assistant use: ` internal: true` 133 | 134 | ```yaml 135 | 136 | light: 137 |   - platform: neopixelbus 138 |     id: ehmtx_light 139 |     internal: true 140 |     ... 141 | ``` 142 | 143 | ### Time component 144 | 145 | Since it is a clock, you need a time component, e.g., [homeassistant](https://esphome.io/components/time/homeassistant.html). It is referenced by its ID under `time_component:` The display shows `!t!` until the time source is synchronized and valid. 146 | 147 | ### Font 148 | 149 | Download a small “pixel” TTF-font, I use ["monobit.ttf"](https://www.google.com/search?q=monobit.ttf). You can modify this font with [FontForge](https://fontforge.org/) and add **€** on base of a **E** and so on. Due to copyright, I can't provide my modified version :-(. Not all fonts are suitable for this minimalistic display. There are public domain fonts which work well on the display, e.g., [DMDSmall](https://www.pentacom.jp/pentacom/bitfontmaker2/gallery/?id=5993), details on alternative fonts are [here](https://blakadder.com/esphome-pixel-clock/#fonts). 150 | DarkPoet78 is providing special fonts for 8x32 matrices in his [repo](https://github.com/darkpoet78/MatrixClockFonts) 151 | 152 | ```yaml 153 | font: 154 |   - file: monobit.ttf 155 |     id: EHMTX_font 156 |     size: 16 157 |     glyphs:  | 158 |       !"%()+*=,-_.:°0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz€@ 159 | ``` 160 | 161 | ### Icons and Animations 162 | Download and install all needed icons (.jpg/.png) and animations (.gif) under the `ehmtx:` key. All icons have to be 8x8 or 8x32 pixels in size. If necessary, scale them with gimp, check “as animation” for GIFs. 163 | You can also specify a URL to directly download the image file. The URLs will only be downloaded once at compile time, so there is no additional traffic on the hosting website. 164 | There are maximum 90 icons possible. 165 | 166 | ***Sample*** 167 | 168 | ```yaml 169 | emhtx: 170 |   icons: 171 |     - id: boot 172 |       file: icons/rocket.gif 173 |       duration: 75     174 |     - id: temp 175 |       file: temperature.png 176 |     - id: yoga 177 |       file: icons/yoga-bridge.gif 178 |       pingpong: true 179 |     - id: garage 180 |       file: garage.gif 181 |       duration: 100 182 |     - id: homeassistant 183 |       url: https://github.com/home-assistant/assets/raw/master/logo/logo-small.png     184 | ``` 185 | The ID of the icons is used later to configure the screens to display. So, you should name them wisely. If you like to group icons, you should prefix them e.g., with "weather_" (see Service **del_screen**) 186 | The first defined icon will be used as a fallback icon, in case of an error, e.g., if you use a non-existing icon ID. 187 | GIFs are limited to 110 frames to limit the used amount of flash space. 188 | All other solutions provide ready-made icons, especially Lametric has a big database of [icons](https://developer.lametric.com/icons). Please check the copyright of the icons you use. The maximum number of icons is limited to 90 in the code and also by the flash space and the RAM of your board. 189 | See also [icon parameter](#icons) 190 | ## Configuration 191 | ### ehmtx component 192 | This component is highly customizable. 193 | ***Example*** 194 | ```yaml 195 | ehmtx: 196 |   id: rgb8x32 197 |   time_component: ehmtx_time 198 |   matrix_component: ehmtx_display 199 |   clock_time: 5    # seconds 200 |   screen_time: 8   # seconds 201 |   font_id: ehmtx_font 202 |   show_dow: true   # day of week 203 |   show_date: true  # also show the date 204 |   icons2html: false # generate html with con overview 205 |   brightness: 80 # percent 206 |   time_format: "%H:%M" 207 |   date_format: "%d.%m." 208 |   week_start_monday: true # false equals sunday 209 |   xoffset: 1 210 |   yoffset: 2 211 |   scroll_count: 2 # scroll long text at least two times 212 |   scroll_interval: 80 # milliseconds 213 |   frame_interval: 192 # milliseconds 214 | ``` 215 | 216 | ***Parameters*** 217 | 218 | **id** (required, ID): Manually specify the ID used for code generation and in service definitions. 219 | **clock_interval** (optional, seconds): show the clock at least each x seconds, (default=60) 220 | **clock_time** (optional, seconds): duration to display the clock after this time, the date is displayed until next "show_screen". If `show_date` is false and `clock_time` > 0 the clock will be display as long as a normal screen! Setting `clock_time` to 0 will not show the clock or date. If there are no screens ind the queue, the display will be blank until the next screen is sent. 221 | ![timing](./images/timing.png) 222 | **screen_time** (optional, seconds): default duration to display a screen or a clock/date sequence, a long text will be scrolled at least `scroll_count` times (default: 10 seconds). This may be overwritten by the add_screen service. 223 | **hold_time** (optional, seconds): extends the display time of the current screen in seconds (default=20). Used in services or automations, see `hold_screen` 224 | **date_format** (optional, string): formats the date display with [strftime syntax](https://esphome.io/components/time.html?highlight=strftime), defaults `"%d.%m."` (use `"%m.%d."` for the US) 225 | **show_seconds** (optional, boolean): toggle an indicator for seconds while the clock is displayed (default: false)) 226 | **time_format** (optional, string): formats the date display with [strftime syntax](https://esphome.io/components/time.html?highlight=strftime), defaults `"%H:%M"` (use `"%I:%M%p"` for the US) 227 | **yoffset** (optional, pixel): yoffset the text is aligned BASELINE_LEFT, the baseline defaults to `6` 228 | **xoffset** (optional, pixel): xoffset the text is aligned BASELINE_LEFT, the left defaults to `1` 229 | **matrix_component** (required, ID): ID of the addressable display 230 | **show_dow** (optional, bool): draw the day of week indicator on the bottom of the clock screen. Disable, e.g., if you want larger fonts, defaults to true. 231 | **show_date** (optional, bool): if true, show the date for `screen_time - clock_time` seconds, otherwise only shows the clock for `screen_time` seconds, defaults to true. 232 | **time_component** (required, ID): ID of the time component. The display shows `!t!` until the time source is valid. 233 | **font** (required, ID): ID of the font component 234 | **week_start_monday** (optional, bool): default Monday is first day of week, false => Sunday 235 | **scroll_interval** (optional, ms): the interval in ms to scroll the text (default=80), should be a multiple of the ```update_interval``` of the [display](https://esphome.io/components/display/addressable_light.html) 236 | **frame_interval** (optional, ms): the interval in ms to display the next animation/icon frame (default = 192), should be a multiple of the ```update_interval``` of the [display](https://esphome.io/components/display/addressable_light.html). It can be overwritten per icon/gif, see [icons](#icons-and-animations) parameter `frame_duration` 237 | **icons2html** (optional, boolean): If true, generate the HTML (_filename_.html) file to show all included icons.  (default = `false`) 238 | ***Example output:*** 239 | ![icon preview](./images/icons_preview.png) 240 | ### icons 241 | ***Parameters*** 242 | See [icon details](#icons-and-animations) 243 | - **frame_duration** (optional, ms): in the case of a GIF file, the component tries to read the default interval for each frame. The default/fallback interval is 192 ms. In case you need to override the default value, set the duration per icon. 244 | - **pingpong** (optional, boolean): in the case of a GIF file, you can reverse the frames instead of starting from the first frame. 245 | - **file** (Exclusive, filename): a local filename 246 | - **url** (Exclusive, url): a URL to download the icon 247 | - **lameid** (Exclusive, number): the ID from the LaMetric icon database 248 | 249 | ### Compile errors `animation.h` is missing 250 | ```cpp 251 | Error: 252 | In file included from src/esphome.h:25, 253 | from src/esphome/components/ehmtx/EHMTX.cpp:1: 254 | src/esphome/components/ehmtx/EHMTX.h:6:10: fatal error: esphome/components/animation/animation.h: No such file or directory 255 | #include "esphome/components/animation/animation.h" 256 | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 257 | compilation terminated. 258 | ... 259 | ``` 260 | 261 | #### Solution 262 | Add in your given YAML this Code at the end: 263 | ```yaml 264 | animation: 265 | - id: animation_black 266 | file: "black.gif" 267 | 268 | image: 269 | - id: image_black 270 | file: "black.gif" 271 | ``` 272 | 273 | and copy furthermore the black.gif Picture to your esphome Folder (most under **/config/esphome/**) 274 | You will find this picture in the image Folder in this git. 275 | 276 | --- 277 | 278 | ## Control your display 279 | Plenty of the features are accessible with actions, you can use in your YAML 280 | 281 | ### Local actions/lambdas 282 | 283 | #### Show all icons on your matrix 284 | 285 | This code shows all icons once on boot up, depending on the amount of your icons it can take a while to see them all. 286 | 287 | ```yaml 288 | esphome: 289 |   .... 290 |   on_boot: 291 |     priority: -100 292 |     # ... 293 |     then: 294 |       - lambda: !lambda |- 295 |           id(rgb8x32)->show_all_icons(); 296 | ``` 297 | --- 298 | 299 | #### Add screen to your display queue 300 | You can add screens locally and display data directly from any local sensor. See this sample: 301 | 302 | ##### Lambda 303 | Take care that the ```char text[30];``` has enough space to store the formatted text. 304 | 305 | ```yaml 306 | sensor: 307 |   - platform: bh1750 308 |     id: sensorlx 309 |     ... 310 |     on_value: 311 |       then: 312 |         lambda: |- 313 |           char text[30]; 314 |           sprintf(text,"Light: %2.1f lx", id(sensorlx).state); 315 |           // 5 Minutes,each time at least 11 seconds,no alarm 316 |            id(rgb8x32)->add_screen("sun", text, 5,11, false); 317 | ``` 318 | 319 | ##### Action 320 | 321 | ```yaml 322 | sensor: 323 |   - platform: bh1750 324 |     id: sensorlx 325 |     ... 326 |     on_value: 327 |       then: 328 |         - ehtmx.add.screen 329 |           id: rgb8x32 330 |           icon_name: "sun" 331 |           text: "new data from lux sensor" 332 |           lifetime: 5 # minutes optional 333 |           screen_time: 35 # seconds optional 334 |           alarm: true # optional 335 | ``` 336 | 337 | ***Parameters*** 338 | 339 | - **id** (required, ID): ID of the ehmtx component 340 | - **text** (required, string): the text to display 341 | - **icon_name** (required, string): the name of the icon to display 342 | - **lifetime** (optional, int): the lifetime of the screen in minutes (default=5) 343 | - **screen_time** (optional, int): the display time of a screen per loop in seconds (default=10) 344 | - **alarm** (optional, bool): if alarm set true (default = false) 345 | 346 | --- 347 | 348 | #### Set (alarm/clock/gauge/text/today/weekday) color action 349 | Sets the color of the selected element 350 | 351 | ##### Lambda set text color 352 | 353 | ```yaml 354 |   lamda: 355 |     id(rgb8x32)->set_text_color(200,45,12); 356 | ``` 357 | ##### Action for set text color 358 | You have to use the ID of your ehmtx component, e.g., `rgb8x32` 359 | 360 | ```yaml 361 |      - ehmtx.***.color: 362 |         id: rgb8x32 363 |         red: !lambda return r; 364 |         green: !lambda return g; 365 |         blue: !lambda return b; 366 | ``` 367 | 368 | **valid elements:** 369 | 370 | - `ehmtx.alarm.color:` 371 | - `ehmtx.clock.color:` 372 | - `ehmtx.gauge.color:` 373 | - `ehmtx.text.color:` 374 | - `ehmtx.today.color:` 375 | - `ehmtx.weekday.color:` 376 | - ```red, green, blue```: the color components (`0..255`) *(default = `80`)* 377 | --- 378 | #### Set screen color action 379 | Sets the color of the **active** screen in the queue, so it's best to use it directly after add_screen. 380 | ##### Lambda for set_screen 381 | ```yaml 382 |   lamda: 383 |     id(rgb8x32)->set_screen_color("sun",200,45,12); 384 | ``` 385 |   386 | ##### Action for set_screen 387 |   388 | You have to use the ID of your ehmtx component, e.g., `rgb8x32` 389 |   390 | ```yaml 391 |      - ehmtx.screen.color: 392 |         id: rgb8x32 393 |         icon_name: sun 394 |         red: !lambda return r; 395 |         green: !lambda return g; 396 |         blue: !lambda return b; 397 | ``` 398 |   399 | - ```icon_name```: name of the actual icon/screen 400 | - ```red, green, blue```: the color components (`0..255`) _(default = `80`)_ 401 |   402 | --- 403 |   404 | ##### Show date 405 |   406 | You can dynamically enable or disable the display of the date, see parameter `show_date`. 407 |   408 | ```yaml 409 |     - ehmtx.show.date: 410 |         id: rgb8x32 411 |         flag: !lambda return true; 412 | ``` 413 |   414 | ##### Show day of week 415 |   416 | You can dynamically enable or disable the display of the day of week, see parameter `day_of_week`. 417 |   418 | ```yaml 419 |     - ehmtx.show.dayofweek: 420 |         id: rgb8x32 421 |         flag: !lambda return true; 422 | ``` 423 |   424 | ##### Force screen 425 |   426 | Force the selected screen ```icon_name``` to be displayed next. Afterward, the loop continues from this screen. e.g., helpful for alarms. Or after an update of the value/text. 427 |   428 | ```yaml 429 |     - ehmtx.force.screen: 430 |         id: rgb8x32 431 |         icon_name: !lambda return icon_name; 432 | ``` 433 |   434 | ##### Change configuration during runtime 435 |   436 | _Configuration variables/functions:_ 437 |   438 | Experienced programmers can use these public methods: 439 |   440 | ```c 441 |     void draw_day_of_week(); 442 |     void show_all_icons(); 443 |     void get_status(); 444 |     void skip_screen(); 445 |     void hold_screen(); 446 |     void set_screen_time(uint16_t t); 447 |     void set_clock_time(uint16_t t); 448 |     void set_hold_time(uint16_t t); 449 |     void set_clock_interval(uint16_t t); 450 |     void set_show_day_of_week(bool b); 451 |     void set_show_seconds(bool b); 452 |     void set_show_date(bool b); 453 |     void set_brightness(int b); // int because of register_service! 454 |     uint8_t get_brightness(); 455 |     void add_screen(std::string icon_name, std::string text, int lifetime, int show_time, bool alarm); 456 |     void set_screen_color(std::string icon_name,int r, int g, int b); 457 |     void del_screen(std::string icon_name); 458 |     void set_frame_interval(uint16_t interval); 459 |     void set_scroll_interval(uint16_t interval); 460 |     void set_scroll_count(uint8_t count); 461 |     void set_duration(uint8_t d); 462 |     void set_indicator_off(); 463 |     void set_indicator_on(int r, int g, int b); 464 |     void set_gauge_off(); 465 |     void set_gauge_value(int v); // valid: 0 - 100 int because of register_service 466 |     void set_gauge_color(int r, int g, int b); 467 |     void set_text_color(int r, int g, int b); 468 |     void set_clock_color(int r, int g, int b); 469 |     void set_today_color(int r, int g, int b); 470 |     void set_weekday_color(int r, int g, int b); 471 |     void set_alarm_color(int r, int g, int b); 472 |     void draw_clock(); 473 |     void draw_gauge(); 474 |     void set_display_on(); 475 |     void set_display_off(); 476 | ``` 477 |   478 | ***Sample*** 479 |   480 | You can set values during runtime, e.g., for a night mode 481 |   482 | ```yaml 483 | # sample for ulanzi tc001 484 | binary_sensor: 485 |   - platform: gpio 486 |     pin: 487 |       number: $left_button_pin 488 |       inverted: true 489 |     on_press: 490 |       - logger.log: "Clock on" 491 |       - lambda: 492 |           id(rgb8x32)->set_clock_time(6); 493 |     name: "clock on" 494 |   - platform: gpio 495 |     pin: 496 |       number: $right_button_pin 497 |       inverted: true 498 |    name: "Clock off" 499 |     on_press: 500 |       - logger.log: "clock off" 501 |       - lambda: 502 |           id(rgb8x32)->set_clock_time(0); 503 | ``` 504 |   505 | ### Local trigger 506 |   507 | To use the display without home assistant automations, you may use the [advanced functionality](#change-configuration-during-runtime) with triggers. The triggers can be fired by sensors, time or by the ehmtx component. 508 |   509 | #### on_next_screen 510 | There is a trigger available to do some local magic. The trigger ```on_next_screen``` is triggered every time a new screen is displayed (it doesn't trigger on the clock/date display!!). In lambda's you can use two local string variables: 511 |   512 | **x** (Name of the icon, std::string): value to use in lambda 513 |   514 | **y** (displayed text, std::string): value to use in lambda 515 |   516 | See the examples: 517 |   518 | ##### Write information to esphome log 519 |   520 | ```yaml 521 | ehmtx: 522 |   .... 523 |   on_next_screen: 524 |     lambda: |- 525 |         ESP_LOGD("TriggerTest","Iconname: %s",x.c_str()); 526 |         ESP_LOGI("TriggerTest","Text: %s",y.c_str()); 527 | ``` 528 |   529 | ##### Change the text color like crazy 530 |   531 | ```yaml 532 | ehmtx: 533 |   .... 534 |   on_next_screen: 535 |     lambda: |- 536 |       id(rgb8x32)->set_text_color(rand() % 255, rand() % 255, rand() % 255); 537 | ``` 538 |   539 | ##### Send an event to Home Assistant 540 |   541 | To send data back to home assistant, you can use events. 542 |   543 | ```yaml 544 | ehmtx: 545 |   .... 546 |   on_next_screen: 547 |     - homeassistant.event: 548 |         event: esphome.next_screen 549 |         data_template: 550 |           iconname: !lambda "return x.c_str();" 551 |           text: !lambda "return y.c_str();" 552 | ``` 553 |   554 | ***Result*** 555 |   556 | ![events](./images/events.png) 557 |   558 | #### on_next_clock 559 | The trigger ```on_next_clock``` is triggered every time a new clock display circle starts. 560 | See the examples: 561 |   562 | ##### Change Clock colors like crazy for each clock circle 563 |   564 | ```yaml 565 | ehmtx: 566 |   .... 567 |   on_next_clock: 568 |     lambda: |- 569 |       id(rgb8x32)->set_clock_color(rand() % 255, rand() % 255, rand() % 255); 570 |       id(rgb8x32)->set_weekday_color(rand() % 255, rand() % 255, rand() % 255); 571 |       id(rgb8x32)->set_today_color(rand() % 255, rand() % 255, rand() % 255); 572 | ``` 573 |   574 | ***Example*** 575 |   576 | ```yaml 577 | api: 578 |   services: 579 |     - service: alarm 580 |       variables: 581 |         icon_name: string 582 |         text: string 583 |       then: 584 |         lambda: |- 585 |           id(rgb8x32)->add_screen(icon_name, text, 7,20, true); // 7 minutes lifetime/screen time 20 sec/alarm=true 586 | ``` 587 |   588 | **(D)** Service **brightness** 589 |   590 | Sets the overall brightness of the display (`0..255`) 591 |   592 | _parameters:_ 593 |   594 | - ```brightness```: from dark to bright (`0..255`) (default = `80`) as set in the light component by ```color_correct: [30%, 30%, 30%]``` 595 |   596 | There's an easier way, by using a number component: 597 |   598 | ```yaml 599 | number: 600 |   - platform: template 601 |     name: "LED brightness" 602 |     min_value: 0 603 |     max_value: 255 604 |     step: 1 605 |     lambda: |- 606 |       return id(rgb8x32)->get_brightness(); 607 |     set_action: 608 |       lambda: |- 609 |         id(rgb8x32)->set_brightness(x); 610 | ``` 611 |   612 | Service **screen** (defined in YAML) 613 |   614 | Queues a screen with an icon/animation and a text. There can only be one text per icon ID. If you need to show, e.g., an indoor and an outdoor temperature, you have to use different icon ID's! 615 |   616 | You can update the text on the fly. If the screen is displayed, and you change the text for the icon, it will start a new lifetime (see ```lifetime```) with the new text. 617 |   618 | _parameters:_ 619 |   620 | - ```icon_name```: The number of the predefined icons (see installation) 621 | - ```text```: The text to be displayed 622 |   623 | _definition:_ 624 | ```yaml 625 | api: 626 |   services: 627 |     - service: screen 628 |       variables: 629 |         icon_name: string 630 |         text: string 631 |       then: 632 |         - lambda: |- 633 |             id(rgb8x32)->add_screen(icon_name,text,5,10,false); 634 | ``` 635 |   636 | Service **alarm** (defined in YAML) 637 |   638 | The alarm is like a regular screen, but it is displayed two minutes longer and has a red text color and a red marker in the upper-right corner. 639 |   640 | _parameters:_ 641 |   642 | - ```icon_name```: The name of the predefined icon ID (see installation) 643 | - ```text```: The text to be displayed 644 |   645 | _definition:_ 646 | ```yaml 647 | api: 648 |   services: 649 |     - service: alarm 650 |       variables: 651 |         icon_name: string 652 |         text: string 653 |       then: 654 |         - lambda: |- 655 |             id(rgb8x32)->add_screen(icon_name,text,10,30,true); 656 |             id(rgb8x32)->force_screen(icon_name); 657 | ``` 658 |   659 | **(D)** Service **del_screen** 660 |   661 | Removes a screen from the display by icon name. If this screen is actually displayed while sending this command, the screen will be displayed until its "show_screen"-time has ended. 662 |   663 | optionally, you can suffix a "*" to the icon name to perform a wildcard delete, which will delete all screens beginning with the icon_name specified. 664 |   665 | For example, if you have multiple icons named weather_sunny, weather_rain & weather_cloudy, you can issue a del_screen weather_* to remove whichever screen is currently in a slot and replace it with a new weather screen. 666 |   667 | _parameters:_ 668 |   669 | - ```icon_name```: Icon `id` defined in the YAML (see installation) 670 |   671 | **(D)** Service **indicator_on** / **indicator_off** 672 |   673 | Turns indicator on/off 674 |   675 | Display a colored corner on all screens and the clock. You can define the color by parameter. 676 |   677 | _parameters:_ 678 |   679 | - ```r``` red in 0..255 680 | - ```g``` green in 0..255 681 | - ```b``` blue in 0..255 682 |   683 | **(D)** Service **alarm_color** / **clock_color** / **gauge_color** / **text_color** / **today_color** / **weekday_color** 684 |   685 | Set the color of the named text-type 686 |   687 | _parameters:_ 688 |   689 | - ```r``` red in 0..255 690 | - ```g``` green in 0..255 691 | - ```b``` blue in 0..255 692 |   693 | **(D)** Service **display_on** / **display_off** 694 |   695 | Turns the display on or off 696 |   697 | There's an easier way in using a switch component: 698 |   699 | ***Sample*** 700 |   701 | ```yaml 702 | switch: 703 |   - platform: template 704 |     name: "$devicename Display" 705 |     icon: "mdi:power" 706 |     restore_mode: ALWAYS_ON 707 |     lambda: |- 708 |       return id(rgb8x32)->show_display; 709 |     turn_on_action: 710 |       lambda: |- 711 |         id(rgb8x32)->set_display_on(); 712 |     turn_off_action: 713 |       lambda: |- 714 |         id(rgb8x32)->set_display_off(); 715 | ``` 716 |   717 | Service **skip_screen** 718 |   719 | If there is more than one screen in the queue, skip to the next screen. 720 |   721 | e.g., on the Ulanzi TC001 722 |   723 | ```yaml 724 | binary_sensor: 725 |   - platform: gpio 726 |     pin: 727 |       number: $left_button_pin 728 |       inverted: true 729 |     on_press: 730 |       lambda: 731 |         id(rgb8x32)->skip_screen(); 732 | ``` 733 |   734 | Service **hold_screen** 735 |   736 | Displays the current screen for a configured amount (see **hold_time**) (default=20) seconds longer. 737 |   738 | e.g., on the Ulanzi TC001 739 |   740 | ``` 741 | binary_sensor: 742 |   - platform: gpio 743 |     pin: 744 |       number: $right_button_pin 745 |       inverted: true 746 |     on_press: 747 |       lambda: 748 |         id(rgb8x32)->hold_screen(); 749 | ``` 750 |   751 | **(D)** Service **status** 752 |   753 | This service displays the running queue and a list of icons in the logs 754 |   755 | ```log 756 | [13:10:10][I][EHMTX:175]: status status: 1  as: 1 757 | [13:10:10][I][EHMTX:176]: status screen count: 3 758 | [13:10:10][I][EHMTX:181]: status slot: 0 icon: 36  text: 47.9°C end: 400 759 | [13:10:10][I][EHMTX:181]: status slot: 1 icon: 23  text: Supa langer Text end: 310 760 | [13:10:10][I][EHMTX:181]: status slot: 2 icon: 1  text: 10.3°C end: 363 761 | [13:10:10][I][EHMTX:186]: status icon: 0 name: boot 762 | [13:10:10][I][EHMTX:186]: status icon: 1 name: temp 763 | [13:10:10][I][EHMTX:186]: status icon: 2 name: garage 764 | [13:10:10][I][EHMTX:186]: status icon: 3 name: wind 765 | [13:10:10][I][EHMTX:186]: status icon: 4 name: rain 766 | ``` 767 |   768 | **(D)** Service **show_all_icons** 769 |   770 | Display all of your icons sequentially by ID. 771 |   772 | Service **gauge_value** / **gauge_off** 773 |   774 | **(D)** Turns gauge on/off 775 | Displays a colored gauge on the left side of the display. You can define the color by parameter. 776 |   777 | _parameters:_ 778 |   779 | - ```percent``` gauge percentage 780 |   781 | ## Integration in Home Assistant 782 |   783 | To control your display, it has to be integrated in Home Assistant. Then it provides several services, all prefixed with the configured `devicename` e.g., "ehmtx". See the default services marked as **(D)** [above](#services), but you can add your own (see alarm and screen). 784 |   785 | ### Services 786 |   787 | All communication with Home Assistant use the home asistant API. The services can be provided by default or also defined additionally in the YAML. To define the additional services, you need the ID of the ehmtx-component e.g. ```id(rgb8x32)```. 788 |   789 | #### Overview of default services 790 |   791 | These services are the same as the local services, so you can adapt the documentation there 792 |   793 |   |name|parameter| 794 |   |----|----| 795 |   |`get_status`|*none*| 796 |   |`set_display_on`|*none*| 797 |   |`set_display_off`|*none*| 798 |   |`show_all_icons`|*none*| 799 |   |`hold_screen`|*none*| 800 |   |`set_indicator_on`| {"r", "g", "b"}| 801 |   |`set_indicator_off`|*none*| 802 |   |`set_gauge_value`| {"percent"}| 803 |   |`set_gauge_off`|*none*| 804 |   |`set_alarm_color`| {"r", "g", "b"}| 805 |   |`set_text_color` | {"r", "g", "b"}| 806 |   |`set_clock_color`| {"r", "g", "b"}| 807 |   |`set_today_color`| {"r", "g", "b"}| 808 |   |`set_gauge_color`| {"r", "g", "b"}| 809 |   |`set_weekday_color` |{"r", "g", "b"}| 810 |   |`set_screen_color` |{"icon_name","r", "g", "b"}| 811 |   |`add_screen`  |{"icon_name", "text", "lifetime","screen_time", "alarm"}| 812 |   |`force_screen`| {"icon_name"}| 813 |   |`del_screen`| {"icon_name"}| 814 |   |`set_brightness`| {"value"}| 815 |   816 | ### Use in Home Assistant automations 817 |   818 | The easiest way to use ehmtx as a status display is to use the icon names as trigger ID. In my example, I have an icon named “wind” when the sensor.wind_speed has a new state, this automation sends the new data to the screen with the icon named “wind” and so on. 819 |   820 | ```yaml 821 | alias: EHMTX 8266 Test 822 | description: '' 823 | trigger: 824 |   - platform: numeric_state 825 |     entity_id: sensor.wind_speed 826 |     id: wind 827 |   - platform: state 828 |     entity_id: sensor.actual_temperature 829 |     id: temp 830 |   - platform: state 831 |     entity_id: sensor.wg_cover_device 832 |     id: cover 833 | condition: [] 834 | action: 835 |   - service: esphome.ehmtx8266_screen 836 |     data: 837 |       icon_name: '{{trigger.id}}' 838 |       text: >- 839 |         {{trigger.to_state.state}}{{trigger.to_state.attributes.unit_of_measurement}} 840 | mode: queued 841 | max: 10 842 | ``` 843 |   844 | ## How to update 845 |   846 | Since version **2023.4.0** was a massive cleanup, you may have to check your YAML and your automations for all breaking changes. 847 |   848 | ### repairs 849 |   850 | ![sample repair](./images/repair.png "Sample repair dialog. because of changed service definitions") 851 |   852 | mostly, you have to check your automations if the [service](#overview-of-default-services) definitions have changed. 853 |   854 | ![compile_error](./images/compile_error.png "Sample compile error. the naming has changed") 855 |   856 | |old name|new name| 857 | |----|----| 858 | |display8x32|matrix_component| 859 | |show_clock|clock_time| 860 | |show_screen|screen_time| 861 | |time|time_component| 862 | |html|icons2html| 863 |   864 | --- 865 |   866 | ![icon_error](./images/icon_error.png "changed icon parameter") 867 |   868 | |old name|new name| 869 | |----|----| 870 | |duration|frame_duration| 871 |   872 | --- 873 |   874 | Old functions in YAML 875 | ![old_add_screen](./images/old_add_screen.png "old functions in YAML file") 876 |   877 | **old function style** 878 |   879 | ```yaml 880 | api: 881 |   services: 882 |     - service: alarm 883 |       variables: 884 |         icon_name: string 885 |         text: string 886 |       then: 887 |         - lambda: |- 888 |             id(rgb8x32)->add_screen(icon_name,text,7,true); 889 |         - ehmtx.force.screen: 890 |             icon_name: !lambda return icon_name; 891 | ``` 892 | the old `add_screen` function had 4 parameters, the new one has got 5. 893 |   894 | **correct code** 895 |   896 | ```yaml 897 | # Enable Home Assistant API 898 | api: #!include ehmtx_service.yaml 899 |   services: 900 |     ..... 901 |       then: 902 |         - lambda: |- 903 |             id(rgb8x32)->add_screen(icon_name,text,7,30,true); 904 |     .....   905 | ``` 906 | ### old options 907 | ![duration and select](./images/duration_select.png "Duration and ehmtxselect were removed in this version") 908 |   909 | Remove these entries. 910 |   911 |   912 | #### Display precision after home assistant 2023.3.0 913 |   914 | See [templating](https://www.home-assistant.io/docs/configuration/templating/#states) for possibilities to optimize the output 915 | e.g. 916 | ```{{ states(sensor.solarpower, rounded=True) }} kWh``` 917 |   918 | ### Specific icons per condition 919 |   920 | Add an icon per weather condition to the ehmtx component 921 |   922 | ```yaml 923 |   - id: weather_clear_night 924 |       lameid: 52163 925 |     - id: weather_cloudy 926 |       lameid: 25991 927 |     - id: weather_fog 928 |       lameid: 52167 929 |     ...... 930 | ``` 931 |   932 | Sample automation to show the weather with local temperature 933 |   934 | ```yaml 935 | alias: EHMTX weather 936 | description: weather with icon per condition 937 | trigger: 938 |   - platform: state 939 |     entity_id: weather.metno 940 | action: 941 |   - service: esphome.ulanzi_del_screen 942 |     data: 943 |       icon_name: weather_* 944 |   - service: esphome.ulanzi_screen 945 |     data: 946 |       icon_name: weather_{{ trigger.to_state.state }} 947 |       text: >- 948 |         {{ states("sensor.external_actual_temperature") }}°C 949 | ``` 950 |   951 | or another sample automation for the trashcan type 952 |   953 | ```yaml 954 | alias: "EHMTX Müllanzeige" 955 | description: Anzeige welche Tonne raus muss. iconnamen gekürzt 956 | trigger: 957 |   - platform: time 958 |     at: 959 |       - "06:30" 960 |       - "08:30" 961 |       - "10:30" 962 |       - "15:00" 963 |       - "17:00" 964 |       - "19:00" 965 | condition: 966 |   - condition: numeric_state 967 |     entity_id: sensor.mulltrigger 968 |     below: "3" 969 | action: 970 |   - service: esphome.ulanzi_del_screen 971 |     data: 972 |       icon_name: trash_* 973 |   - data: 974 |       icon_name: >- 975 |         trash_{{ states("sensor.mulldetails") | replace("Biotonne",   "brow")| 976 |         replace("Papiertonne","blue")| replace("Restmüll",   "grey")| 977 |         replace("gelbe Tonne","yell|") | truncate(4,true,"")  }}     978 |       text: >- 979 |         {{ states("sensor.mulldetails")|replace(" in","")|replace(" days"," 980 |         Tagen") | replace ("0 Tagen","heute") | replace ("1 Tagen","morgen")}} 981 |       duration: 120 982 |     service: esphome.ulanzi_screen 983 | mode: single 984 | ``` 985 |   986 | Prerequisites: This works since 2023.3.1 thanx to @andrew-codechimp for the new del_screen 987 |   988 | ### Integrate in Home Assistant UI 989 |   990 | Add entities to the Home Assistant UI for interactive control of your display 991 |   992 | #### Brightness 993 |   994 | ```yaml 995 | number: 996 |   - platform: template 997 |     name: "$devicename brightness" 998 |     min_value: 0 999 |     max_value: 255 1000 |     step: 1 1001 |     lambda: |- 1002 |       return id(rgb8x32)->get_brightness(); 1003 |     set_action: 1004 |       lambda: |- 1005 |         id(rgb8x32)->set_brightness(x); 1006 | ``` 1007 |   1008 | #### Display switch 1009 |   1010 | ```yaml 1011 | switch: 1012 |   - platform: template 1013 |     name: "$devicename Display" 1014 |     icon: "mdi:power" 1015 |     restore_mode: ALWAYS_ON 1016 |     lambda: |- 1017 |       return id(rgb8x32)->show_display; 1018 |     turn_on_action: 1019 |       lambda: |- 1020 |         id(rgb8x32)->set_display_on(); 1021 |     turn_off_action: 1022 |       lambda: |- 1023 |         id(rgb8x32)->set_display_off(); 1024 | ``` 1025 | 1026 | ## Buzzer, sound, buttons, and automatic brightness 1027 |   1028 | Awtrix and PixelIt have hard-coded functionality. EHMTX is also capable of building something like that by lambdas. But this is all your freedom. 1029 |   1030 | Example: automatic brightness control with a bh1570 sensor 1031 |   1032 | ```yaml 1033 | sensor: 1034 |   - platform: bh1570 1035 |     # ... 1036 |     on_value: 1037 |       then: 1038 |          lambda: |- 1039 |             if (x > 200) 1040 |             { 1041 |                id(rgb8x32)->set_brightness(50); 1042 |             } else { 1043 |                id(rgb8x32)->set_brightness(250); 1044 |             } 1045 | ``` 1046 | 1047 | ## Notifier Custom Component 1048 | 1049 | There is an optional [notifier custom component](https://github.com/lubeda/EHMTX_custom_component) you can install with [HACS](https://hacs.xyz/). It is comparable to the **_screen** service, but more streamlined. 1050 | 1051 | ## Breaking changes 1052 | 1053 | - 2022.6.1 removed image types only `rgb565` is valid! 1054 | - 2023.2.0 removed awtrix icon `awtrixid` support 1055 | - 2023.3.5 removed automatic scaling of images and animations 1056 | - 2023.3.5 added status, display_on, display_off as default service => remove these from your YAML 1057 | - 2023.3.5 added indicator_on/off as default services => remove these from your YAML 1058 | - 2023.3.5 added *_color as default services => remove these from your YAML 1059 | - 2023.3.5 added show_all_icons, gauge_percent/gauge_off as default services => remove these from your YAML 1060 | - 2023.4.0 **cleaner naming**, please check all automations and YAML for change naming!!! 1061 | - 2023.4.0 removed the select component 1062 | 1063 | ## Usage 1064 | 1065 | The integration works with the Home Assistant API so, after boot of the device, it takes a few seconds until the service calls start working. 1066 | 1067 | ## Disclaimer 1068 | 1069 | THE SOFTWARE IS PROVIDED "AS IS", use at your own risk! 1070 | 1071 | ## Thanks 1072 | 1073 | - **[blakadder](https://github.com/blakadder)** for his contribution (cleanup README.md, fixed sample) 1074 | - **[andrew-codechimp](https://github.com/andrew-codechimp)** for his contribution (display on/off & del_screen "*" & show_clock with 0) 1075 | - **[jd1](https://github.com/jd1)** for his contributions 1076 | - **[aptonline](https://github.com/aptonline)** for his work on the ulanzi hardware 1077 | - **[wsbtak](https://github.com/wsbtak)** for the work on the ulanzi hardware 1078 | - **[ofirsnb](https://github.com/ofirsnb)** for his contributions 1079 | - **[darkpoet78](https://github.com/darkpoet78/MatrixClockFonts)** for his work on optimized fonts 1080 | - **[dennisse](https://github.com/dennisse)** Auto-brightness for the Ulanzi 1081 | - **everbody** that found bugs/issues and reported them! 1082 | 1083 | ## Special thanks to all sponsors 1084 | --------------------------------------------------------------------------------