initial push

pull/2/head
LuBeDa 3 years ago
parent aa5ae56f31
commit e932a086a7

@ -0,0 +1,14 @@
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
custom: ["https://www.paypal.com/donate/?hosted_button_id=FZDKSLQ46HJTU"]

23
.gitignore vendored

@ -0,0 +1,23 @@
components/ehmtx/__init__ copy._py
components/ehmtx/__pycache__/__init__.cpython-39.pyc
venv
.esphome/
_icons/
_fonts/
secrets.yaml
esphome.ps1
dev_*.yaml
ehmtx8266.yaml
info.de.md
components/ehmtx/select/__pycache__/__init__.cpython-39.pyc
ehmtx8266-rgb565.yaml
icons.html
components/ehmtx/__init__ copy.py
ehmtx8266-rgb565.yaml.html
ehmtx8266-rgb565-font.yaml
components/ehmtx/EHMTX_icons._cpp
.vscode/settings.json
ehmtx8266-select.yaml
svganimtest.html
servicetest
.DS_Store

Binary file not shown.

@ -0,0 +1,47 @@
# Changelog
## 2023.4.1
- breaking: removed show_icons
## 2023.4.0
- new: set_screen_color service to set color for a screen (service & action)
- breaking: all settings and parameters are named more consistant
- changed: center small text instead of left aligned
- removed: select component
## 2023.3.5
- new: show_seconds indicator top left corner
- breaking: removed automatic scaling of images
- new: support 8x32 icons without text
- breaking: added status,display_on,display_off as default service => remove these from your yaml
- breaking: added indicator_on/off as default service => remove these from your yaml
- breaking: added alarm_color,text_color,clock_color as default service => remove these from your yaml
- breaking: gauge is also schown while the clock is displayed but without moving the screen to the right
- breaking: show_icons as default service => remove from yaml
## 2023.3.4
- added: option to not display clock/date #53
- added: dynamic set_show_clock
- added: on_next_clock trigger
## 2023.3.3
- fixed: force_screen skips imediatly to the selected screen
- added: hold_time configurable
## 2023.3.2
- added: hold_screen for 20 additional seconds
## 2023.3.1
- added: del_screen with wildcards
- changed: maximum icons to 80
- fixed: skip_next
- fixed: show_all_icons on boot
## 2023.3.0
see README.md for features
e.g. Service **display_on** / **display_off**

Binary file not shown.

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2023 LuBeDa
Copyright (c) 2022,2023 LuBeDa
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

File diff suppressed because it is too large Load Diff

@ -0,0 +1,902 @@
# EspHoMaTriX version 2 (EHMTXv2)
A simple but very flexible DIY status display, build with a flexible 8x32 RGB LED panel implemented with [esphome.io](https://esphome.io)
![sample image](./images/sample.png)
## Important information
If you like my work please donate me a star on github and consider sponsoring me!!
## Introduction
There are some "RGB-matrix" status displays/clocks out there, the commercial one from Lametric and some very good DIY-alternatives.
- [LaMetric](https://lametric.com/en-US/) commercial ~ 199€
- [Ulanzi TC001](https://www.aliexpress.com/item/1005005008682055.html) commercial ~ 50€
- [Awtrix](https://awtrixdocs.blueforcer.de/#/) (project has been discontinued after more than 4 years now in August 2022)
- [PixelIt](https://docs.bastelbunker.de/pixelit/) (project is under active development)
- [Awtrix-Light](https://github.com/Blueforcer/awtrix-light) From the developer of Awtrix, optimized for the Ulanzi TC001 Hardware
The 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, without paid blueprints and the need of mqtt. But it had to be extensible, e.g. for the use as poolthermometer or as mediaplayer. All done by the magical power of esphome.
### ehmtx in the media
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).
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)
See this [nice article](https://blakadder.com/esphome-pixel-clock/) about EsphoMaTrix on a Ulanzi TC001 from [blakadder](https://github.com/blakadder).
Short video on Instagram [@blak_adder](https://www.instagram.com/reel/CpYVByRIaSI)
https://www.instagram.com/reel/CpYVByRIaSI
See this english discussions:
[Share your projects](https://community.home-assistant.io/t/esphomatrix-a-simple-clock-status-display/425325)
[ESPHOME](https://community.home-assistant.io/t/a-simple-diy-status-display-with-an-8x32-rgb-led/379051)
It was also mentiond in the blog [Building the Open Home](https://building.open-home.io/local-control-is-the-only-way/) and in the home assistant [livestream](https://youtu.be/IGnCGDaXR0M?t=6267)
Or in german:
[Showroom](https://community.simon42.com/t/8x32-pixel-uhr-mit-homeassistant-anbindung/1076)
### Features
Based a 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. Even 8x32 gif animations are possible. You can control nearly everything of the component.
### State
After the [old](https://github.com/lubeda/EsphoMaTrix) component became favorite there where some feature request which showed that my old code was a mess. I reworked the whole code and restructered it, so it is hopefully
more extensible.
## How to use
### The easy way
There is a little hype around the Ulanzi TC001 pixel clock. This hardware can be used with **EspHoMaTriX v2** (with some limitations). You can connect the device and flash it via USB-C. As a starting point you can use the [``UlanziTC001-simple.yaml``](https://github.com/lubeda/EspHoMaTriXv2/blob/main/UlanziTC001-simple.yaml). To control it from Home Assistant you can use the provided [blueprint](https://github.com/lubeda/EspHoMaTriXv2/blob/main/BP colored state.yaml)
### Steps (easy)
#### Step 1
Copy these files:
- UlanziTC001-simple.yaml
- ehmtxfont.ttf
to your esphome directory (usually /config/esphome).
#### Step 2
connect your ulanzi device to your host with USB-C and flash the firmware
#### Step 3
Copy EHMTXv2-colored-states.yaml to your blueprint path (usually /config/blueprints) in an subfolder ehmtxv2.
Reload your automations and have fun after configuring some states with this blueprint.
### result
The device should boot
![boot](images/booting.png)
and after a while (~30 seconds) it should display the correct time
![clock screen](images/clock_screen.png).
If not check the esphome logs for further investigations.
### The funny but more elaborate way
This is for the more advanced users. If you unterstand the concept of esphome you can't do nearly everything with this component.
#### Concept
You can add screens to a queue and all this screens are displayed one after another.
![timing](./images/timingv2.png)
Each screen can display different informations or is of a different kind. They all have a lifetime, if a screen isn't refreshed during its lifetime it will be removed from the queue. If there is nothing left in the queue the date and time screens are displayed. Some screens can show additional features like an alarm or indicator see [elements](#elements).
You can add screens from home assistant with services or from esphome via yaml.
#### Screen types
##### Date/Time screen
![clock screen](./images/clock_screen.png)
###### service
clock_screen => {"lifetime", "screen_time", "default_font", "r", "g", "b"}
date_screen => {"lifetime", "screen_time", "default_font", "r", "g", "b"}
###### api
all parameters have a default value.
```c
void clock_screen(int lifetime=D_LIFETIME, int screen_time=D_SCREEN_TIME,bool default_font=true,int r=C_RED, int g=C_GREEN, int b=C_BLUE);
void date_screen(int lifetime=D_LIFETIME, int screen_time=D_SCREEN_TIME,bool default_font=true, int r=C_RED, int g=C_GREEN, int b=C_BLUE);
```
##### icon screen
![icon screen](./images/icon_screen.png)
###### service
{"icon_name", "text", "lifetime", "screen_time", "default_font", "r", "g", "b"}
###### api
```c
void icon_screen(std::string icon, std::string text, int lifetime=D_LIFETIME, int screen_time=D_SCREEN_TIME,bool default_font=true,int r=C_RED, int g=C_GREEN, int b=C_BLUE);
```
##### fullscreen
For 8x32 icons or animations
<img src="https://raw.githubusercontent.com/lubeda/EsphoMaTrix/2023.5.b1/images/fullscreen.png" width=320 height=80 alt="Fullscreen">
###### service
{"icon_name", "lifetime", "screen_time"}
###### api
```c
void EHMTX::fullscreen(std::string iconname, int lifetime, int screen_time)
```
#### Installation
##### **EspHoMaTriXv2** custom component
**EspHoMaTriXv2** 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.
###### local use
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.
```yaml
external_components:
- source:
type: local
path: components # e.g. /config/esphome/components
```
##### use from repo
Use the github repo as 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 to logs while installing the firmware.
```yaml
external_components:
- source:
type: git
url: https://github.com/lubeda/EspHoMaTriXv2
ref: release # optional select a special branch or tag
```
##### Addressable_light component
The **EspHoMaTriXv2** component requires a 8x32 pixel addressable_light, it is referenced by the id `matrix_component`.
See the default [options](https://esphome.io/components/display/index.html)
There are some different matrices-types on the market, to adapt them to **EspHoMaTriXv2** you have to find the proper pixelmapper. If there is garbage on your display try the other `pixel_mapper`. Here are the most common types for flexible 8x32 matrices:
#### Type 1
Common for 8x32 RGB flexible matrices.
under the display tag specify this pixelmapper:
```yaml
display:
- platform: addressable_light
.....
pixel_mapper: |-
if (x % 2 == 0) {
return (x * 8) + y;
}
return (x * 8) + (7 - y);
.....
```
#### Type 2 (e.g. Ulanzi TC001)
Under the display tag specify this pixelmapper:
```yaml
display:
- platform: addressable_light
.....
pixel_mapper: |-
if (y % 2 == 0) {
return (y * 32) + x;
}
return (y * 32) + (31 - x);
.....
```
#### Type 3 (daisychained 8x8 panels)
```yaml
display:
- platform: addressable_light
.....
pixel_mapper: |-
return ((int) (x/8 * 64)) + x % 8 + y * 8;
.....
```
#### How to configure the pixel_mapper
You have to configure this `lambda` under the `display:` section to use the **EspHoMaTriXv2** component
```yaml
display:
- platform: addressable_light
id: ehmtx_display
.....
auto_clear_enabled: true
lambda: |-
id(rgb8x32)->tick();
id(rgb8x32)->draw();
```
#### Light component
The light component is used by the addressable_light component and referenced by id under `addressable_light_id:`.
To use the light component directly from home assistant add the sample lambdas```on_turn_on``` and ```on_turn_off``` to the light component.
***Sample***
```yaml
light:
- platform: neopixelbus
id: ehmtx_light
....
on_turn_on:
lambda: |-
id(rgb8x32)->set_enabled(false);
on_turn_off:
lambda: |-
id(rgb8x32)->set_enabled(true);
```
To hide the light component in home assistant use: `internal: true`
```yaml
light:
- platform: neopixelbus
id: ehmtx_light
internal: true
...
```
#### Time component
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.
#### Font
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 added **€** 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 wich 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).
DarkPoet78 is providing special fonts for 8x32 matrices in his [repo](https://github.com/darkpoet78/MatrixClockFonts)
```yaml
font:
- file: monobit.ttf
id: EHMTX_font
size: 16
glyphs: |
!"%()+*=,-_.:°0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz€@
```
#### Icons and Animations
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.
You can also specify an 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.
There are maximum 90 icons possible.
***Sample***
```yaml
emhtx:
icons:
- id: boot
file: icons/rocket.gif
duration: 75
- id: temp
file: temperature.png
- id: yoga
file: icons/yoga-bridge.gif
pingpong: true
- id: garage
file: garage.gif
duration: 100
- id: homeassistant
url: https://github.com/home-assistant/assets/raw/master/logo/logo-special.png
```
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**)
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.
GIFs are limited to 110 frames to limit the used amount of flash space.
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 amount of icons is limited to 90 in the code and also by the flash space and the RAM of your board.
See also [icon parameter](#icons)
#### Configuration
##### ehmtx component
This component is highly customizable.
***Example***
```yaml
ehmtx:
id: rgb8x32
clock_time: 7
screen_time: 9
show_seconds: true
matrix_component: ehmtx_display
time_component: ehmtx_time
icons2html: true
default_font_id: default_font
special_font_id: special_font
special_font_yoffset: 7
default_font_yoffset: 6
brightness: 80 # percent
time_format: "%H:%M"
date_format: "%d.%m."
week_start_monday: true # false equals sunday
scroll_count: 2 # scroll long text at least two times
scroll_interval: 80 # milliseconds
frame_interval: 192 # milliseconds
icons:
.....
```
***Parameters***
**id** (required, ID): Manually specify the ID used for code generation and in service definitions.
**clock_time** (optional, seconds): duration to display the clock after this time the date is display 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.
**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.
**hold_time** (optional, seconds): extends the display time of the current screen in seconds (default=20). Used in services or automations, see `hold_screen`
**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)
**show_seconds** (optional, boolean): toggle an indicator for seconds while the clock is displayed (default: false))
**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)
**default_font_yoffset** (optional, pixel): yoffset the text is aligned BASELINE_LEFT, the baseline defaults to `6`
**default_font_xoffset** (optional, pixel): xoffset the text is aligned BASELINE_LEFT, the left defaults to `1`
**special_font_yoffset** (optional, pixel): yoffset the text is aligned BASELINE_LEFT, the baseline defaults to `6`
**special_font_xoffset** (optional, pixel): xoffset the text is aligned BASELINE_LEFT, the left defaults to `1`
**matrix_component** (required, ID): ID of the addressable display
**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.
**time_component** (required, ID): ID of the time component. The display shows `!t!` until the time source is valid.
**default_font** (required, ID): ID of the default font
**special_font** (required, ID): ID of the special font, you can reuse your default font, but sometimes its nice to have a specialer font to minimise scrolling
**week_start_monday** (optional, bool): default monday is first day of week, false => Sunday
**scroll_interval** (optional, ms): the interval in ms to scroll the text (default=80), should be a multiple of the ```update_interval``` from the [display](https://esphome.io/components/display/addressable_light.html)
**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``` from the [display](https://esphome.io/components/display/addressable_light.html). Can be overwritten per icon/gif see [icons](#icons-and-animations) parameter `frame_duration`
**icons2html** (optional, boolean): If true, generate the html (_filename_.html) file to show all included icons. (default = `false`)
***Example output:***
![icon preview](./images/icons_preview.png)
### icons
***Parameters***
See [icon details](#icons-and-animations)
- **frame_duration** (optional, ms): in 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.
- **pingpong** (optional, boolean): in case of a gif file you can reverse the frames instead of starting from the first frame.
- **file** (Exlusive, filename): a local filename
- **url** (Exclusive, url): an URL to download the icon
- **lameid** (Exclusive, number): the ID from the lametric icon database
## Control your display
A lot of features are accessible with actions, you can use in your yaml
### Local actions/lambdas
#### Add screen to your display queue
You can add screens locally and display data directly from any local sensor. See this sample:
##### Lambda
Take care that the ```char text[30];``` has enough space to store the formated text.
```yaml
sensor:
- platform: bh1750
id: sensorlx
...
on_value:
then:
lambda: |-
char text[30];
sprintf(text,"Light: %2.1f lx", id(sensorlx).state);
// 5 Minutes,each time at least 11 seconds,no alarm
id(rgb8x32)->icon_screen("sun", text); // uses default values for color etc.
```
#### Set (alarm/clock/gauge/text/today/weekday) color action
Sets the default color of the selected element
##### Lambda set text color
```yaml
lamda:
id(rgb8x32)->set_text_color(200,45,12);
```
##### Force screen
Force the selected [icon_screen](#icon-screen) ```icon_name``` to be displayed next. Afterwards the loop is continuing from this screen. e.g. helpfull for alarms. Or after an update of the value/text.
```yaml
id(rgb8x32)->force_screen("homeassistant");
```
##### Change configuration during runtime
_Configuration variables/functions:_
Experienced programmers can use this public methods:
```c
```
### Local trigger
To use the display without homeassistant 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.
#### on_next_screen
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:
**x** (Name of the icon, std::string): value to use in lambda
**y** (displayed text, std::string): value to use in lambda
See the examples:
##### Write information to esphome log
```yaml
ehmtx:
....
on_next_screen:
lambda: |-
ESP_LOGD("TriggerTest","Iconname: %s",x.c_str());
ESP_LOGI("TriggerTest","Text: %s",y.c_str());
```
##### Send an event to Home Assistant
To send data back to home assistant you can use events.
```yaml
ehmtx:
....
on_next_screen:
- homeassistant.event:
event: esphome.next_screen
data_template:
iconname: !lambda "return x.c_str();"
text: !lambda "return y.c_str();"
```
***Result***
![events](./images/events.png)
#### on_next_clock
The trigger ```on_next_clock``` is triggered every time a new clock display circle starts.
See the examples:
##### Change something for each clock circle
```yaml
ehmtx:
....
on_next_clock:
lambda: |-
id(rgb8x32)->.......
```
**(D)** Service **brightness**
Sets the overall brightness of the display (`0..255`)
_parameters:_
- ```brightness```: from dark to bright (`0..255`) (default = `80`) as set in the light component by ```color_correct: [30%, 30%, 30%]```
There's an easier way by using a number component:
```yaml
number:
- platform: template
name: "LED brightness"
min_value: 0
max_value: 255
step: 1
lambda: |-
return id(rgb8x32)->get_brightness();
set_action:
lambda: |-
id(rgb8x32)->set_brightness(x);
```
**(D)** Service **del_screen**
Removes a screen from the display by icon name. If this screen is actually display while sending this command the screen will be displayed until its "show_screen"-time has ended.
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.
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.
_parameters:_
- ```icon_name```: Icon `id` defined in the yaml (see installation)
- ```mode```: The mode is for internal purposes use 5 for icon_screen
**(D)** Service **indicator_on** / **indicator_off**
Turns indicator on/off
Display a colored corner on all screens and the clock. You can define the color by parameter.
_parameters:_
- ```r``` red in 0..255
- ```g``` green in 0..255
- ```b``` blue in 0..255
**(D)** Service **display_on** / **display_off**
Turns the display on or off
There's an easier way in using a switch component:
***Sample***
```yaml
switch:
- platform: template
name: "$devicename Display"
icon: "mdi:power"
restore_mode: ALWAYS_ON
lambda: |-
return id(rgb8x32)->show_display;
turn_on_action:
lambda: |-
id(rgb8x32)->set_display_on();
turn_off_action:
lambda: |-
id(rgb8x32)->set_display_off();
```
Service **skip_screen**
If there is more than one screen in the queue, skip to the next screen.
e.g. on the Ulanzi TC001
```yaml
binary_sensor:
- platform: gpio
pin:
number: $left_button_pin
inverted: true
on_press:
lambda:
id(rgb8x32)->skip_screen();
```
Service **hold_screen**
Displays the current screen for configured ammount (see **hold_time**) (default=20) seconds longer.
e.g. on the Ulanzi TC001
```
binary_sensor:
- platform: gpio
pin:
number: $right_button_pin
inverted: true
on_press:
lambda:
id(rgb8x32)->hold_screen();
```
**(D)** Service **status**
This service displays the running queue and a list of icons in the logs
```log
[13:10:10][I][EHMTX:175]: status status: 1 as: 1
[13:10:10][I][EHMTX:176]: status screen count: 3
[13:10:10][I][EHMTX:181]: status slot: 0 icon: 36 text: 47.9°C end: 400
[13:10:10][I][EHMTX:181]: status slot: 1 icon: 23 text: Supa langer Text end: 310
[13:10:10][I][EHMTX:181]: status slot: 2 icon: 1 text: 10.3°C end: 363
[13:10:10][I][EHMTX:186]: status icon: 0 name: boot
[13:10:10][I][EHMTX:186]: status icon: 1 name: temp
[13:10:10][I][EHMTX:186]: status icon: 2 name: garage
[13:10:10][I][EHMTX:186]: status icon: 3 name: wind
[13:10:10][I][EHMTX:186]: status icon: 4 name: rain
```
Service **show_gauge** / **del_gauge_off**
**(D)** Turns gauge on/off
Displays a colored gauge at the left side of the display. You can define the color by parameter.
_parameters:_
- ```percent``` gauge percentage
## Integration in Home Assistant
To control your display it has to be integrated in Home Assistant. Then it provides a number of 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).
### Services
All communication with Home Assistant use the homeasistant-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)```.
#### Overview of default services
These services are the same as the local services, so you can adapt the documentation there
|name|parameter|
|----|----|
|`get_status`|*none*|
|`set_display_on`|*none*|
|`set_display_off`|*none*|
|`show_all_icons`|*none*|
|`hold_screen`|*none*|
|`set_indicator_on`| {"r", "g", "b"}|
|`set_indicator_off`|*none*|
|`set_gauge_value`| {"percent"}|
|`set_gauge_off`|*none*|
|`set_alarm_color`| {"r", "g", "b"}|
|`set_text_color` | {"r", "g", "b"}|
|`set_clock_color`| {"r", "g", "b"}|
|`set_today_color`| {"r", "g", "b"}|
|`set_gauge_color`| {"r", "g", "b"}|
|`set_weekday_color` |{"r", "g", "b"}|
|`set_screen_color` |{"icon_name","r", "g", "b"}|
|`add_screen` |{"icon_name", "text", "lifetime","screen_time", "alarm"}|
|`force_screen`| {"icon_name"}|
|`del_screen`| {"icon_name"}|
|`set_brightness`| {"value"}|
### Use in Home Assistant automations
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.
```yaml
alias: EHMTX 8266 Test
description: ''
trigger:
- platform: numeric_state
entity_id: sensor.wind_speed
id: wind
- platform: state
entity_id: sensor.actual_temperature
id: temp
- platform: state
entity_id: sensor.wg_cover_device
id: cover
condition: []
action:
- service: esphome.ehmtx8266_screen
data:
icon_name: '{{trigger.id}}'
text: >-
{{trigger.to_state.state}}{{trigger.to_state.attributes.unit_of_measurement}}
mode: queued
max: 10
```
## Tipps
### Display precision after home assistant 2023.3.0
See [templating](https://www.home-assistant.io/docs/configuration/templating/#states) for possibilities to optimize the output
e.g.
```{{ states(sensor.solarpower, rounded=True) }} kWh```
### Specific icons per condition
Add an icon per weather condition to the ehmtx component
```yaml
- id: weather_clear_night
lameid: 52163
- id: weather_cloudy
lameid: 25991
- id: weather_fog
lameid: 52167
......
```
Sample automation to show the weather with local temperature
```yaml
alias: EHMTX weather
description: weather with icon per condition
trigger:
- platform: state
entity_id: weather.metno
action:
- service: esphome.ulanzi_del_screen
data:
icon_name: weather_*
mode: 5
- service: esphome.ulanzi_icon_screen
data:
icon_name: weather_{{ trigger.to_state.state }}
text: >-
{{ states("sensor.external_actual_temperature") }}°C
....
```
or another sample automation for the trashcan type
```yaml
alias: "EHMTX Müllanzeige"
description: Anzeige welche Tonne raus muss. iconnamen gekürzt
trigger:
- platform: time
at:
- "06:30"
- "08:30"
- "10:30"
- "15:00"
- "17:00"
- "19:00"
condition:
- condition: numeric_state
entity_id: sensor.mulltrigger
below: "3"
action:
- service: esphome.ulanzi_del_screen
data:
icon_name: trash_*
mode: 5
- service: esphome.ulanzi_icon_screen
data:
icon_name: >-
trash_{{ states("sensor.mulldetails") | replace("Biotonne", "brow")|
replace("Papiertonne","blue")| replace("Restmüll", "grey")|
replace("gelbe Tonne","yell|") | truncate(4,true,"") }}
text: >-
{{ states("sensor.mulldetails")|replace(" in","")|replace(" days","
Tagen") | replace ("0 Tagen","heute") | replace ("1 Tagen","morgen")}}
lifetime: 120
...
```
### Integrate in Home Assistant UI
Add entities to Home Assistant UI for interactive control of your display
#### Brightness
```yaml
number:
- platform: template
name: "$devicename brightness"
min_value: 0
max_value: 255
step: 1
lambda: |-
return id(rgb8x32)->get_brightness();
set_action:
lambda: |-
id(rgb8x32)->set_brightness(x);
```
#### Display switch
```yaml
switch:
- platform: template
name: "$devicename Display"
icon: "mdi:power"
restore_mode: ALWAYS_ON
lambda: |-
return id(rgb8x32)->show_display;
turn_on_action:
lambda: |-
id(rgb8x32)->set_display_on();
turn_off_action:
lambda: |-
id(rgb8x32)->set_display_off();
```
### automatic brightness
Awtrix and PixelIt have hardcoded functionality. EHMTX is also capable to build something like that by lambdas. But this is all your freedom. See the Ulanzi simple yaml as guide.
Example: automatic brightness control with an bh1570 sensor
```yaml
sensor:
- platform: bh1570
# ...
on_value:
then:
lambda: |-
if (x > 200)
{
id(rgb8x32)->set_brightness(50);
} else {
id(rgb8x32)->set_brightness(250);
}
```
## Breaking changes
## Usage
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. If you see a growing green rectangle after boot you have to wait a bit until the api is connected etc.
![timing](./images/booting.png)
## Disclaimer
THE SOFTWARE IS PROVIDED "AS IS", use at your own risk!
## Thanks
- **[blakadder](https://github.com/blakadder)** for his contribution (cleanup README.md,fixed sample)
- **[andrew-codechimp](https://github.com/andrew-codechimp)** for his contribution (display on/off & del_screen "*" & show_clock with 0)
- **[jd1](https://github.com/jd1)** for his contributions
- **[aptonline](https://github.com/aptonline)** for his work on the ulanzi hardware
- **[wsbtak](https://github.com/wsbtak)** for the work on the ulanzi hardware
- **[ofirsnb](https://github.com/ofirsnb)** for his contributions
- **[darkpoet78](https://github.com/darkpoet78/MatrixClockFonts)** for his work on optimized fonts
- ** everbody that found bugs/issues and reported them!
## Special thanks to all sponsors

@ -0,0 +1,45 @@
# To Do's
## function
- [ ] alarm on all screen but fullscreen
- [ ] indicator on all screen but fullscreen and clock
- [ ] refreshing an icon screen should extend the display time
- [ ] scroll left to right
- [x] center text
- [x] alarm unabhängig von screen
- [x] del_screen with * and filter by type do delete all clocks etc.
- [x] force_screen with * and filter by type do delete all clocks etc.
- [x] gauge mit farbe und del_gaugeall clocks etc.
- [ ] second point moveable
- [ ] second point to clock screen
## ux
- [ ] blueprints
- [ ] Color in blueprints
- [ ] all modes in one blueprint
- [x] default values for all functions
- [ ] provide sample font from [url](https://www.pentacom.jp/pentacom/bitfontmaker2/)
- [x] start animation
## style
- [ ] in screen rename text_color to color
- [x] default_font before alarm paramater
- [x] screen_time instead of showtime
- [x] lifetime instead of durations
- [x] remove actions in favor of functions
## documentation
- [ ] simple mode sample
- [ ] enhanced mode sample
- [ ] remake documentation
- [ ] more explanation for fonts
- [ ] reference to https://www.pentacom.jp/pentacom/bitfontmaker2/gallery/?id=13768
## noch prüfen
- [ ] Timer display with formation H:M:S
- [ ] find a way for automastically add smalfont

@ -0,0 +1,207 @@
substitutions:
devicename: ulanzi
ledpin: GPIO32
buzzerpin: GPIO15
friendly_name: LED Matrix
board: esp32dev
# Pin definition from https://github.com/aptonline/PixelIt_Ulanzi
battery_pin: GPIO34
ldr_pin: GPIO35
matrix_pin: GPIO32
left_button_pin: GPIO26
mid_button_pin: GPIO27
right_button_pin: GPIO14
buzzer_pin: GPIO15
scl_pin: GPIO22
sda_pin: GPIO21
external_components:
- source:
type: git
url: https://github.com/lubeda/EsphoMaTrix
refresh: 60s
components: [ ehmtx ]
esphome:
comment: "ehmtx from LuBeDa"
name: $devicename
on_boot:
then:
- ds1307.read_time:
esp32:
board: esp32dev
font:
- file: MatrixClock-Chunky8.bdf
id: default_font
glyphs: |
!?"%()+*=,-_.:°0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnÖÄÜöäüopqrstuvwxyz@<>ß§/
- file: MatrixClock-Chunky6.bdf
id: special_font
binary_sensor:
- platform: status
name: "$devicename Status"
- platform: gpio
pin:
number: $left_button_pin
inverted: true
name: "Left button"
- platform: gpio
pin:
inverted: true
number: $mid_button_pin
mode: INPUT_PULLUP
name: "Middle button"
- platform: gpio
pin:
number: $right_button_pin
inverted: true
name: "Right button"
logger:
level: WARNING
# Enable Home Assistant API
api:
services:
- service: alarm
variables:
icon_name: string
text: string
then:
lambda: |-
id(rgb8x32)->add_icon_screen(icon_name,text, 5, 30, true, true, 200, 50, 50);
id(rgb8x32)->force_icon_screen(icon_name);
- service: screen
variables:
icon_name: string
text: string
then:
id(rgb8x32)->add_icon_screen(icon_name,text, 5, 20, false, true, 240, 240, 240);
- service: tune
variables:
tune: string
then:
- rtttl.play:
rtttl: !lambda 'return tune;'
sensor:
- platform: sht3xd
temperature:
name: "$devicename Temperature"
humidity:
name: "$devicename Relative Humidity"
update_interval: 60s
- platform: adc
pin: $battery_pin
name: "$devicename Battery"
id: battery_voltage
update_interval: 10s
device_class: battery
accuracy_decimals: 0
attenuation: auto
filters:
- sliding_window_moving_average:
window_size: 15
send_every: 15
send_first_at: 1
- multiply: 1.6
- lambda: |-
auto r = ((x - 3) / 0.69 * 100.00);
if (r >= 100) return 100;
if (r > 0) return r;
if (r <= 0) return 1;
return 0;
unit_of_measurement: '%'
- platform: adc
id: light_sensor
name: "$devicename Illuminance"
pin: $ldr_pin
update_interval: 10s
attenuation: auto
unit_of_measurement: lx
device_class: illuminance
accuracy_decimals: 0
filters:
- lambda: |-
return (x / 10000.0) * 2000000.0 - 15 ;
ota:
password: !secret ota_password
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
web_server:
output:
- platform: ledc
pin: $buzzerpin
id: rtttl_out
rtttl:
output: rtttl_out
i2c:
sda: $sda_pin
scl: $scl_pin
scan: true
id: i2cbus
light:
- platform: neopixelbus
id: ehmtx_light
type: GRB
internal: true
variant: WS2812
pin: $ledpin
num_leds: 256
color_correct: [30%, 30%, 30%]
gamma_correction: 1.0
name: "$devicename Light"
restore_mode: ALWAYS_OFF
time:
- platform: homeassistant
on_time_sync:
then:
ds1307.write_time:
- platform: ds1307
update_interval: never
id: ehmtx_time
display:
- platform: addressable_light
id: ehmtx_display
addressable_light_id: ehmtx_light
width: 32
height: 8
internal: true
pixel_mapper: |-
if (y % 2 == 0) {
return (y * 32) + x;
}
return (y * 32) + (31 - x);
rotation: 0°
update_interval: 16ms
auto_clear_enabled: true
lambda: |-
id(rgb8x32)->tick();
id(rgb8x32)->draw();
ehmtx:
id: rgb8x32
clock_time: 8
screen_time: 10
hold_time: 27
icons2html: true
yoffset: 7
matrix_component: ehmtx_display
time_component: ehmtx_time
time_format: "%H:%M"
show_seconds: false
font_id: special_font
icons: !include ehmtx_icons.yaml

@ -0,0 +1,239 @@
blueprint:
name: |-
ehmtxv2: show state with unit on on 8x32 RGB-display
description: This blueprint is triggered by a state change. If the state is between certain values it will be displayed with special colors or no state change will be displayed
domain: automation
input:
ehmtx_device:
name: which device to display at
selector:
device:
integration: esphome
trigger_sensor:
name: which state to show
description: This sensor state will be displayed
selector:
entity:
domain: sensor
default_color:
name: default text color
description: this is the default textcolor for values within the low and the high value
selector:
color_rgb:
default: [240, 240, 240]
lifetime:
name: how many minutes is this screen in the loop
selector:
number:
min: 1
max: 1440
step: 1
default: 2
screen_time:
name: minimum display time per loop in seconds
selector:
number:
min: 6
max: 120
step: 2
default: 12
icon_name:
name: the icon
selector:
select:
mode: dropdown
options:
[
"error",
"lamp",
"sonos",
"print3d",
"internet",
"speaker",
"alien",
"temp",
"garage",
"door",
"wind",
"rain",
"shop",
"phone",
"fire",
"alexa",
"tv",
"frost",
"muell",
"cookit",
"nature",
"work",
"bike",
"school",
"amazon",
"post",
"money",
"power",
"solar",
"yoga",
"startrek",
"energy",
"sun",
"diesel",
"benzine10",
"vacuum",
"rainprecip",
"iss",
"thunder",
"nina_warning",
"birthday",
"firework",
"coffee",
"lightning",
"xmastree",
"sauna",
"trash_grey",
"trash_blue",
"trash_yell",
"trash_brow",
"weather_clear_night",
"weather_cloudy",
"weather_fog",
"weather_lightingrainy",
"weather_partlycloudy",
"weather_pouring",
"weather_rainy",
"weather_snowy",
"weather_snowy_rainy",
"weather_sunny",
"f_mario",
"f_onair",
"f_bat",
"f_matrix",
"f_invaders",
"f_amongus",
"theodor",
"f_sleeping",
"mops",
"girl",
"timer",
"lasticon",
]
default: homeassistant
use_colors:
name: Use colors based on state values
selector:
boolean:
default: true
low_value:
name: below this vaule is a special color
selector:
number:
mode: box
min: -10000
max: +10000
default: 0
low_color:
name: Low color
description: the color for values below the limit
selector:
color_rgb:
default: [40, 240, 40]
high_value:
name: upper limit
selector:
number:
mode: box
min: -10000
max: +10000
default: 0
high_color:
name: High Color
description: Text color for values above the defined value
selector:
color_rgb:
default: [240, 40, 40]
default_font:
name: true uses the default, false the specialer font
selector:
boolean:
default: true
variables:
display: !input ehmtx_device
def_color: !input default_color
lo_color: !input low_color
hi_color: !input high_color
trigger:
- platform: state
entity_id: !input trigger_sensor
action:
- if:
- condition: template
value_template: "{{ not use_colors }}"
then:
- service: esphome.{{ device_attr(display, "name") }}_icon_screen
data:
icon_name: !input icon_name
screen_time: !input screen_time
lifetime: !input lifetime
text: "{{trigger.to_state.state}} {{trigger.to_state.attributes.unit_of_measurement}}"
default_font: !input default_font
r: |-
{{ def_color[0] }}
g: |-
{{ def_color[1] }}
b: |-
{{ def_color[2] }}
else:
- choose:
- conditions:
- condition: numeric_state
entity_id: !input trigger_sensor
below: !input low_value
sequence:
- service: esphome.{{ device_attr(display, "name") }}_icon_screen
data:
icon_name: !input icon_name
screen_time: !input screen_time
lifetime: !input lifetime
text: "{{trigger.to_state.state}} {{trigger.to_state.attributes.unit_of_measurement}}"
default_font: !input default_font
r: |-
{{ lo_color[0] }}
g: |-
{{ lo_color[1] }}
b: |-
{{ lo_color[2] }}
- conditions:
- condition: numeric_state
entity_id: !input trigger_sensor
above: !input high_value
sequence:
- service: esphome.{{ device_attr(display, "name") }}_icon_screen
data:
icon_name: !input icon_name
screen_time: !input screen_time
lifetime: !input lifetime
text: "{{trigger.to_state.state}} {{trigger.to_state.attributes.unit_of_measurement}}"
default_font: !input default_font
r: |-
{{ hi_color[0] }}
g: |-
{{ hi_color[1] }}
b: |-
{{ hi_color[2] }}
default:
- service: esphome.{{ device_attr(display, "name") }}_icon_screen
data:
icon_name: !input icon_name
screen_time: !input screen_time
lifetime: !input lifetime
text: "{{trigger.to_state.state}} {{trigger.to_state.attributes.unit_of_measurement}}"
default_font: !input default_font
r: |-
{{ def_color[0] }}
g: |-
{{ def_color[1] }}
b: |-
{{ def_color[2] }}

@ -0,0 +1,212 @@
blueprint:
name: |-
ehmtxv2':' delete screen from 8x32 RGB-display
description: This blueprint is triggered by a state change. If the state is between certain values it will be displayed with special colors or no state change will be displayed
domain: automation
input:
ehmtx_device:
name: which device to display at
selector:
device:
integration: esphome
mode:
name: the mode
selector:
select:
custom_value: true
mode: dropdown
options:
- label: MODE_ICONSCREEN
value: 5
- label: MODE_BLANK
value: 1
- label: MODE_CLOCK
value: 2
- label: MODE_DATE
value: 3
- label: MODE_FULLSCREEN
value: 4
- label: MODE_TEXT
value: 6
default: 5
icon_name:
name: the icon
selector:
select:
mode: dropdown
options:
[
"*",
"error",
"github",
"precipitation",
"nina",
"waschmaschine",
"car",
"lamp",
"sonos",
"print3d",
"internet",
"speaker",
"alien",
"temp",
"garage",
"door",
"wind",
"rain",
"shop",
"phone",
"fire",
"alexa",
"tv",
"frost",
"muell",
"cookit",
"nature",
"work",
"bike",
"school",
"amazon",
"post",
"money",
"power",
"solar",
"yoga",
"startrek",
"energy",
"sun",
"diesel",
"benzine10",
"vacuum",
"rainprecip",
"iss",
"thunder",
"nina_warning",
"birthday",
"firework",
"coffee",
"lightning",
"xmastree",
"sauna",
"trash_grey",
"trash_blue",
"trash_yell",
"trash_brow",
"weather_clear_night",
"weather_cloudy",
"weather_fog",
"weather_lightingrainy",
"weather_partlycloudy",
"weather_pouring",
"weather_rainy",
"weather_snowy",
"weather_snowy_rainy",
"weather_sunny",
"f_mario",
"f_onair",
"f_bat",
"f_matrix",
"f_invaders",
"f_amongus",
"theodor",
"f_sleeping",
"mops",
"girl",
"timer",
"lasticon",
]
default: homeassistant
low_value:
name: below this vaule is a special color
selector:
number:
mode: box
min: -10000
max: +10000
low_color:
name: Low color
description: the color for values below the limit
selector:
color_rgb:
default: [40, 240, 40]
high_value:
name: upper limit
selector:
number:
mode: box
min: -10000
max: +10000
high_color:
name: High Color
description: Text color for values above the defined value
selector:
color_rgb:
default: [240, 40, 40]
default_font:
name: true uses the default, false the specialer font
selector:
boolean:
default: true
variables:
display: !input ehmtx_device
def_color: !input default_color
lo_color: !input low_color
hi_color: !input high_color
trigger:
- platform: state
entity_id: !input trigger_sensor
action:
- choose:
- conditions:
- condition: numeric_state
entity_id: !input trigger_sensor
below: !input low_value
sequence:
- service: esphome.{{ device_attr(display, "name") }}_icon_screen
data:
icon_name: !input icon_name
screen_time: !input screen_time
lifetime: !input lifetime
text: "{{trigger.to_state.state}} {{trigger.to_state.attributes.unit_of_measurement}}"
default_font: !input default_font
r: |-
{{ lo_color[0] }}
g: |-
{{ lo_color[1] }}
b: |-
{{ lo_color[2] }}
- conditions:
- condition: numeric_state
entity_id: !input trigger_sensor
above: !input high_value
sequence:
- service: esphome.{{ device_attr(display, "name") }}_icon_screen
data:
icon_name: !input icon_name
screen_time: !input screen_time
lifetime: !input lifetime
text: "{{trigger.to_state.state}} {{trigger.to_state.attributes.unit_of_measurement}}"
default_font: !input default_font
r: |-
{{ hi_color[0] }}
g: |-
{{ hi_color[1] }}
b: |-
{{ hi_color[2] }}
default:
- service: esphome.{{ device_attr(display, "name") }}_icon_screen
data:
icon_name: !input icon_name
screen_time: !input screen_time
lifetime: !input lifetime
text: "{{trigger.to_state.state}} {{trigger.to_state.attributes.unit_of_measurement}}"
default_font: !input default_font
r: |-
{{ def_color[0] }}
g: |-
{{ def_color[1] }}
b: |-
{{ def_color[2] }}

@ -0,0 +1,826 @@
#include "esphome.h"
namespace esphome
{
EHMTX::EHMTX() : PollingComponent(TICKINTERVAL)
{
this->show_display = true;
this->display_gauge = false;
this->display_indicator = false;
this->display_alarm = false;
this->icon_count = 0;
this->text_color = Color(C_RED, C_GREEN, C_BLUE);
this->today_color = Color(C_RED, C_GREEN, C_BLUE);
this->weekday_color = Color(CD_RED, CD_GREEN, CD_BLUE);
this->clock_color = Color(C_RED, C_GREEN, C_BLUE);
this->alarm_color = Color(CA_RED, CA_GREEN, CA_BLUE);
this->gauge_color = Color(CD_RED, CD_GREEN, CD_BLUE);
this->gauge_value = 0;
this->screen_pointer = 0;
for (uint8_t i = 0; i < MAXQUEUE; i++)
{
this->queue[i] = new EHMTX_queue(this);
}
this->is_running =false;
}
void EHMTX::set_time_format(std::string s)
{
this->time_fmt = s;
}
void EHMTX::set_date_format(std::string s)
{
this->date_fmt = s;
}
void EHMTX::show_indicator(int r, int g, int b)
{
this->indicator_color = Color((uint8_t)r & 248, (uint8_t)g & 252, (uint8_t)b & 248);
this->display_indicator = true;
ESP_LOGD(TAG, "show_indicator r: %d g: %d b: %d", r, g, b);
}
void EHMTX::hide_indicator()
{
this->display_indicator = false;
ESP_LOGD(TAG, "hide_indicator");
}
void EHMTX::set_display_off()
{
this->show_display = false;
ESP_LOGD(TAG, "display off");
}
void EHMTX::set_display_on()
{
this->show_display = true;
ESP_LOGD(TAG, "display on");
}
void EHMTX::set_today_color(int r, int g, int b)
{
this->today_color = Color((uint8_t)r & 248, (uint8_t)g & 252, (uint8_t)b & 248);
ESP_LOGD(TAG, "default today color r: %d g: %d b: %d", r, g, b);
}
void EHMTX::set_weekday_color(int r, int g, int b)
{
this->weekday_color = Color((uint8_t)r & 248, (uint8_t)g & 252, (uint8_t)b & 248);
ESP_LOGD(TAG, "default weekday color: %d g: %d b: %d", r, g, b);
}
void EHMTX::set_clock_color(int r, int g, int b)
{
this->clock_color = Color((uint8_t)r & 248, (uint8_t)g & 252, (uint8_t)b & 248);
ESP_LOGD(TAG, "default clock color r: %d g: %d b: %d", r, g, b);
}
void EHMTX::set_text_color(int r, int g, int b)
{
this->text_color = Color((uint8_t)r & 248, (uint8_t)g & 252, (uint8_t)b & 248);
ESP_LOGD(TAG, "default text color r: %d g: %d b: %d", r, g, b);
}
bool EHMTX::string_has_ending(std::string const &fullString, std::string const &ending)
{
if (fullString.length() >= ending.length())
{
return (0 == fullString.compare(fullString.length() - ending.length(), ending.length(), ending));
}
else
{
return false;
}
}
uint8_t EHMTX::find_icon(std::string name)
{
for (uint8_t i = 0; i < this->icon_count; i++)
{
if (strcmp(this->icons[i]->name.c_str(), name.c_str()) == 0)
{
ESP_LOGD(TAG, "icon: %s found id: %d", name.c_str(), i);
return i;
}
}
ESP_LOGW(TAG, "icon: %s not found", name.c_str());
return MAXICONS;
}
uint8_t EHMTX::find_icon_in_queue(std::string name)
{
for (uint8_t i = 0; i < MAXQUEUE; i++)
{
if (strcmp(this->queue[i]->icon_name.c_str(), name.c_str()) == 0)
{
ESP_LOGD(TAG, "find_icon_in_queue: icon: %s at position %d", name.c_str(), i);
return i;
}
}
ESP_LOGW(TAG, "find_icon_in_queue: icon: %s not found", name.c_str());
return MAXICONS;
}
void EHMTX::hide_gauge()
{
this->display_gauge = false;
ESP_LOGD(TAG, "hide gauge");
}
void EHMTX::show_gauge(int percent, int r, int g, int)
{
this->display_gauge = false;
if (percent <= 100)
{
this->display_gauge = true;
this->gauge_value = percent; // (uint8_t)(100 - percent) * 7 / 100;
ESP_LOGD(TAG, "set gauge value: %d", percent);
}
}
void EHMTX::draw_gauge()
{
if (this->display_gauge)
{
this->display->line(0, 7, 0, 0, esphome::display::COLOR_OFF);
this->display->line(1, 7, 1, 0, esphome::display::COLOR_OFF);
if (this->gauge_value > 11)
{
uint8_t height = 7 - (int)(this->gauge_value / 12.5);
this->display->line(0, 7, 0, height, this->gauge_color);
}
}
}
void EHMTX::setup()
{
ESP_LOGD(TAG, "Setting up services");
register_service(&EHMTX::get_status, "status");
register_service(&EHMTX::set_display_on, "display_on");
register_service(&EHMTX::set_display_off, "display_off");
register_service(&EHMTX::hold_screen, "hold_screen");
register_service(&EHMTX::hide_indicator, "hide_indicator");
register_service(&EHMTX::hide_gauge, "hide_gauge");
register_service(&EHMTX::hide_alarm, "hide_alarm");
register_service(&EHMTX::show_gauge, "show_gauge", {"percent", "r", "g", "b"});
register_service(&EHMTX::show_alarm, "show_alarm", {"r", "g", "b"});
register_service(&EHMTX::show_indicator, "show_indicator", {"r", "g", "b"});
register_service(&EHMTX::set_text_color, "text_color", {"r", "g", "b"});
register_service(&EHMTX::set_clock_color, "clock_color", {"r", "g", "b"});
register_service(&EHMTX::set_today_color, "today_color", {"r", "g", "b"});
register_service(&EHMTX::set_weekday_color, "weekday_color", {"r", "g", "b"});
register_service(&EHMTX::del_screen, "del_screen", {"icon_name","mode"});
register_service(&EHMTX::force_screen, "force_screen", {"icon_name","mode"});
register_service(&EHMTX::fullscreen, "fullscreen", {"icon_name", "lifetime", "screen_time"});
register_service(&EHMTX::icon_screen, "icon_screen", {"icon_name", "text", "lifetime", "screen_time", "default_font", "r", "g", "b"});
register_service(&EHMTX::text_screen, "text_screen", {"text", "lifetime", "screen_time", "default_font", "r", "g", "b"});
register_service(&EHMTX::clock_screen, "clock_screen", {"lifetime", "screen_time", "default_font", "r", "g", "b"});
register_service(&EHMTX::blank_screen, "blank_screen", {"lifetime", "screen_time"});
register_service(&EHMTX::date_screen, "date_screen", {"lifetime", "screen_time", "default_font", "r", "g", "b"});
register_service(&EHMTX::set_brightness, "brightness", {"value"});
this->is_running=true;
}
void EHMTX::show_alarm(int r, int g, int b)
{
this->alarm_color = Color((uint8_t)r & 248, (uint8_t)g & 252, (uint8_t)b & 248);
this->display_alarm = true;
ESP_LOGD(TAG, "show alarm color r: %d g: %d b: %d", r, g, b);
}
void EHMTX::hide_alarm()
{
this->display_alarm = false;
ESP_LOGD(TAG, "hide_alarm");
}
void EHMTX::blank_screen(int lifetime, int showtime)
{
auto scr = this->find_free_queue_element();
scr->screen_time = showtime;
scr->mode = MODE_BLANK;
scr->endtime = this->clock->now().timestamp + lifetime * 60;
}
void EHMTX::update() // called from polling component
{
}
void EHMTX::force_screen(std::string icon_name, int mode)
{
// if (this->string_has_ending(icon_name, "*"))
// {
// // remove the *
// icon_name = icon_name.substr(0, icon_name.length() - 1);
// }
for (uint8_t i = 0; i < MAXQUEUE; i++)
{
if (this->queue[i]->mode == mode)
{
bool force = true;
if ((mode == MODE_ICONSCREEN)||(mode == MODE_FULLSCREEN))
{
if (strcmp(this->queue[i]->icon_name.c_str(), icon_name.c_str()) != 0)
{
force = false;
}
if (force)
{
ESP_LOGD(TAG, "force_screen: found position: %d", i);
this->queue[i]->last_time = 0;
this->queue[i]->endtime += this->queue[i]->screen_time;
this->next_action_time = this->clock->now().timestamp;
ESP_LOGW(TAG, "force_screen: icon %s in mode %d", icon_name.c_str(), mode);
}
}
}
}
}
uint8_t EHMTX::find_oldest_queue_element()
{
uint8_t hit = MAXQUEUE;
time_t last_time = this->clock->now().timestamp;
for (size_t i = 0; i < MAXQUEUE; i++)
{
if ((this->queue[i]->endtime > 0) && (this->queue[i]->last_time < last_time))
{
hit = i;
last_time = this->queue[i]->last_time;
}
}
if (hit != MAXQUEUE)
{
// ESP_LOGD(TAG, "find_oldest_queue_element: oldest screen is: %d", hit);
this->queue[hit]->status();
}
return hit;
}
void EHMTX::remove_expired_queue_element()
{
time_t ts = this->clock->now().timestamp;
std::string infotext;
for (size_t i = 0; i < MAXQUEUE; i++)
{
if ((this->queue[i]->endtime > 0) && (this->queue[i]->endtime < ts))
{
this->queue[i]->endtime = 0;
if ((this->queue[i]->mode == MODE_ICONSCREEN) || (this->queue[i]->mode == MODE_FULLSCREEN))
{
ESP_LOGD(TAG, "remove_expired_queue_element: removed slot %d: icon_name: %s text: %s", i, this->queue[i]->icon_name.c_str(), this->queue[i]->text.c_str());
for (auto *t : on_expired_screen_triggers_)
{
infotext = "";
switch (this->queue[i]->mode)
{
case MODE_EMPTY:
break;
case MODE_BLANK:
break;
case MODE_CLOCK:
infotext = "clock";
break;
case MODE_DATE:
infotext = "clock";
break;
case MODE_FULLSCREEN:
infotext = "fullscreen " + this->queue[i]->icon_name;
break;
case MODE_ICONSCREEN:
infotext = this->queue[i]->icon_name.c_str();
break;
case MODE_TEXT:
infotext = "TEXT";
break;
default:
break;
}
t->process(this->queue[i]->icon_name, infotext);
}
}
this->queue[i]->mode = MODE_EMPTY;
}
}
}
void EHMTX::tick()
{
if (this->is_running)
{
time_t ts = this->clock->now().timestamp;
if (ts > this->next_action_time)
{
this->remove_expired_queue_element();
this->screen_pointer = find_oldest_queue_element();
if (this->screen_pointer != MAXQUEUE)
{
this->queue[this->screen_pointer]->reset_shiftx();
this->queue[this->screen_pointer]->last_time = ts + this->queue[this->screen_pointer]->screen_time;
if (this->queue[this->screen_pointer]->icon < this->icon_count) {
this->icons[this->queue[this->screen_pointer]->icon]->set_frame(0);
}
this->next_action_time = this->queue[this->screen_pointer]->last_time;
// Todo switch for Triggers
if (this->queue[this->screen_pointer]->mode == MODE_CLOCK)
{
for (auto *t : on_next_clock_triggers_)
{
t->process();
}
}
else
{
for (auto *t : on_next_screen_triggers_)
{
t->process(this->queue[this->screen_pointer]->icon_name, this->queue[this->screen_pointer]->text);
}
}
}
else
{
ESP_LOGW(TAG, "tick: nothing to do. Restarting clock display!");
this->clock_screen(24 * 60, this->clock_time, false, C_RED, C_GREEN, C_BLUE);
this->date_screen(24 * 60, (int)this->clock_time / 2, false, C_RED, C_GREEN, C_BLUE);
this->next_action_time = ts + this->clock_time;
}
}
}
else {
uint8_t w = ((uint8_t) (32/14) * (this->boot_anim / 14)) % 32;
this->display->rectangle(0,1,w,6,Color(120,190,40));
this->boot_anim++;
}
}
void EHMTX::set_screen_time(uint16_t t)
{
ESP_LOGD(TAG, "default screen time: %d", t);
this->screen_time = t;
}
void EHMTX::skip_screen()
{
this->next_action_time = this->clock->now().timestamp - 1;
}
void EHMTX::hold_screen()
{
this->next_action_time += this->hold_time;
}
void EHMTX::get_status()
{
time_t ts = this->clock->now().timestamp;
ESP_LOGI(TAG, "status time: %d.%d.%d %02d:%02d", this->clock->now().day_of_month,
this->clock->now().month, this->clock->now().year,
this->clock->now().hour, this->clock->now().minute);
ESP_LOGI(TAG, "status brightness: %d (0..255)", this->brightness_);
ESP_LOGI(TAG, "status date format: %s", this->date_fmt.c_str());
ESP_LOGI(TAG, "status time format: %s", this->time_fmt.c_str());
ESP_LOGI(TAG, "status text_color: RGB(%d,%d,%d)", this->text_color.r, this->text_color.g, this->text_color.b);
ESP_LOGI(TAG, "status alarm_color: RGB(%d,%d,%d)", this->alarm_color.r, this->alarm_color.g, this->alarm_color.b);
if (this->display_indicator)
{
ESP_LOGI(TAG, "status indicator on");
}
else
{
ESP_LOGI(TAG, "status indicator off");
}
if (this->show_display)
{
ESP_LOGI(TAG, "status display on");
}
else
{
ESP_LOGI(TAG, "status display off");
}
this->queue_status();
}
void EHMTX::queue_status()
{
uint8_t empty = 0;
for (uint8_t i = 0; i < MAXQUEUE; i++)
{
if (this->queue[i]->mode != MODE_EMPTY)
this->queue[i]->status();
else
empty++;
}
if (empty > 0)
ESP_LOGI(TAG, "queue: %d empty slots", empty);
}
void EHMTX::set_default_font(display::Font * font)
{
this->default_font = font;
}
void EHMTX::set_special_font(display::Font * font)
{
this->special_font = font;
}
void EHMTX::set_frame_interval(uint16_t fi)
{
this->frame_interval = fi;
}
void EHMTX::set_scroll_interval(uint16_t si)
{
this->scroll_interval = si;
}
void EHMTX::del_screen(std::string icon_name,int mode)
{
for (uint8_t i = 0; i < MAXQUEUE; i++)
{
if (this->queue[i]->mode == mode)
{
bool force = true;
if ((mode == MODE_ICONSCREEN)||(mode == MODE_FULLSCREEN))
{
if (strcmp(this->queue[i]->icon_name.c_str(), icon_name.c_str()) != 0)
{
force = false;
}
}
if (force)
{
this->queue[i]->mode = MODE_EMPTY;
this->queue[i]->endtime = 0;
ESP_LOGW(TAG, "del_screen: icon %s in position: %d mode %d", icon_name.c_str(),i, mode);
if (i == this->screen_pointer){
this->next_action_time= this->clock->now().timestamp;
}
}
}
}
}
void EHMTX::icon_screen(std::string iconname, std::string text, int lifetime, int screen_time, bool default_font, int r, int g, int b)
{
uint8_t icon = this->find_icon(iconname.c_str());
if (icon >= this->icon_count)
{
ESP_LOGW(TAG, "icon %d not found => default: 0", icon);
icon = 0;
}
EHMTX_queue *screen = this->find_icon_queue_element(icon);
int x, y, w, h;
if (default_font)
{
this->display->get_text_bounds(0, 0, text.c_str(), this->default_font, display::TextAlign::LEFT, &x, &y, &w, &h);
}
else
{
this->display->get_text_bounds(0, 0, text.c_str(), this->special_font, display::TextAlign::LEFT, &x, &y, &w, &h);
}
screen->set_text(text, icon, w, lifetime, screen_time);
screen->text_color = Color(r, g, b);
screen->default_font = default_font;
screen->mode = MODE_ICONSCREEN;
screen->icon_name = iconname;
ESP_LOGD(TAG, "icon_screen icon: %d iconname: %s text: %s lifetime: %d screen_time: %d", icon, iconname.c_str(), text.c_str(), lifetime, screen_time);
screen->status();
}
void EHMTX::timer_screen(std::string icon_name, int seconds, int lifetime, int screen_time, bool default_font, int r, int g, int b)
{
uint8_t icon = this->find_icon(icon_name.c_str());
if (icon >= this->icon_count)
{
ESP_LOGW(TAG, "icon %d not found => default: 0", icon);
icon = 0;
}
EHMTX_queue *screen = this->find_icon_queue_element(icon);
screen->text = "00:00";
screen->icon = icon;
screen->text_color = Color(r, g, b);
screen->default_font = default_font;
screen->mode = MODE_TIMER;
screen->icon_name = icon_name;
screen->screen_time = screen_time;
screen->endtime = this->clock->now().timestamp + lifetime * 60;
ESP_LOGD(TAG, "TIMER_screen icon: %d icon_ name: %s seconds: %d lifetime: %d screen_time: %d", icon, icon_name.c_str(), seconds, lifetime, screen_time);
screen->status();
}
void EHMTX::text_screen(std::string text, int lifetime, int screen_time, bool default_font, int r, int g, int b)
{
EHMTX_queue *screen = this->find_free_queue_element();
int x, y, w, h;
if (default_font)
{
this->display->get_text_bounds(0, 0, text.c_str(), this->default_font, display::TextAlign::LEFT, &x, &y, &w, &h);
}
else
{
this->display->get_text_bounds(0, 0, text.c_str(), this->special_font, display::TextAlign::LEFT, &x, &y, &w, &h);
}
screen->text = text;
screen->pixels_ = w;
if (screen->pixels_ < 32)
{
screen->centerx_ = ceil((32 - screen->pixels_) / 2);
}
screen->shiftx_ = 0;
float display_duration = ceil((this->scroll_count * w * this->scroll_interval) / 1000);
screen->screen_time = (display_duration > screen_time) ? display_duration : screen_time;
ESP_LOGD(TAG, "text_screen text: text: %s pixels %d screen_time: %d lifetime: %d", text.c_str(), w, screen->screen_time, lifetime);
screen->endtime = this->clock->now().timestamp + lifetime * 60;
screen->text_color = Color(r, g, b);
screen->default_font = default_font;
screen->mode = MODE_TEXT;
screen->status();
}
void EHMTX::fullscreen(std::string iconname, int lifetime, int screen_time)
{
uint8_t icon = this->find_icon(iconname.c_str());
if (icon >= this->icon_count)
{
ESP_LOGW(TAG, "fullscreen: icon %d not found => default: 0", icon);
icon = 0;
}
EHMTX_queue *screen = this->find_icon_queue_element(icon);
screen->mode = MODE_FULLSCREEN;
screen->icon = icon;
screen->icon_name = iconname;
screen->screen_time = screen_time;
screen->endtime = this->clock->now().timestamp + lifetime * 60;
ESP_LOGD(TAG, "fullscreen: icon: %d iconname: %s lifetime: %d screen_time:%d ", icon, iconname.c_str(), lifetime, screen_time);
screen->status();
}
void EHMTX::clock_screen(int lifetime, int screen_time, bool default_font, int r, int g, int b)
{
EHMTX_queue *screen = this->find_free_queue_element();
screen->text_color = Color(r, g, b);
ESP_LOGD(TAG, "clock_screen_color lifetime: %d screen_time: %d red: %d green: %d blue: %d", lifetime, screen_time, r, g, b);
screen->mode = MODE_CLOCK;
screen->default_font = default_font;
screen->screen_time = screen_time;
screen->endtime = this->clock->now().timestamp + lifetime * 60;
screen->status();
}
void EHMTX::date_screen(int lifetime, int screen_time, bool default_font, int r, int g, int b)
{
EHMTX_queue *screen = this->find_free_queue_element();
screen->text_color = Color(r, g, b);
ESP_LOGD(TAG, "date_screen lifetime: %d screen_time: %d red: %d green: %d blue: %d", lifetime, screen_time, r, g, b);
screen->mode = MODE_DATE;
screen->screen_time = screen_time;
screen->default_font = default_font;
screen->endtime = this->clock->now().timestamp + lifetime * 60;
screen->status();
}
EHMTX_queue *EHMTX::find_icon_queue_element(uint8_t icon)
{
for (size_t i = 0; i < MAXQUEUE; i++)
{
if ((this->queue[i]->mode == MODE_ICONSCREEN) && (this->queue[i]->icon == icon))
{
ESP_LOGD(TAG, "free_screen: found by icon");
return this->queue[i];
}
}
return this->find_free_queue_element();
}
EHMTX_queue *EHMTX::find_free_queue_element()
{
time_t ts = this->clock->now().timestamp;
for (size_t i = 0; i < MAXQUEUE; i++)
{
if (this->queue[i]->endtime < ts)
{
ESP_LOGD(TAG, "free_screen: found by endtime %d", i);
return this->queue[i];
}
}
return this->queue[0];
}
void EHMTX::set_show_date(bool b)
{
this->show_date = b;
if (b)
{
ESP_LOGI(TAG, "show date");
}
else
{
ESP_LOGI(TAG, "don't show date");
}
}
void EHMTX::set_show_seconds(bool b)
{
this->show_seconds = b;
if (b)
{
ESP_LOGI(TAG, "show seconds");
}
else
{
ESP_LOGI(TAG, "don't show seconds");
}
}
void EHMTX::set_show_day_of_week(bool b)
{
this->show_day_of_week = b;
if (b)
{
ESP_LOGI(TAG, "show day of week");
}
else
{
ESP_LOGI(TAG, "don't show day of week");
}
}
void EHMTX::set_week_start(bool b)
{
this->week_starts_monday = b;
if (b)
{
ESP_LOGI(TAG, "weekstart: monday");
}
else
{
ESP_LOGI(TAG, "weekstart: sunday");
}
}
void EHMTX::set_brightness(int value)
{
if (value < 256)
{
this->brightness_ = value;
float br = (float)value / (float)255;
ESP_LOGI(TAG, "set_brightness %d => %.2f %%", value, 100 * br);
this->display->get_light()->set_correction(br, br, br);
}
}
uint8_t EHMTX::get_brightness()
{
return this->brightness_;
}
void EHMTX::set_clock_time(uint16_t t)
{
this->clock_time = t;
}
void EHMTX::set_hold_time(uint16_t t)
{
this->hold_time = t;
}
void EHMTX::set_scroll_count(uint8_t c)
{
this->scroll_count = c;
}
void EHMTX::set_display(addressable_light::AddressableLightDisplay * disp)
{
this->display = disp;
this->show_display = true;
}
void EHMTX::set_clock(time::RealTimeClock * clock)
{
this->clock = clock;
}
void EHMTX::draw_day_of_week()
{
if (this->show_day_of_week)
{
auto dow = this->clock->now().day_of_week - 1; // SUN = 0
for (uint8_t i = 0; i <= 6; i++)
{
if (((!this->week_starts_monday) && (dow == i)) ||
((this->week_starts_monday) && ((dow == (i + 1)) || ((dow == 0 && i == 6)))))
{
this->display->line(2 + i * 4, 7, i * 4 + 4, 7, this->today_color);
}
else
{
this->display->line(2 + i * 4, 7, i * 4 + 4, 7, this->weekday_color);
}
}
}
};
void EHMTX::set_default_font_offset(int8_t y, int8_t x)
{
this->default_xoffset = x;
this->default_yoffset = y;
ESP_LOGD(TAG, "set_default_font_offset x: %d y: %d", x, y);
}
void EHMTX::set_special_font_offset(int8_t y, int8_t x)
{
this->special_xoffset = x;
this->special_yoffset = y;
ESP_LOGD(TAG, "set_special_font_offset x: %d y: %d", x, y);
}
void EHMTX::dump_config()
{
ESP_LOGCONFIG(TAG, "EspHoMatriX %s", EHMTX_VERSION);
ESP_LOGCONFIG(TAG, "Boot anim: %d", this->boot_anim);
ESP_LOGCONFIG(TAG, "Icons: %d of %d", this->icon_count, MAXICONS);
ESP_LOGCONFIG(TAG, "Max screens: %d", MAXQUEUE);
ESP_LOGCONFIG(TAG, "Date format: %s", this->date_fmt.c_str());
ESP_LOGCONFIG(TAG, "Time format: %s", this->time_fmt.c_str());
ESP_LOGCONFIG(TAG, "Interval (ms) scroll: %d frame: %d", this->scroll_interval, this->frame_interval);
ESP_LOGCONFIG(TAG, "Displaytime (s) clock: %d screen: %d", this->clock_time, this->screen_time);
if (this->show_day_of_week)
{
ESP_LOGCONFIG(TAG, "show day of week");
}
if (this->show_date)
{
ESP_LOGCONFIG(TAG, "show date");
}
if (this->week_starts_monday)
{
ESP_LOGCONFIG(TAG, "weekstart: monday");
}
else
{
ESP_LOGCONFIG(TAG, "weekstart: sunday");
}
}
void EHMTX::add_icon(EHMTX_Icon * icon)
{
this->icons[this->icon_count] = icon;
ESP_LOGD(TAG, "add_icon no.: %d name: %s frame_duration: %d ms", this->icon_count, icon->name.c_str(), icon->frame_duration);
this->icon_count++;
}
void EHMTX::draw()
{
if ((this->is_running) && (this->show_display) && (this->screen_pointer != MAXQUEUE))
{
this->queue[this->screen_pointer]->draw();
this->draw_gauge();
if (this->display_indicator)
{
this->display->line(31, 5, 29, 7, this->indicator_color);
this->display->draw_pixel_at(30, 7, this->indicator_color);
this->display->draw_pixel_at(31, 6, this->indicator_color);
this->display->draw_pixel_at(31, 7, this->indicator_color);
}
}
}
void EHMTXNextScreenTrigger::process(std::string iconname, std::string text)
{
this->trigger(iconname, text);
}
void EHMTXExpiredScreenTrigger::process(std::string iconname, std::string text)
{
this->trigger(iconname, text);
}
void EHMTXNextClockTrigger::process()
{
this->trigger();
}
}

@ -0,0 +1,227 @@
#ifndef EHMTX_H
#define EHMTX_H
#include "esphome.h"
const uint8_t MAXQUEUE = 24;
const uint8_t C_RED = 240; //default
const uint8_t C_BLUE = 240;
const uint8_t C_GREEN = 240;
const uint8_t CD_RED = 100; // dim
const uint8_t CD_BLUE = 100;
const uint8_t CD_GREEN = 100;
const uint8_t CA_RED = 200; // alarm
const uint8_t CA_BLUE = 50;
const uint8_t CA_GREEN = 50;
const uint8_t D_LIFETIME = 5;
const uint8_t D_SCREEN_TIME = 10;
const uint8_t MAXICONS = 90;
const uint8_t TEXTSCROLLSTART = 8;
const uint8_t TEXTSTARTOFFSET = (32 - 8);
const uint16_t TICKINTERVAL = 1000; // each 1000ms
static const char *const EHMTX_VERSION = "Version: 2023.5.0";
static const char *const TAG = "EHMTX";
enum show_mode : uint8_t { MODE_EMPTY = 0,MODE_BLANK = 1, MODE_CLOCK = 2, MODE_DATE = 3, MODE_FULLSCREEN = 4, MODE_ICONSCREEN = 5, MODE_TEXT = 6 , MODE_TIMER = 7 };
namespace esphome
{
class EHMTX_queue;
class EHMTX_Icon;
class EHMTXNextScreenTrigger;
class EHMTXExpiredScreenTrigger;
class EHMTXNextClockTrigger;
class EHMTX : public PollingComponent, public api::CustomAPIDevice {
protected:
float get_setup_priority() const override { return esphome::setup_priority::WIFI; }
uint8_t brightness_;
uint32_t boot_anim=0;
uint8_t screen_pointer;
bool week_starts_monday;
bool show_day_of_week;
std::vector<EHMTXNextScreenTrigger *> on_next_screen_triggers_;
std::vector<EHMTXExpiredScreenTrigger *> on_expired_screen_triggers_;
std::vector<EHMTXNextClockTrigger *> on_next_clock_triggers_;
EHMTX_queue *find_icon_queue_element(uint8_t icon);
EHMTX_queue *find_free_queue_element();
public:
void setup() override;
EHMTX();
Color text_color, alarm_color, gauge_color,indicator_color,clock_color;
Color today_color,weekday_color;
void dump_config();
std::string time_fmt;
std::string date_fmt;
bool display_indicator;
bool display_alarm;
bool display_gauge;
bool is_running=false;
bool show_date;
uint8_t gauge_value;
uint16_t clock_time;
uint8_t scroll_count;
void remove_expired_queue_element();
uint8_t find_oldest_queue_element();
uint8_t find_icon_in_queue(std::string);
void force_screen(std::string name,int mode=MODE_ICONSCREEN);
EHMTX_Icon *icons[MAXICONS];
EHMTX_queue *queue[MAXQUEUE];
void add_icon(EHMTX_Icon *icon);
bool show_display=false;
addressable_light::AddressableLightDisplay *display;
time::RealTimeClock *clock;
display::Font *default_font;
display::Font *special_font;
int8_t default_yoffset, default_xoffset;
int8_t special_yoffset, special_xoffset;
uint8_t find_icon(std::string name);
bool string_has_ending(std::string const &fullString, std::string const &ending);
bool show_seconds;
uint16_t scroll_interval; // ms to between scrollsteps
uint16_t frame_interval; // ms to next_frame()
uint16_t hold_time; // seconds display of screen_time to extend
uint16_t screen_time; // seconds display of screen
uint8_t icon_count; // max iconnumber -1
unsigned long last_scroll_time;
unsigned long last_anim_time;
time_t next_action_time = 0; // when is the next screen change
void draw_day_of_week();
void show_all_icons();
void tick();
void Ntick();
void draw();
void get_status();
void queue_status();
void skip_screen();
void hold_screen();
void set_display(addressable_light::AddressableLightDisplay *disp);
void set_screen_time(uint16_t t);
void set_hold_time(uint16_t t);
void set_clock_time(uint16_t t);
void set_show_day_of_week(bool b);
void set_show_seconds(bool b);
void set_show_date(bool b);
void set_font_offset(int8_t x, int8_t y);
void set_week_start(bool b);
void set_brightness(int b);
void set_default_font_offset(int8_t x, int8_t y);
void set_special_font_offset(int8_t x, int8_t y);
void set_display_on();
void set_display_off();
void set_clock(time::RealTimeClock *clock);
void set_default_font(display::Font *font);
void set_special_font(display::Font *font);
void set_frame_interval(uint16_t interval);
void set_scroll_interval(uint16_t interval);
void set_scroll_count(uint8_t count);
void set_time_format(std::string s);
void set_date_format(std::string s);
void show_indicator(int r=C_RED, int g=C_GREEN, int b=C_BLUE);
void set_text_color(int r, int g, int b);
void set_clock_color(int r=C_RED, int g=C_GREEN, int b=C_BLUE);
void set_today_color(int r, int g, int b);
void set_weekday_color(int r, int g, int b);
void show_alarm(int r=C_RED, int g=C_GREEN, int b=C_BLUE);
void show_gauge(int v,int r=C_RED, int g=C_GREEN, int b=C_BLUE); // int because of register_service
void hide_gauge();
void hide_indicator();
void hide_alarm();
void fullscreen(std::string icon, int lifetime=D_LIFETIME, int screen_time=D_SCREEN_TIME);
void icon_screen(std::string icon, std::string text, int lifetime=D_LIFETIME, int screen_time=D_SCREEN_TIME,bool default_font=true,int r=C_RED, int g=C_GREEN, int b=C_BLUE);
void timer_screen(std::string icon, int seconds, int lifetime=D_LIFETIME, int screen_time=D_SCREEN_TIME, bool default_font=true, int r=C_RED, int g=C_GREEN, int b=C_BLUE);
void text_screen(std::string text, int lifetime=D_LIFETIME, int screen_time=D_SCREEN_TIME, bool default_font=true, int r=C_RED, int g=C_GREEN, int b=C_BLUE);
void clock_screen(int lifetime=D_LIFETIME, int screen_time=D_SCREEN_TIME,bool default_font=true,int r=C_RED, int g=C_GREEN, int b=C_BLUE);
void date_screen(int lifetime=D_LIFETIME, int screen_time=D_SCREEN_TIME,bool default_font=true, int r=C_RED, int g=C_GREEN, int b=C_BLUE);
void blank_screen(int lifetime=D_LIFETIME, int screen_time=D_SCREEN_TIME);
void del_screen(std::string icon, int mode=MODE_ICONSCREEN);
void draw_gauge();
void add_on_next_screen_trigger(EHMTXNextScreenTrigger *t) { this->on_next_screen_triggers_.push_back(t); }
void add_on_expired_screen_trigger(EHMTXExpiredScreenTrigger *t) { this->on_expired_screen_triggers_.push_back(t); }
void add_on_next_clock_trigger(EHMTXNextClockTrigger *t) { this->on_next_clock_triggers_.push_back(t); }
void update();
uint8_t get_brightness();
};
class EHMTX_queue
{
protected:
EHMTX *config_;
public:
uint8_t centerx_;
uint16_t shiftx_;
uint16_t pixels_;
uint16_t screen_time;
bool default_font;
time_t endtime;
time_t last_time;
uint8_t icon;
Color text_color;
show_mode mode;
std::string text;
std::string icon_name;
EHMTX_queue(EHMTX *config);
void status();
void draw();
void draw_();
bool isfree();
void reset_shiftx();
bool update_slot(uint8_t _icon);
void update_screen();
bool del_slot(uint8_t _icon);
void hold_slot(uint8_t _sec);
void set_text(std::string text, uint8_t icon, uint16_t pixel, uint16_t et, uint16_t st);
void set_text_color(uint8_t icon_id,Color text_color);
};
class EHMTXNextScreenTrigger : public Trigger<std::string, std::string>
{
public:
explicit EHMTXNextScreenTrigger(EHMTX *parent) { parent->add_on_next_screen_trigger(this); }
void process(std::string, std::string);
};
class EHMTXExpiredScreenTrigger : public Trigger<std::string, std::string>
{
public:
explicit EHMTXExpiredScreenTrigger(EHMTX *parent) { parent->add_on_expired_screen_trigger(this); }
void process(std::string, std::string);
};
class EHMTXNextClockTrigger : public Trigger<>
{
public:
explicit EHMTXNextClockTrigger(EHMTX *parent) { parent->add_on_next_clock_trigger(this); }
void process();
};
class EHMTX_Icon : public display::Animation
{
protected:
bool counting_up;
public:
EHMTX_Icon(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, display::ImageType type, std::string icon_name, bool revers, uint16_t frame_duration);
std::string name;
uint16_t frame_duration;
void next_frame();
bool reverse;
};
}
#endif

@ -0,0 +1,37 @@
#include "esphome.h"
namespace esphome
{
EHMTX_Icon::EHMTX_Icon(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, display::ImageType type, std::string icon_name, bool revers, uint16_t frame_duration)
: Animation(data_start, width, height, animation_frame_count, type)
{
this->name = icon_name;
this->reverse = revers;
this->frame_duration = frame_duration;
this->counting_up = true;
}
void EHMTX_Icon::next_frame()
{
if (this->get_animation_frame_count() > 1)
{
if (this->counting_up)
{
if (this->reverse && (this->get_current_frame() == this->get_animation_frame_count() - 2))
{
this->counting_up = false;
}
Animation::next_frame();
}
else
{
if (this->get_current_frame() == 1) // this->get_animation_frame_count())
{
this->counting_up = true;
}
Animation::prev_frame();
}
}
}
}

@ -0,0 +1,250 @@
#include "esphome.h"
namespace esphome
{
EHMTX_queue::EHMTX_queue(EHMTX *config)
{
this->config_ = config;
this->endtime = 0;
this->last_time = 0;
this->centerx_ = 0;
this->shiftx_ = 0;
this->screen_time = 0;
this->mode = MODE_EMPTY;
this->icon_name = "";
this->text = "";
this->default_font = true;
}
void EHMTX_queue::status()
{
switch (this->mode)
{
case MODE_EMPTY:
ESP_LOGD(TAG, "queue: empty slot");
break;
case MODE_BLANK:
ESP_LOGD(TAG, "queue: show empty screen");
break;
case MODE_CLOCK:
ESP_LOGD(TAG, "queue: show clock for %d sec", this->screen_time);
break;
case MODE_DATE:
ESP_LOGD(TAG, "queue: show date for %d sec", this->screen_time);
break;
case MODE_FULLSCREEN:
ESP_LOGD(TAG, "queue: show fullscreen: %s for %d sec", this->icon_name.c_str(), this->screen_time);
break;
case MODE_ICONSCREEN:
ESP_LOGD(TAG, "queue: show icon screen: %s text: %s for %d sec", this->icon_name.c_str(), this->text.c_str(), this->screen_time);
break;
case MODE_TEXT:
ESP_LOGD(TAG, "queue: show text text: %s for %d sec", this->text.c_str(), this->screen_time);
break;
case MODE_TIMER:
ESP_LOGD(TAG, "queue: show timer text: %s for %d sec", this->text.c_str(), this->screen_time);
break;
default:
ESP_LOGD(TAG, "queue: UPPS");
break;
}
}
bool EHMTX_queue::del_slot(uint8_t _icon)
{
if (this->icon == _icon)
{
this->endtime = 0;
ESP_LOGW(TAG, "delete screen icon: %d", _icon);
return true;
}
return false;
}
void EHMTX_queue::reset_shiftx()
{
this->shiftx_ = 0;
}
void EHMTX_queue::update_screen()
{
if (this->mode == MODE_ICONSCREEN)
{
if (millis() - this->config_->last_scroll_time >= this->config_->scroll_interval && this->pixels_ > TEXTSTARTOFFSET)
{
this->shiftx_++;
if (this->shiftx_ > this->pixels_ + TEXTSTARTOFFSET)
{
this->shiftx_ = 0;
}
this->config_->last_scroll_time = millis();
}
}
if (this->mode == MODE_TEXT)
{
if (millis() - this->config_->last_scroll_time >= this->config_->scroll_interval && this->pixels_ >= 32)
{
this->shiftx_++;
if (this->shiftx_ > this->pixels_ + 32)
{
this->shiftx_ = 0;
}
this->config_->last_scroll_time = millis();
}
}
if (millis() - this->config_->last_anim_time >= this->config_->icons[this->icon]->frame_duration)
{
this->config_->icons[this->icon]->next_frame();
this->config_->last_anim_time = millis();
}
}
void EHMTX_queue::draw_()
{
}
void EHMTX_queue::draw()
{
display::Font *font = this->default_font ? this->config_->default_font : this->config_->special_font;
int8_t yoffset = this->default_font ? this->config_->default_xoffset : this->config_->special_xoffset;
int8_t xoffset = this->default_font ? this->config_->default_yoffset : this->config_->special_yoffset;
int8_t extraoffset = 0;
switch (this->mode)
{
case MODE_EMPTY:
break;
case MODE_BLANK:
break;
case MODE_CLOCK:
if (this->config_->clock->now().timestamp > 6000) // valid time
{
time_t ts = this->config_->clock->now().timestamp;
this->config_->display->strftime(xoffset + 15, yoffset, font, this->text_color, display::TextAlign::BASELINE_CENTER, this->config_->time_fmt.c_str(),
this->config_->clock->now());
if ((this->config_->clock->now().second % 2 == 0) && this->config_->show_seconds)
{
this->config_->display->draw_pixel_at(0, 0, this->config_->clock_color);
}
this->config_->draw_day_of_week();
}
else
{
this->config_->display->print(15+xoffset, yoffset, font, this->config_->alarm_color, display::TextAlign::BASELINE_CENTER, "!t!");
}
break;
case MODE_DATE:
if (this->config_->clock->now().timestamp > 6000) // valid time
{
time_t ts = this->config_->clock->now().timestamp;
this->config_->display->strftime(xoffset + 15, yoffset, font, this->text_color, display::TextAlign::BASELINE_CENTER, this->config_->date_fmt.c_str(),
this->config_->clock->now());
if ((this->config_->clock->now().second % 2 == 0) && this->config_->show_seconds)
{
this->config_->display->draw_pixel_at(0, 0, this->config_->clock_color);
}
this->config_->draw_day_of_week();
}
else
{
this->config_->display->print(xoffset + 15, yoffset, font, this->config_->alarm_color, display::TextAlign::BASELINE_CENTER, "!d!");
}
break;
case MODE_FULLSCREEN:
this->config_->display->image(0, 0, this->config_->icons[this->icon]);
break;
case MODE_ICONSCREEN:
{
if (this->pixels_ > TEXTSTARTOFFSET)
{
extraoffset = TEXTSTARTOFFSET;
}
if (this->config_->display_gauge)
{
extraoffset += 2;
}
this->config_->display->print(this->centerx_ + TEXTSCROLLSTART - this->shiftx_ + extraoffset + xoffset, yoffset, font, this->text_color, esphome::display::TextAlign::BASELINE_LEFT,
this->text.c_str());
if (this->config_->display_alarm)
{
this->config_->display->draw_pixel_at(30, 0, this->config_->alarm_color);
this->config_->display->draw_pixel_at(31, 1, this->config_->alarm_color);
this->config_->display->draw_pixel_at(31, 0, this->config_->alarm_color);
}
if (this->config_->display_gauge)
{
this->config_->draw_gauge();
this->config_->display->image(2, 0, this->config_->icons[this->icon]);
this->config_->display->line(10, 0, 10, 7, esphome::display::COLOR_OFF);
}
else
{
this->config_->display->line(8, 0, 8, 7, esphome::display::COLOR_OFF);
this->config_->display->image(0, 0, this->config_->icons[this->icon]);
}
}
break;
case MODE_TEXT:
if (this->pixels_ > 32)
{
extraoffset = 32;
}
if (this->config_->display_gauge)
{
extraoffset += 2;
}
this->config_->display->print(this->centerx_ - this->shiftx_ + xoffset + extraoffset, yoffset, font, this->text_color, esphome::display::TextAlign::BASELINE_LEFT,
this->text.c_str());
break;
case MODE_TIMER:
this->config_->display->image(0, 0, this->config_->icons[this->icon]);
this->config_->display->print(9+xoffset, yoffset, font, this->text_color, esphome::display::TextAlign::BASELINE_LEFT,
this->text.c_str());
break;
default:
break;
}
this->update_screen();
}
void EHMTX_queue::hold_slot(uint8_t _sec)
{
this->endtime += _sec;
ESP_LOGD(TAG, "hold for %d secs", _sec);
}
// TODO void EHMTX_queue::set_mode_icon()
void EHMTX_queue::set_text(std::string text, uint8_t icon, uint16_t pixel, uint16_t et, uint16_t screen_time)
{
this->text = text;
this->pixels_ = pixel;
if (pixel < 23)
{
this->centerx_ = ceil((22 - pixel) / 2);
}
this->shiftx_ = 0;
float display_duration = ceil((this->config_->scroll_count * (TEXTSTARTOFFSET + pixel) * this->config_->scroll_interval) / 1000);
this->screen_time = (display_duration > screen_time) ? display_duration : screen_time;
ESP_LOGD(TAG, "display text: %s pixels %d calculated: %d screen_time: %d default: %d", text.c_str(), pixel, this->screen_time, screen_time, this->config_->screen_time);
this->endtime = this->config_->clock->now().timestamp + et * 60;
this->icon = icon;
}
void EHMTX_queue::set_text_color(uint8_t icon_id, Color text_color)
{
if (this->icon == icon_id)
{
this->text_color = text_color;
}
}
}

@ -0,0 +1,412 @@
from argparse import Namespace
import logging
import io
import requests
from esphome import core, automation
from esphome.components import display, font, time
import esphome.components.image as espImage
import esphome.config_validation as cv
import esphome.codegen as cg
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
from esphome.core import CORE, HexInt
from esphome.cpp_generator import RawExpression
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ["display", "light", "api"]
AUTO_LOAD = ["ehmtx"]
IMAGE_TYPE_RGB565 = 4
MAXFRAMES = 110
MAXICONS = 90
ICONWIDTH = 8
ICONHEIGHT = 8
ICONBUFFERSIZE = ICONWIDTH * ICONHEIGHT * 4
SVG_ICONSTART = '<svg width="80px" height="80px" viewBox="0 0 80 80">'
SVG_FULLSCREENSTART = '<svg width="320px" height="80px" viewBox="0 0 320 80">'
SVG_END = "</svg>"
logging.warning(f"")
logging.warning(f"If you are upgrading EsphoMaTrix from a version before 2023.5.0,")
logging.warning(f"you should read the section https://github.com/lubeda/EsphoMaTrix/#how-to-update for tipps.")
logging.warning(f"")
def rgb565_svg(x,y,r,g,b):
return f"<rect style=\"fill:rgb({(r << 3) | (r >> 2)},{(g << 2) | (g >> 4)},{(b << 3) | (b >> 2)});\" x=\"{x*10}\" y=\"{y*10}\" width=\"10\" height=\"10\"/>"
ehmtx_ns = cg.esphome_ns.namespace("esphome")
EHMTX_ = ehmtx_ns.class_("EHMTX", cg.Component)
Icons_ = ehmtx_ns.class_("EHMTX_Icon")
NextScreenTrigger = ehmtx_ns.class_(
"EHMTXNextScreenTrigger", automation.Trigger.template(cg.std_string)
)
ExpiredScreenTrigger = ehmtx_ns.class_(
"EHMTXExpiredScreenTrigger", automation.Trigger.template(cg.std_string)
)
NextClockTrigger = ehmtx_ns.class_(
"EHMTXNextClockTrigger", automation.Trigger.template(cg.std_string)
)
CONF_CLOCKTIME = "clock_time"
CONF_SCREENTIME = "screen_time"
CONF_EHMTX = "ehmtx"
CONF_URL = "url"
CONF_FLAG = "flag"
CONF_TIMECOMPONENT = "time_component"
CONF_LAMEID = "lameid"
CONF_LIFETIME = "lifetime"
CONF_ICONS = "icons"
CONF_SHOWDOW = "show_dow"
CONF_SHOWDATE = "show_date"
CONF_FRAMEDURATION = "frame_duration"
CONF_HOLD_TIME = "hold_time"
CONF_SCROLLCOUNT = "scroll_count"
CONF_MATRIXCOMPONENT = "matrix_component"
CONF_HTML = "icons2html"
CONF_SCROLLINTERVAL = "scroll_interval"
CONF_FRAMEINTERVAL = "frame_interval"
CONF_DEFAULT_FONT_ID = "default_font_id"
CONF_DEFAULT_FONT = "default_font"
CONF_DEFAULT_FONT_XOFFSET = "default_font_xoffset"
CONF_DEFAULT_FONT_YOFFSET = "default_font_yoffset"
CONF_special_FONT_ID = "special_font_id"
CONF_special_FONT_XOFFSET = "special_font_xoffset"
CONF_special_FONT_YOFFSET = "special_font_yoffset"
CONF_PINGPONG = "pingpong"
CONF_TIME_FORMAT = "time_format"
CONF_DATE_FORMAT = "date_format"
CONF_ON_NEXT_SCREEN = "on_next_screen"
CONF_ON_NEXT_CLOCK = "on_next_clock"
CONF_ON_EXPIRED_SCREEN= "on_expired_screen"
CONF_SHOW_SECONDS = "show_seconds"
CONF_WEEK_START_MONDAY = "week_start_monday"
CONF_ICON = "icon_name"
CONF_TEXT = "text"
CONF_ALARM = "alarm"
EHMTX_SCHEMA = cv.Schema({
cv.Required(CONF_ID): cv.declare_id(EHMTX_),
cv.Required(CONF_TIMECOMPONENT): cv.use_id(time),
cv.Required(CONF_MATRIXCOMPONENT): cv.use_id(display),
cv.Required(CONF_DEFAULT_FONT_ID): cv.use_id(font),
cv.Required(CONF_special_FONT_ID): cv.use_id(font),
cv.Optional(
CONF_CLOCKTIME, default="5"
): cv.templatable(cv.positive_int),
cv.Optional(
CONF_HTML, default=False
): cv.boolean,
cv.Optional(
CONF_SHOW_SECONDS, default=False
): cv.boolean,
cv.Optional(
CONF_SHOWDATE, default=True
): cv.boolean,
cv.Optional(
CONF_WEEK_START_MONDAY, default=True
): cv.boolean,
cv.Optional(
CONF_SHOWDOW, default=True
): cv.boolean,
cv.Optional(
CONF_SHOWDATE, default=True
): cv.boolean,
cv.Optional(
CONF_TIME_FORMAT, default="%H:%M"
): cv.string,
cv.Optional(
CONF_DATE_FORMAT, default="%d.%m."
): cv.string,
cv.Optional(
CONF_DEFAULT_FONT_XOFFSET, default="1"
): cv.templatable(cv.int_range(min=-32, max=32)),
cv.Optional(
CONF_DEFAULT_FONT_YOFFSET, default="6"
): cv.templatable(cv.int_range(min=-32, max=32)),
cv.Optional(
CONF_special_FONT_XOFFSET, default="1"
): cv.templatable(cv.int_range(min=-32, max=32)),
cv.Optional(
CONF_special_FONT_YOFFSET, default="6"
): cv.templatable(cv.int_range(min=-32, max=32)),
cv.Optional(
CONF_HOLD_TIME, default="20"
): cv.templatable(cv.int_range(min=0, max=3600)),
cv.Optional(CONF_SCROLLINTERVAL, default="80"
): cv.templatable(cv.positive_int),
cv.Optional(CONF_SCROLLCOUNT, default="2"
): cv.templatable(cv.positive_int),
cv.Optional(
CONF_FRAMEINTERVAL, default="192"
): cv.templatable(cv.positive_int),
cv.Optional(
CONF_SCREENTIME, default="8"
): cv.templatable(cv.positive_int),
cv.Optional(CONF_BRIGHTNESS, default=80): cv.templatable(cv.int_range(min=0, max=255)),
cv.Optional(CONF_ON_NEXT_SCREEN): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(NextScreenTrigger),
}
),
cv.Optional(CONF_ON_EXPIRED_SCREEN): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(NextScreenTrigger),
}
),
cv.Optional(CONF_ON_NEXT_CLOCK): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(NextClockTrigger),
}
),
cv.Optional(CONF_ON_EXPIRED_SCREEN): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ExpiredScreenTrigger),
}
),
cv.Required(CONF_ICONS): cv.All(
cv.ensure_list(
{
cv.Required(CONF_ID): cv.declare_id(Icons_),
cv.Exclusive(CONF_FILE,"uri"): cv.file_,
cv.Exclusive(CONF_URL,"uri"): cv.url,
cv.Exclusive(CONF_LAMEID,"uri"): cv.string,
cv.Optional(
CONF_FRAMEDURATION, default="0"
): cv.templatable(cv.positive_int),
cv.Optional(
CONF_PINGPONG, default=False
): cv.boolean,
cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8),
}
),
cv.Length(max=MAXICONS),
)})
CONFIG_SCHEMA = cv.All(font.validate_pillow_installed, EHMTX_SCHEMA)
SET_COLOR_ACTION_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.use_id(EHMTX_),
cv.Optional(CONF_RED,default="80"): cv.templatable(cv.uint8_t),
cv.Optional(CONF_BLUE,default="80"): cv.templatable(cv.uint8_t),
cv.Optional(CONF_GREEN,default="80"): cv.templatable(cv.uint8_t),
}
)
SetTodayColorAction = ehmtx_ns.class_("SetTodayColor", automation.Action)
@automation.register_action(
"ehmtx.today.color", SetTodayColorAction, SET_COLOR_ACTION_SCHEMA
)
async def ehmtx_set_today_color_action_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
template_ = await cg.templatable(config[CONF_RED], args, cg.int_)
cg.add(var.set_red(template_))
template_ = await cg.templatable(config[CONF_GREEN], args, cg.int_)
cg.add(var.set_green(template_))
template_ = await cg.templatable(config[CONF_BLUE], args, cg.int_)
cg.add(var.set_blue(template_))
return var
SetWeekdayColorAction = ehmtx_ns.class_("SetWeekdayColor", automation.Action)
@automation.register_action(
"ehmtx.weekday.color", SetWeekdayColorAction, SET_COLOR_ACTION_SCHEMA
)
async def ehmtx_set_week_color_action_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
template_ = await cg.templatable(config[CONF_RED], args, cg.int_)
cg.add(var.set_red(template_))
template_ = await cg.templatable(config[CONF_GREEN], args, cg.int_)
cg.add(var.set_green(template_))
template_ = await cg.templatable(config[CONF_BLUE], args, cg.int_)
cg.add(var.set_blue(template_))
return var
CODEOWNERS = ["@lubeda"]
async def to_code(config):
from PIL import Image, ImageSequence
def openImageFile(path):
try:
return Image.open(path)
except Exception as e:
raise core.EsphomeError(f" ICONS: Could not load image file {path}: {e}")
def thumbnails(frames):
for frame in frames:
thumbnail = frame.copy()
thumbnail.thumbnail((32,8), Image.ANTIALIAS)
yield thumbnail
var = cg.new_Pvariable(config[CONF_ID])
logging.info(f"Preparing icons, this may take some seconds.")
html_string = F"<HTML><HEAD><TITLE>{CORE.config_path}</TITLE></HEAD>"
html_string += '''\
<STYLE>
svg { padding-top: 2x; padding-right: 2px; padding-bottom: 2px; padding-left: 2px; }
</STYLE><BODY>\
'''
yaml_string= ""
for conf in config[CONF_ICONS]:
if CONF_FILE in conf:
path = CORE.relative_config_path(conf[CONF_FILE])
try:
image = openImageFile(path)
except Exception as e:
raise core.EsphomeError(f" ICONS: Could not load image file {path}: {e}")
elif CONF_LAMEID in conf:
r = requests.get("https://developer.lametric.com/content/apps/icon_thumbs/" + conf[CONF_LAMEID], timeout=4.0)
if r.status_code != requests.codes.ok:
raise core.EsphomeError(f" ICONS: Could not download image file {conf[CONF_LAMEID]}: {conf[CONF_ID]}")
image = Image.open(io.BytesIO(r.content))
elif CONF_URL in conf:
r = requests.get(conf[CONF_URL], timeout=4.0)
if r.status_code != requests.codes.ok:
raise core.EsphomeError(f" ICONS: Could not download image file {conf[CONF_URL]}: {conf[CONF_ID]}")
image = Image.open(io.BytesIO(r.content))
width, height = image.size
if hasattr(image, 'n_frames'):
frames = min(image.n_frames, MAXFRAMES)
else:
frames = 1
if ((width != 4*ICONWIDTH) or (width != ICONWIDTH)) and (height != ICONHEIGHT):
logging.warning(f" icon wrong size valid 8x8 or 8x32: {conf[CONF_ID]} skipped!")
else:
if (conf[CONF_FRAMEDURATION] == 0):
try:
duration = image.info['duration']
except:
duration = config[CONF_FRAMEINTERVAL]
else:
duration = conf[CONF_FRAMEDURATION]
html_string += F"<BR><B>{conf[CONF_ID]}</B>&nbsp;-&nbsp;({duration} ms):<BR>"
yaml_string += F"\"{conf[CONF_ID]}\","
pos = 0
frameIndex = 0
html_string += f"<DIV ID={conf[CONF_ID]}>"
data = [0 for _ in range(ICONBUFFERSIZE * 2 * frames)]
for frameIndex in range(frames):
image.seek(frameIndex)
frame = image.convert("RGB")
pixels = list(frame.getdata())
width, height = image.size
if width == 8:
html_string += SVG_ICONSTART
else:
html_string += SVG_FULLSCREENSTART
i = 0
for pix in pixels:
R = pix[0] >> 3
G = pix[1] >> 2
B = pix[2] >> 3
x = (i % width)
y = i//width
i +=1
rgb = (R << 11) | (G << 5) | B
html_string += rgb565_svg(x,y,R,G,B)
data[pos] = rgb >> 8
pos += 1
data[pos] = rgb & 255
pos += 1
html_string += SVG_END
html_string += f"</DIV>"
rhs = [HexInt(x) for x in data]
prog_arr = cg.progmem_array(conf[CONF_RAW_DATA_ID], rhs)
cg.new_Pvariable(
conf[CONF_ID],
prog_arr,
width,
height,
frames,
espImage.IMAGE_TYPE["RGB565"],
str(conf[CONF_ID]),
conf[CONF_PINGPONG],
duration,
)
cg.add(var.add_icon(RawExpression(str(conf[CONF_ID]))))
html_string += "</BODY></HTML>"
if config[CONF_HTML]:
try:
htmlfn = CORE.config_path.replace(".yaml","") + ".html"
with open(htmlfn, 'w') as f:
f.truncate()
f.write(html_string)
f.close()
logging.info(f"EsphoMaTrix: wrote html-file with icon preview: {htmlfn}")
except:
logging.warning(f"EsphoMaTrix: Error writing HTML file: {htmlfn}")
logging.info("List of icons for e.g. blueprint:\n\n\r["+yaml_string+"]\n")
disp = await cg.get_variable(config[CONF_MATRIXCOMPONENT])
cg.add(var.set_display(disp))
f = await cg.get_variable(config[CONF_DEFAULT_FONT_ID])
cg.add(var.set_default_font(f))
f = await cg.get_variable(config[CONF_special_FONT_ID])
cg.add(var.set_special_font(f))
ehmtxtime = await cg.get_variable(config[CONF_TIMECOMPONENT])
cg.add(var.set_clock(ehmtxtime))
cg.add(var.set_clock_time(config[CONF_CLOCKTIME]))
cg.add(var.set_brightness(config[CONF_BRIGHTNESS]))
cg.add(var.set_screen_time(config[CONF_SCREENTIME]))
cg.add(var.set_scroll_interval(config[CONF_SCROLLINTERVAL]))
cg.add(var.set_scroll_count(config[CONF_SCROLLCOUNT]))
cg.add(var.set_frame_interval(config[CONF_FRAMEINTERVAL]))
cg.add(var.set_week_start(config[CONF_WEEK_START_MONDAY]))
cg.add(var.set_time_format(config[CONF_TIME_FORMAT]))
cg.add(var.set_date_format(config[CONF_DATE_FORMAT]))
cg.add(var.set_show_day_of_week(config[CONF_SHOWDOW]))
cg.add(var.set_hold_time(config[CONF_HOLD_TIME]))
cg.add(var.set_show_date(config[CONF_SHOWDATE]))
cg.add(var.set_show_seconds(config[CONF_SHOW_SECONDS]))
cg.add(var.set_default_font_offset(config[CONF_DEFAULT_FONT_XOFFSET], config[CONF_DEFAULT_FONT_YOFFSET] ))
cg.add(var.set_special_font_offset(config[CONF_special_FONT_XOFFSET], config[CONF_special_FONT_YOFFSET] ))
for conf in config.get(CONF_ON_NEXT_SCREEN, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [(cg.std_string, "x"), (cg.std_string, "y")], conf)
for conf in config.get(CONF_ON_NEXT_CLOCK, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
for conf in config.get(CONF_ON_EXPIRED_SCREEN, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [(cg.std_string, "x"), (cg.std_string, "y")] , conf)
await cg.register_component(var, config)

@ -0,0 +1,164 @@
# esphome config file with all features
substitutions:
devicename: ehmtx8266
ledpin: GPIO02
board: d1_mini
loglevel: DEBUG
external_components:
- source:
type: git
url: https://github.com/lubeda/EsphoMaTrix
esphome:
name: $devicename
on_boot:
priority: -100
then:
- ehmtx.text.color:
id: rgb8x32
red: !lambda return 200;
green: !lambda return 100;
blue: !lambda return 50;
- ehmtx.clock.color:
id: rgb8x32
red: !lambda return 150;
green: !lambda return 0;
blue: !lambda return 100;
- ehmtx.today.color:
id: rgb8x32
red: !lambda return 0;
green: !lambda return 100;
blue: !lambda return 0;
- ehmtx.weekday.color:
id: rgb8x32
red: !lambda return 0;
green: !lambda return 0;
blue: !lambda return 100;
- ehmtx.alarm.color:
id: rgb8x32
red: !lambda return 200;
green: !lambda return 150;
blue: !lambda return 30;
web_server:
port: 80
esp8266:
board: $board
font:
- file: monobit.ttf
id: ehmtx_font
size: 16
glyphs: |
!?"%()+*=,-_.:°0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz€@<>/
logger:
level: $loglevel
api:
services:
ota:
password: !secret ota_password
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
light:
- platform: neopixelbus
id: ehmtx_light
type: GRB
variant: WS2812
pin: $ledpin
num_leds: 256
color_correct: [30%, 30%, 30%]
name: "$devicename Light"
restore_mode: ALWAYS_OFF
on_turn_on:
lambda: |-
id(ehmtx_display)->set_enabled(false);
on_turn_off:
lambda: |-
id(ehmtx_display)->set_enabled(true);
time:
- platform: homeassistant
id: ehmtx_time
display:
- platform: addressable_light
id: ehmtx_display
addressable_light_id: ehmtx_light
width: 32
height: 8
pixel_mapper: |-
if (x % 2 == 0) {
return (x * 8) + y;
}
return (x * 8) + (7 - y);
rotation: 0°
update_interval: 16ms
auto_clear_enabled: true
lambda: |-
id(rgb8x32)->tick();
id(rgb8x32)->draw();
sensor:
- platform: uptime
name: Uptime Sensor
ehmtx:
id: rgb8x32
time_component: ehmtx_time
matrix_component: ehmtx_display
clock_time: 5 # seconds
screen_time: 8 # seconds
font_id: ehmtx_font
show_dow: true # day of week
icons2html: true # generate html with con overview
brightness: 80 # percent
time_format: "%H:%M"
date_format: "%d.%m."
week_start_monday: true
xoffset: 1
yoffset: 2
scroll_count: 2
scroll_interval: 80
frame_interval: 192
on_next_screen: # trigger on screen change
lambda: |-
ESP_LOGD("TriggerTest","Iconname: %s",x.c_str());
ESP_LOGI("TriggerTest","Text: %s",y.c_str());
on_next_clock: # trigger on clock display
then:
- ehmtx.clock.color:
id: rgb8x32
red: !lambda return 150;
green: !lambda return rand() % 255;
blue: !lambda return 100;
icons:
- id: xani
lameid: 6075
- id: xsta
lameid: 11236
- url: https://developer.lametric.com/content/apps/icon_thumbs/48720.gif
pingpong: true
id: pipo
frame_duration: 300
- lameid: 5965
frame_duration: 180
id: d180
- lameid: 5965
frame_duration: 80
id: d080
- id: fullscreen
file: sample8x32.gif
- id: samplegif
file: sample8x8.gif

Binary file not shown.

After

Width:  |  Height:  |  Size: 591 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 637 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 709 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 695 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 708 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 619 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

Loading…
Cancel
Save