├── fonts ├── README.md ├── TT0246M.ttf ├── GothamRnd-Bold.ttf ├── GothamRnd-Book.ttf └── materialdesignicons.ttf ├── eink_clock.yaml ├── eink_clock_with_temp.yaml ├── README.md ├── eink_clock_with_forecast.yaml ├── eink_clock_with_pages.yaml ├── README_en.md └── eink_clock_with_purifier_and_motion.yaml /fonts/README.md: -------------------------------------------------------------------------------- 1 | put these files in esphome/fonts 2 | -------------------------------------------------------------------------------- /fonts/TT0246M.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xangin/esphome_eink2.9_series/HEAD/fonts/TT0246M.ttf -------------------------------------------------------------------------------- /fonts/GothamRnd-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xangin/esphome_eink2.9_series/HEAD/fonts/GothamRnd-Bold.ttf -------------------------------------------------------------------------------- /fonts/GothamRnd-Book.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xangin/esphome_eink2.9_series/HEAD/fonts/GothamRnd-Book.ttf -------------------------------------------------------------------------------- /fonts/materialdesignicons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xangin/esphome_eink2.9_series/HEAD/fonts/materialdesignicons.ttf -------------------------------------------------------------------------------- /eink_clock.yaml: -------------------------------------------------------------------------------- 1 | substitutions: 2 | device_name: eink-clock 3 | 4 | esphome: 5 | name: eink-clock 6 | 7 | esp8266: 8 | board: d1_mini 9 | 10 | # Enable logging 11 | logger: 12 | 13 | # Enable Home Assistant API 14 | api: 15 | 16 | ota: 17 | 18 | wifi: 19 | ssid: !secret my_ap_ssid 20 | password: !secret my_ap_password 21 | 22 | # Enable fallback hotspot (captive portal) in case wifi connection fails 23 | ap: 24 | ssid: "eink-clock" 25 | password: "12345678" 26 | 27 | time: 28 | - platform: sntp 29 | id: sntp_time 30 | timezone: Asia/Taipei 31 | 32 | sensor: 33 | - platform: wifi_signal 34 | id: wifisignal 35 | update_interval: 60s 36 | 37 | font: 38 | - file: "fonts/GothamRnd-Bold.ttf" 39 | id: time_font 40 | size: 95 41 | glyphs: [0, 1, 2, 3, 4, 5, 6, 7 ,8, 9, ':'] 42 | 43 | - file: "fonts/GothamRnd-Book.ttf" 44 | id: time_font_book 45 | size: 95 46 | glyphs: [0, 1, 2, 3, 4, 5, 6, 7 ,8, 9, ':'] 47 | 48 | - file: "fonts/GothamRnd-Bold.ttf" 49 | id: weekday_font 50 | size: 35 51 | glyphs: &font-glyphs 52 | ['!', ',', '.', '"', '%', '-', '_', ':', '°', '/', 53 | '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ' ', 54 | 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 55 | 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 56 | 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 57 | 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 58 | 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'] 59 | 60 | - file: "fonts/GothamRnd-Book.ttf" 61 | id: date_font 62 | size: 32 63 | glyphs: *font-glyphs 64 | 65 | - file: 'fonts/materialdesignicons.ttf' 66 | id: icon_font 67 | size: 20 68 | glyphs: &mdi-weather-glyphs 69 | - "\U000F0783" # mdi-signal-off 70 | - "\U000F08BF" # mdi-signal-cellular-outline bad 71 | - "\U000F08BC" # mdi-signal-cellular-1 72 | - "\U000F08BD" # mdi-signal-cellular-2 73 | - "\U000F08BE" # mdi-signal-cellular-3 good 74 | 75 | spi: 76 | clk_pin: D5 77 | mosi_pin: D7 78 | 79 | display: 80 | - platform: waveshare_epaper 81 | cs_pin: D8 82 | dc_pin: D2 83 | busy_pin: D1 84 | reset_pin: D4 85 | model: 2.90in 86 | rotation: 270 87 | update_interval: 10s 88 | full_update_every: 600 89 | lambda: |- 90 | 91 | //wifi signal 92 | if (id(wifisignal).state >= -60) { 93 | //Excellent 94 | it.print(0, 0, id(icon_font), "\U000F08BE"); 95 | } else if (id(wifisignal).state >= -70) { 96 | //Good 97 | it.print(0, 0, id(icon_font), "\U000F08BD"); 98 | } else if (id(wifisignal).state >= -75) { 99 | //Fair 100 | it.print(0, 0, id(icon_font),"\U000F08BC"); 101 | } else if (id(wifisignal).state >= -85) { 102 | //Weak 103 | it.print(0, 0, id(icon_font),"\U000F08BF"); 104 | } else { 105 | //Unlikely working signal 106 | it.print(0, 0, id(icon_font),"\U000F0783"); 107 | } 108 | //Time: hour 109 | it.strftime(75,1, id(time_font_book), TextAlign::TOP_CENTER, "%H",id(sntp_time).now()); 110 | 111 | //Time: ":" 112 | it.printf(150,-8, id(time_font_book), TextAlign::TOP_CENTER, ":"); 113 | 114 | //Time: minutes 115 | it.strftime(225,1, id(time_font_book), TextAlign::TOP_CENTER, "%M",id(sntp_time).now()); 116 | 117 | //Time: date 118 | it.strftime(104,93, id(date_font), TextAlign::TOP_CENTER, "%Y/%m/%d",id(sntp_time).now()); 119 | 120 | //Weekday 121 | it.strftime(250,90, id(weekday_font), TextAlign::TOP_CENTER, "%a",id(sntp_time).now()); 122 | 123 | -------------------------------------------------------------------------------- /eink_clock_with_temp.yaml: -------------------------------------------------------------------------------- 1 | substitutions: 2 | device_name: eink-clock-temp 3 | temperature_entity_id: sensor.kids_room_temperature #replace with your temperature entity_id 4 | humidity_entity_id: sensor.kids_room_humidity #replace with your humidity entity_id 5 | 6 | esphome: 7 | name: eink-clock-temp 8 | 9 | esp8266: 10 | board: d1_mini 11 | 12 | # Enable logging 13 | logger: 14 | 15 | # Enable Home Assistant API 16 | api: 17 | 18 | ota: 19 | password: !secret my_ap_password 20 | 21 | wifi: 22 | ssid: !secret my_ap_ssid 23 | password: !secret my_ap_password 24 | 25 | # Enable fallback hotspot (captive portal) in case wifi connection fails 26 | ap: 27 | ssid: "eink-clock-temp" 28 | password: "12345678" 29 | 30 | captive_portal: 31 | 32 | time: 33 | - platform: homeassistant 34 | id: ha_time 35 | 36 | binary_sensor: 37 | - platform: status 38 | name: "${device_name} Status" 39 | id: system_status 40 | 41 | sensor: 42 | - platform: wifi_signal 43 | id: wifisignal 44 | name: "${device_name} WiFi Signal" 45 | update_interval: 60s 46 | 47 | - platform: homeassistant 48 | entity_id: ${temperature_entity_id} 49 | id: temp_data 50 | 51 | - platform: homeassistant 52 | entity_id: ${humidity_entity_id} 53 | id: hum_data 54 | 55 | 56 | font: 57 | - file: "fonts/GothamRnd-Book.ttf" 58 | id: time_font 59 | size: 80 60 | glyphs: [0, 1, 2, 3, 4, 5, 6, 7 ,8, 9, ':'] 61 | 62 | - file: "fonts/GothamRnd-Bold.ttf" 63 | id: temp_font 64 | size: 30 65 | glyphs: &font-glyphs 66 | ['!', ',', '.', '"', '%', '-', '_', ':', '°', '/', 67 | '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ' ', 68 | 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 69 | 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 70 | 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 71 | 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 72 | 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'] 73 | 74 | - file: "fonts/GothamRnd-Book.ttf" 75 | id: book_40 76 | size: 40 77 | glyphs: *font-glyphs 78 | 79 | - file: "fonts/GothamRnd-Bold.ttf" 80 | id: bold_40 81 | size: 40 82 | glyphs: *font-glyphs 83 | 84 | - file: "fonts/GothamRnd-Bold.ttf" 85 | id: bold_25 86 | size: 25 87 | glyphs: *font-glyphs 88 | 89 | - file: "fonts/GothamRnd-Book.ttf" 90 | id: date_font 91 | size: 30 92 | glyphs: *font-glyphs 93 | 94 | - file: "fonts/materialdesignicons.ttf" 95 | id: icon_font 96 | size: 30 97 | glyphs: 98 | - "\U000F058E" # mdi-water-percent 99 | - "\U000F050F" # mdi-thermometer 100 | 101 | 102 | spi: 103 | clk_pin: D5 104 | mosi_pin: D7 105 | 106 | display: 107 | - platform: waveshare_epaper 108 | id: my_display 109 | cs_pin: D8 110 | dc_pin: D2 111 | busy_pin: D1 112 | reset_pin: D4 113 | model: 2.90in 114 | rotation: 270 115 | update_interval: 10s 116 | full_update_every: 60 117 | lambda: |- 118 | //time 119 | it.strftime(75,1, id(time_font), TextAlign::TOP_CENTER, "%H",id(ha_time).now()); 120 | 121 | it.printf(150,-6, id(time_font), TextAlign::TOP_CENTER, ":"); 122 | 123 | it.strftime(225,1, id(time_font), TextAlign::TOP_CENTER, "%M",id(ha_time).now()); 124 | 125 | it.strftime(150, 70, id(date_font), TextAlign::TOP_CENTER, "%Y/%m/%d %a", id(ha_time).now()); 126 | 127 | //temp 128 | it.print(25, 97, id(icon_font),TextAlign::TOP_CENTER, "\U000F050F"); 129 | if(id(temp_data).has_state()){ 130 | it.printf(90,100, id(temp_font), TextAlign::TOP_CENTER, "%.1f°C", id(temp_data).state); 131 | } 132 | //hum 133 | it.print(185, 100, id(icon_font),TextAlign::TOP_CENTER, "\U000F058E"); 134 | 135 | if(id(hum_data).has_state()){ 136 | it.printf(240, 100, id(temp_font), TextAlign::TOP_CENTER, "%.1f%%", id(hum_data).state); 137 | } 138 | 139 | 140 | 141 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ESPhome E-ink 2.9" series sample 2 | 3 | Buy Me A Coffee 4 | 5 | 正體中文 | [English](https://github.com/xangin/esphome_eink2.9_series/blob/main/README_en.md) 6 | 7 | 本專案為利用2.9"電子紙搭配ESPhome與Home Assistant,可自由調整喜歡的版面 8 | 9 | 或是加上更多頁面並透過HA控制換頁,例如當衣服洗好時,會自動將畫面切為訊息提示 10 | 11 | 要有多少頁或是顯示什麼都能自由設計 12 | 13 | - 成品尺寸: 寬 88mm x 高 47mm 14 | 15 | 16 | 17 | - 成品背面 18 | 19 | 20 | 21 | 22 | # 範例參考: 23 | 24 | ## 範例1. eink_clock.yaml 25 | 26 | 單純時鐘僅顯示日期與時間 27 | 28 | 29 | 30 | 31 | ## 範例2. eink_clock_with_forecast.yaml 32 | 33 | 除了日期與時間外,再加上讀取來自HA的天氣預報圖示與氣溫 34 | 35 | 36 | 37 | 38 | ## 範例3. eink_clock_with_temp.yaml 39 | 40 | 除了日期與時間外,再加上讀取來自HA的某個溫度與濕度實體 41 | 42 | 43 | 44 | ## 範例4. eink_clock_with_pages.yaml 45 | 46 | 除了日期與時間外,還多了訊息頁面 47 | 48 | 49 | 50 | ## Hardware 硬體架構 51 | 52 | - [微雪 2.9吋黑白墨水屏裸屏](https://detail.tmall.com/item.htm?id=605757420567) - 不帶外殼 53 | - [墨水屏驅動板 ESP8266](https://oshwhub.com/lingdy2012/mo-shui-ping-_esp8266-qu-dong-ban-_0603_wos_v0-1) - 閑魚有售 54 | - [电子墨水屏外壳(EW029F2(2.9寸单电池))](https://item.taobao.com/item.htm?id=601700008521)- 外殼 55 | 56 | 57 | ## Installation 安裝方式 58 | 59 | 1. 將`/fonts`資料夾內的檔案放到HA/config/esphome的資料夾內 60 | 2. 將任一YAML放到HA/config/esphome,並將內容修改成自己想要的,編譯後插上USB即可燒錄至ESP模組 61 | 3. 有讀取HA資訊的必須要將ESPhome裝置加入HA後才正確顯示資訊 62 | 63 | ## ESPHome yaml 說明 64 | 65 | ### 在HA控制換頁 66 | 67 | 有2個按鈕,按下去分別會去顯示p1(Time Page)與p2(Message Page),如果有要再新增更多頁可以再仿照程式碼再新增 68 | 69 | ```YAML 70 | button: 71 | - platform: template 72 | name: "Show Time Page" 73 | icon: 'mdi:clock' 74 | on_press: 75 | then: 76 | - display.page.show: p1 77 | - component.update: my_display 78 | 79 | - platform: template 80 | name: "Show Message Page" 81 | icon: 'mdi:update' 82 | on_press: 83 | then: 84 | - display.page.show: p2 85 | - component.update: my_display 86 | ``` 87 | 88 | ### 根據Wi-Fi強度顯示圖示 89 | 90 | 說明: 91 | - 大於等於-60顯示三格 92 | - -60~-70顯示兩格 93 | - -70~-75顯示一格 94 | - -75~-85顯示零格 95 | - 小於-85顯示中斷 96 | 97 | 可自由變更強度範圍要顯示的格數 98 | 99 | ```YAML 100 | //wifi signal 101 | if (id(wifisignal).state >= -60) { 102 | //Excellent 103 | it.print(0, 0, id(wifi_font), "\U000F08BE"); 104 | } else if (id(wifisignal).state >= -70) { 105 | //Good 106 | it.print(0, 0, id(wifi_font), "\U000F08BD"); 107 | } else if (id(wifisignal).state >= -75) { 108 | //Fair 109 | it.print(0, 0, id(wifi_font),"\U000F08BC"); 110 | } else if (id(wifisignal).state >= -85) { 111 | //Weak 112 | it.print(0, 0, id(wifi_font),"\U000F08BF"); 113 | } else { 114 | //Unlikely working signal 115 | it.print(0, 0, id(wifi_font),"\U000F0783"); 116 | } 117 | ``` 118 | 119 | ### 修改自訂訊息內容 120 | 121 | 作法: 122 | 123 | 1. 在字型宣告處的`msg_font`將要顯示的中文字**先全部寫出來**這樣才能正常顯示唷!! 124 | ```YAML 125 | font: 126 | - file: "fonts/NotoSansTC-Medium.ttf" 127 | id: msg_font 128 | size: 40 129 | glyphs: 衣服已經洗拿去烘好囉!趕快收起來ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz,."%-~_:° 130 | ``` 131 | 2. 在`display`的p2,置換想要顯示的文字,依目前設定的大小,一行就是顯示`7個中文字`,超過將無法顯示唷! 132 | ```YAML 133 | display: 134 | ... 135 | - id: p2 136 | lambda: |- 137 | it.printf(150,15, id(msg_font), TextAlign::TOP_CENTER, "衣服已經洗好囉!"); 138 | it.printf(150,70, id(msg_font), TextAlign::TOP_CENTER, "趕快拿去烘~"); 139 | ``` 140 | 141 | ### 增加更多頁面 142 | 143 | 作法: 仿照按鈕及顯示的程式碼,可再新增多組,例如下面是第三頁,按下去顯示衣服烘好了 144 | 145 | 有不在`msg_font`內的中文字要記得新增,這樣才能正常顯示唷! 146 | 147 | ```YAML 148 | 149 | button: #複製在button程式碼的最下面,不可重複寫button唷! 150 | ... 151 | - platform: template 152 | name: "Show Dryer Done Page" 153 | icon: 'mdi:update' 154 | on_press: 155 | then: 156 | - display.page.show: p3 157 | - component.update: my_display 158 | 159 | 160 | display: #複製在display程式碼的最下面,不可重複寫display唷! 161 | ... 162 | - id: p3 163 | lambda: |- 164 | it.printf(150,15, id(msg_font), TextAlign::TOP_CENTER, "衣服已經烘好囉!"); 165 | it.printf(150,70, id(msg_font), TextAlign::TOP_CENTER, "趕快收起來!!"); 166 | ``` 167 | 168 | -------------------------------------------------------------------------------- /eink_clock_with_forecast.yaml: -------------------------------------------------------------------------------- 1 | substitutions: 2 | device_name: eink-clock-forecast 3 | 4 | esphome: 5 | name: eink-clock-forecast 6 | 7 | esp8266: 8 | board: d1_mini 9 | 10 | # Enable logging 11 | logger: 12 | 13 | # Enable Home Assistant API 14 | api: 15 | 16 | ota: 17 | password: !secret my_ap_password 18 | 19 | wifi: 20 | ssid: !secret my_ap_ssid 21 | password: !secret my_ap_password 22 | 23 | # Enable fallback hotspot (captive portal) in case wifi connection fails 24 | ap: 25 | ssid: "eink-clock-forecast" 26 | password: "12345678" 27 | 28 | captive_portal: 29 | 30 | time: 31 | - platform: homeassistant 32 | id: ha_time 33 | 34 | text_sensor: 35 | #weather icon 36 | - platform: homeassistant 37 | entity_id: sensor.openweathermap_condition 38 | id: today_weather 39 | 40 | #weather temperature 41 | - platform: homeassistant 42 | entity_id: sensor.openweathermap_temperature 43 | id: today_temperature 44 | 45 | 46 | binary_sensor: 47 | - platform: status 48 | name: "${device_name} WiFi Status" 49 | 50 | font: 51 | - file: "fonts/TT0246M.ttf" 52 | id: time_font 53 | size: 72 54 | glyphs: [0, 1, 2, 3, 4, 5, 6, 7 ,8, 9, ':'] 55 | 56 | - file: "fonts/GothamRnd-Book.ttf" 57 | id: date_font 58 | size: 25 59 | glyphs: &font-glyphs 60 | ['!', ',', '.', '"', '%', '-', '_', ':', '°', '/', 61 | '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ' ', 62 | 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 63 | 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 64 | 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 65 | 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 66 | 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'] 67 | 68 | - file: "fonts/GothamRnd-Bold.ttf" 69 | id: weekday_font 70 | size: 25 71 | glyphs: *font-glyphs 72 | 73 | - file: "fonts/GothamRnd-Bold.ttf" 74 | id: temp_font 75 | size: 30 76 | glyphs: *font-glyphs 77 | 78 | - file: 'fonts/materialdesignicons.ttf' 79 | id: icon_weather 80 | size: 80 81 | glyphs: &mdi-weather-glyphs 82 | - "\U000F0590" # mdi-weather-cloudy 83 | - "\U000F0F2F" # mdi-weather-cloudy-alert 84 | - "\U000F0E6E" # mdi-weather-cloudy-arrow-right 85 | - "\U000F0593" # mdi-weather-lightning 86 | - "\U000F067E" # mdi-weather-lightning-rainy 87 | - "\U000F0594" # mdi-weather-night 88 | - "\U000F0F31" # mdi-weather-night-partly-cloudy 89 | - "\U000F0595" # mdi-weather-partly-cloudy 90 | - "\U000F0F32" # mdi-weather-partly-lightning 91 | - "\U000F0F33" # mdi-weather-partly-rainy 92 | - "\U000F0596" # mdi-weather-pouring 93 | - "\U000F0597" # mdi-weather-rainy 94 | - "\U000F0599" # mdi-weather-sunny 95 | - "\U000F0F37" # mdi-weather-sunny-alert 96 | - "\U000F14E4" # mdi-weather-sunny-off 97 | - "\U000F059A" # mdi-weather-sunset 98 | - "\U000F059B" # mdi-weather-sunset-down 99 | - "\U000F059C" # mdi-weather-sunset-up 100 | - "\U000F059D" # mdi-weather-windy 101 | - "\U000F059E" # mdi-weather-windy-variant 102 | 103 | spi: 104 | clk_pin: D5 105 | mosi_pin: D7 106 | 107 | display: 108 | - platform: waveshare_epaper 109 | cs_pin: D8 110 | dc_pin: D2 111 | busy_pin: D1 112 | reset_pin: D4 113 | model: 2.90in 114 | rotation: 270 115 | update_interval: 10s 116 | id: my_display 117 | full_update_every: 30 118 | lambda: |- 119 | // Map weather states to MDI characters. 120 | std::map weather_icon_map 121 | { 122 | {"cloudy", "\U000F0590"}, 123 | {"cloudy-alert", "\U000F0F2F"}, 124 | {"fog", "\U000F0591"}, 125 | {"hail", "\U000F0592"}, 126 | {"hazy", "\U000F0F30"}, 127 | {"lightning", "\U000F0593"}, 128 | {"lightning-rainy", "\U000F067E"}, 129 | {"clear-night", "\U000F0594"}, 130 | {"night-partly-cloudy", "\U000F0F31"}, 131 | {"partlycloudy", "\U000F0595"}, 132 | {"partly-lightning", "\U000F0F32"}, 133 | {"partly-rainy", "\U000F0F33"}, 134 | {"pouring", "\U000F0596"}, 135 | {"rainy", "\U000F0597"}, 136 | {"sunny", "\U000F0599"}, 137 | {"sunny-alert", "\U000F0F37"}, 138 | {"sunny-off", "\U000F14E4"}, 139 | {"sunset", "\U000F059A"}, 140 | {"sunset-down", "\U000F059B"}, 141 | {"sunset-up", "\U000F059C"}, 142 | {"windy", "\U000F059D"}, 143 | {"windy-variant", "\U000F059E"}, 144 | }; 145 | 146 | //time 147 | it.strftime(110, -10, id(time_font), TextAlign::TOP_CENTER, "%H:%M", id(ha_time).now()); 148 | 149 | //date 150 | it.strftime(110, 75, id(weekday_font), TextAlign::TOP_CENTER, "%Y/%m/%d", id(ha_time).now()); 151 | 152 | //weekday 153 | it.strftime(110, 100, id(weekday_font), TextAlign::TOP_CENTER, "%A", id(ha_time).now()); 154 | 155 | //weather icon 156 | it.printf(255, 10, id(icon_weather), TextAlign::TOP_CENTER, "%s", weather_icon_map[id(today_weather).state.c_str()].c_str()); 157 | 158 | //temp 159 | it.printf(255, 95, id(temp_font), TextAlign::TOP_CENTER, "%s°C", id(today_temperature).state.c_str()); 160 | 161 | -------------------------------------------------------------------------------- /eink_clock_with_pages.yaml: -------------------------------------------------------------------------------- 1 | substitutions: 2 | device_name: d1mini-eink-clock 3 | 4 | esphome: 5 | name: d1mini-eink-clock 6 | comment: D1mini 7 | 8 | esp8266: 9 | board: d1_mini 10 | 11 | # Enable logging 12 | logger: 13 | 14 | # Enable Home Assistant API 15 | api: 16 | 17 | ota: 18 | 19 | wifi: 20 | ssid: !secret my_ap_ssid 21 | password: !secret my_ap_password 22 | 23 | # Enable fallback hotspot (captive portal) in case wifi connection fails 24 | ap: 25 | ssid: "d1mini-eink-clock" 26 | password: "12345678" 27 | 28 | captive_portal: 29 | 30 | time: 31 | - platform: sntp 32 | id: sntp_time 33 | timezone: Asia/Taipei 34 | 35 | sensor: 36 | - platform: wifi_signal 37 | id: wifisignal 38 | update_interval: 60s 39 | 40 | button: 41 | - platform: template 42 | name: "Show Time Page" 43 | icon: 'mdi:clock' 44 | on_press: 45 | then: 46 | - display.page.show: p1 47 | - component.update: my_display 48 | 49 | - platform: template 50 | name: "Show Message Page" 51 | icon: 'mdi:update' 52 | on_press: 53 | then: 54 | - display.page.show: p2 55 | - component.update: my_display 56 | 57 | font: 58 | - file: "fonts/GothamRnd-Bold.ttf" 59 | id: time_font 60 | size: 95 61 | glyphs: [0, 1, 2, 3, 4, 5, 6, 7 ,8, 9, ':'] 62 | 63 | - file: "fonts/GothamRnd-Book.ttf" 64 | id: time_font_book 65 | size: 95 66 | glyphs: [0, 1, 2, 3, 4, 5, 6, 7 ,8, 9, ':'] 67 | 68 | - file: "fonts/GothamRnd-Bold.ttf" 69 | id: weekday_font 70 | size: 35 71 | glyphs: &font-glyphs 72 | ['!', ',', '.', '"', '%', '-', '_', ':', '°', '/', 73 | '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ' ', 74 | 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 75 | 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 76 | 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 77 | 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 78 | 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'] 79 | 80 | - file: "fonts/GothamRnd-Book.ttf" 81 | id: date_font 82 | size: 32 83 | glyphs: *font-glyphs 84 | 85 | - file: "fonts/NotoSansTC-Medium.ttf" 86 | id: msg_font 87 | size: 40 88 | glyphs: 衣服已經洗拿去烘好囉!趕快收起來ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz,."%-~_:° 89 | 90 | - file: 'fonts/materialdesignicons-7296.ttf' 91 | id: icon_font 92 | size: 20 93 | glyphs: &mdi-weather-glyphs 94 | - "\U000F0783" # mdi-signal-off 95 | - "\U000F08BF" # mdi-signal-cellular-outline bad 96 | - "\U000F08BC" # mdi-signal-cellular-1 97 | - "\U000F08BD" # mdi-signal-cellular-2 98 | - "\U000F08BE" # mdi-signal-cellular-3 good 99 | 100 | spi: 101 | clk_pin: D5 102 | mosi_pin: D7 103 | 104 | display: 105 | - platform: waveshare_epaper 106 | cs_pin: D8 107 | dc_pin: D2 108 | busy_pin: D1 109 | reset_pin: D4 110 | model: 2.90in 111 | rotation: 270 112 | update_interval: 10s 113 | full_update_every: 60 114 | id: my_display 115 | pages: 116 | - id: p1 117 | lambda: |- 118 | //wifi signal 119 | if (id(wifisignal).state >= -60) { 120 | //Excellent 121 | it.print(0, 0, id(icon_font), "\U000F08BE"); 122 | } else if (id(wifisignal).state >= -70) { 123 | //Good 124 | it.print(0, 0, id(icon_font), "\U000F08BD"); 125 | } else if (id(wifisignal).state >= -75) { 126 | //Fair 127 | it.print(0, 0, id(icon_font),"\U000F08BC"); 128 | } else if (id(wifisignal).state >= -85) { 129 | //Weak 130 | it.print(0, 0, id(icon_font),"\U000F08BF"); 131 | } else { 132 | //Unlikely working signal 133 | it.print(0, 0, id(icon_font),"\U000F0783"); 134 | } 135 | //Time: hour 136 | it.strftime(75,1, id(time_font_book), TextAlign::TOP_CENTER, "%H",id(sntp_time).now()); 137 | 138 | //Time: ":" 139 | it.printf(150,-8, id(time_font_book), TextAlign::TOP_CENTER, ":"); 140 | 141 | //Time: minutes 142 | it.strftime(225,1, id(time_font_book), TextAlign::TOP_CENTER, "%M",id(sntp_time).now()); 143 | 144 | //Time: date 145 | it.strftime(104,93, id(date_font), TextAlign::TOP_CENTER, "%Y/%m/%d",id(sntp_time).now()); 146 | 147 | //Weekday 148 | it.strftime(250,90, id(weekday_font), TextAlign::TOP_CENTER, "%a",id(sntp_time).now()); 149 | 150 | - id: p2 151 | lambda: |- 152 | //wifi signal 153 | if (id(wifisignal).state >= -60) { 154 | //Excellent 155 | it.print(0, 0, id(icon_font), "\U000F08BE"); 156 | } else if (id(wifisignal).state >= -70) { 157 | //Good 158 | it.print(0, 0, id(icon_font), "\U000F08BD"); 159 | } else if (id(wifisignal).state >= -75) { 160 | //Fair 161 | it.print(0, 0, id(icon_font),"\U000F08BC"); 162 | } else if (id(wifisignal).state >= -85) { 163 | //Weak 164 | it.print(0, 0, id(icon_font),"\U000F08BF"); 165 | } else { 166 | //Unlikely working signal 167 | it.print(0, 0, id(icon_font),"\U000F0783"); 168 | } 169 | 170 | it.printf(150,15, id(msg_font), TextAlign::TOP_CENTER, "衣服已經洗好囉!"); 171 | it.printf(150,70, id(msg_font), TextAlign::TOP_CENTER, "趕快拿去烘~"); 172 | -------------------------------------------------------------------------------- /README_en.md: -------------------------------------------------------------------------------- 1 | # ESPhome E-ink 2.9" series sample 2 | 3 | Buy Me A Coffee 4 | 5 | [正體中文](https://github.com/xangin/esphome_eink2.9_series/blob/main/README.md) | English 6 | 7 | This project uses a 2.9" e-ink display with ESPhome and Home Assistant, allowing for customizable layouts. 8 | 9 | You can add more pages and control page switching through HA, for example, when the clothes are done washing, the screen will automatically switch to a message prompt. 10 | 11 | You can design how many pages or what to display freely. 12 | 13 | - Product size: Width 88mm x Height 47mm 14 | 15 | 16 | 17 | - Back of the product 18 | 19 | 20 | 21 | 22 | # Sample references: 23 | 24 | ## Sample 1. eink_clock.yaml 25 | 26 | A simple clock that only displays the date and time. 27 | 28 | 29 | 30 | 31 | ## Sample 2. eink_clock_with_forecast.yaml 32 | 33 | In addition to the date and time, it also reads the weather forecast icon and temperature from HA. 34 | 35 | 36 | 37 | 38 | ## Sample 3. eink_clock_with_temp.yaml 39 | 40 | In addition to the date and time, it also reads a temperature and humidity entity from HA. 41 | 42 | 43 | 44 | ## Sample 4. eink_clock_with_pages.yaml 45 | 46 | In addition to the date and time, it also includes a message page. 47 | 48 | 49 | 50 | ## Hardware 51 | 52 | - [Waveshare 2.9 inch Black and White E-ink Screen](https://detail.tmall.com/item.htm?id=605757420567) - Without casing 53 | - [E-ink Screen Driver Board ESP8266](https://oshwhub.com/lingdy2012/mo-shui-ping-_esp8266-qu-dong-ban-_0603_wos_v0-1) - Available on Xianyu 54 | - [E-ink Screen Casing (EW029F2(2.9 inch single battery))](https://item.taobao.com/item.htm?id=601700008521)- Case 55 | 56 | 57 | ## Installation 58 | 59 | 1. Place the files in the `/fonts` folder into the HA/config/esphome folder. 60 | 2. Place any YAML into HA/config/esphome, modify the content as desired, compile, and flash it to the ESP module via USB. 61 | 3. For reading information from HA, the ESPhome device must be added to HA for accurate display of information. 62 | 63 | ## ESPHome yaml 64 | 65 | ### Page switching control in HA 66 | 67 | There are 2 buttons that display p1 (Time Page) and p2 (Message Page) when pressed. If you want to add more pages, you can follow the code to add more. 68 | 69 | ```YAML 70 | button: 71 | - platform: template 72 | name: "Show Time Page" 73 | icon: 'mdi:clock' 74 | on_press: 75 | then: 76 | - display.page.show: p1 77 | - component.update: my_display 78 | 79 | - platform: template 80 | name: "Show Message Page" 81 | icon: 'mdi:update' 82 | on_press: 83 | then: 84 | - display.page.show: p2 85 | - component.update: my_display 86 | ``` 87 | 88 | ### Display icons based on Wi-Fi strength 89 | 90 | 說明: 91 | 92 | - Greater than or equal to -60 shows three bars 93 | - -60~-70 shows two bars 94 | - -70~-75 shows one bar 95 | - -75~-85 shows zero bars 96 | - Less than -85 shows disconnected 97 | 98 | You can freely change the strength range to display the number of bars. 99 | 100 | ```YAML 101 | //wifi signal 102 | if (id(wifisignal).state >= -60) { 103 | //Excellent 104 | it.print(0, 0, id(wifi_font), "\U000F08BE"); 105 | } else if (id(wifisignal).state >= -70) { 106 | //Good 107 | it.print(0, 0, id(wifi_font), "\U000F08BD"); 108 | } else if (id(wifisignal).state >= -75) { 109 | //Fair 110 | it.print(0, 0, id(wifi_font),"\U000F08BC"); 111 | } else if (id(wifisignal).state >= -85) { 112 | //Weak 113 | it.print(0, 0, id(wifi_font),"\U000F08BF"); 114 | } else { 115 | //Unlikely working signal 116 | it.print(0, 0, id(wifi_font),"\U000F0783"); 117 | } 118 | ``` 119 | 120 | ### Modify custom message content 121 | 122 | Steps: 123 | 124 | 1. In the font declaration `msg_font`, write out **all** the Chinese characters to be displayed first for proper display. 125 | 126 | ```YAML 127 | font: 128 | - file: "fonts/NotoSansTC-Medium.ttf" 129 | id: msg_font 130 | size: 40 131 | glyphs: 衣服已經洗拿去烘好囉!趕快收起來ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz,."%-~_:° 132 | ``` 133 | 134 | 2. In the `display` p2, replace the text you want to show. Based on the current size setting, one line displays `7 Chinese characters`. Exceeding this limit will not display correctly. 135 | 136 | ```YAML 137 | display: 138 | ... 139 | - id: p2 140 | lambda: |- 141 | it.printf(150,15, id(msg_font), TextAlign::TOP_CENTER, "衣服已經洗好囉!"); 142 | it.printf(150,70, id(msg_font), TextAlign::TOP_CENTER, "趕快拿去烘~"); 143 | ``` 144 | 145 | ### Add more pages 146 | 147 | Steps: Follow the button and display code to add more groups. For example, below is a third page that shows the clothes are done drying when pressed. 148 | 149 | Remember to add any Chinese characters not in `msg_font` to ensure proper display. 150 | 151 | ```YAML 152 | 153 | button: #Copy to the very bottom of the button code, do not write button repeatedly! 154 | ... 155 | - platform: template 156 | name: "Show Dryer Done Page" 157 | icon: 'mdi:update' 158 | on_press: 159 | then: 160 | - display.page.show: p3 161 | - component.update: my_display 162 | 163 | 164 | display: #Copy to the very bottom of the display code, do not write display repeatedly! 165 | ... 166 | - id: p3 167 | lambda: |- 168 | it.printf(150,15, id(msg_font), TextAlign::TOP_CENTER, "衣服已經烘好囉!"); 169 | it.printf(150,70, id(msg_font), TextAlign::TOP_CENTER, "趕快收起來!!"); 170 | ``` 171 | 172 | -------------------------------------------------------------------------------- /eink_clock_with_purifier_and_motion.yaml: -------------------------------------------------------------------------------- 1 | esphome: 2 | name: eink-sht-motion 3 | 4 | substitutions: 5 | device_name: kitchen 6 | 7 | esp8266: 8 | board: d1_mini 9 | 10 | # Enable logging 11 | logger: 12 | 13 | # Enable Home Assistant API 14 | api: 15 | 16 | ota: 17 | password: !secret my_ap_password 18 | 19 | wifi: 20 | ssid: !secret my_ap_ssid 21 | password: !secret my_ap_password 22 | 23 | # Enable fallback hotspot (captive portal) in case wifi connection fails 24 | ap: 25 | ssid: "eink-sht-motion" 26 | password: "12345678" 27 | 28 | time: 29 | - platform: homeassistant 30 | id: ha_time 31 | 32 | i2c: 33 | sda: D2 34 | scl: D1 35 | scan: true 36 | 37 | binary_sensor: 38 | - platform: status 39 | name: "${device_name} Status" 40 | id: system_status 41 | 42 | - platform: gpio 43 | id: motion 44 | pin: D6 45 | name: "${device_name} Motion" 46 | device_class: motion 47 | filters: 48 | - delayed_off: 1s 49 | 50 | sensor: 51 | - platform: wifi_signal 52 | id: wifisignal 53 | name: "${device_name} WiFi Signal" 54 | update_interval: 60s 55 | 56 | - platform: sht3xd 57 | address: 0x44 58 | update_interval: 30s 59 | temperature: 60 | name: "${device_name} Temperature" 61 | id: temp_data 62 | filters: 63 | - calibrate_polynomial: 64 | degree: 2 65 | datapoints: 66 | - 0.0 -> 0.0 67 | - 34.6 -> 28.6 68 | - 36.2 -> 29.6 69 | 70 | humidity: 71 | name: "${device_name} Humidity" 72 | id: hum_data 73 | filters: 74 | - calibrate_polynomial: 75 | degree: 2 76 | datapoints: 77 | - 0.0 -> 0.0 78 | - 50.2 -> 65 79 | - 55.5 -> 72 80 | 81 | 82 | 83 | - platform: homeassistant 84 | entity_id: sensor.water_purifier_pp_filter_level 85 | id: pp_filter 86 | 87 | - platform: homeassistant 88 | entity_id: sensor.water_purifier_ro_filter_level 89 | id: ro_filter 90 | 91 | - platform: homeassistant 92 | entity_id: sensor.water_purifier_input_water_tds 93 | id: tds_in 94 | 95 | - platform: homeassistant 96 | entity_id: sensor.water_purifier_output_water_tds 97 | id: tds_out 98 | 99 | button: 100 | - platform: template 101 | name: '${device_name} Show Time Page' 102 | icon: 'mdi:update' 103 | on_press: 104 | then: 105 | - display.page.show: p1 106 | - component.update: my_display 107 | internal: false 108 | 109 | - platform: template 110 | name: '${device_name} Show Water Page' 111 | icon: 'mdi:update' 112 | on_press: 113 | then: 114 | - display.page.show: p2 115 | - component.update: my_display 116 | internal: false 117 | 118 | font: 119 | - file: "fonts/GothamRnd-Book.ttf" 120 | id: time_font 121 | size: 80 122 | glyphs: [0, 1, 2, 3, 4, 5, 6, 7 ,8, 9, ':'] 123 | 124 | - file: "fonts/GothamRnd-Bold.ttf" 125 | id: temp_font 126 | size: 30 127 | glyphs: &font-glyphs 128 | ['!', ',', '.', '"', '%', '-', '_', ':', '°', '/', 129 | '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ' ', 130 | 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 131 | 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 132 | 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 133 | 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 134 | 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'] 135 | 136 | - file: "fonts/GothamRnd-Book.ttf" 137 | id: book_40 138 | size: 40 139 | glyphs: *font-glyphs 140 | 141 | - file: "fonts/GothamRnd-Bold.ttf" 142 | id: bold_40 143 | size: 40 144 | glyphs: *font-glyphs 145 | 146 | - file: "fonts/GothamRnd-Bold.ttf" 147 | id: bold_25 148 | size: 25 149 | glyphs: *font-glyphs 150 | 151 | - file: "fonts/GothamRnd-Book.ttf" 152 | id: date_font 153 | size: 30 154 | glyphs: *font-glyphs 155 | 156 | - file: "fonts/materialdesignicons-7296.ttf" 157 | id: icon_font 158 | size: 30 159 | glyphs: 160 | - "\U000F058E" # mdi-water-percent 161 | - "\U000F050F" # mdi-thermometer 162 | 163 | color: 164 | - id: color_black 165 | red: 0% 166 | green: 0% 167 | blue: 0% 168 | white: 50% 169 | 170 | - id: color_white 171 | red: 0% 172 | green: 0% 173 | blue: 0% 174 | white: 0% 175 | 176 | spi: 177 | clk_pin: D5 178 | mosi_pin: D7 179 | 180 | display: 181 | - platform: waveshare_epaper 182 | id: my_display 183 | cs_pin: D0 184 | dc_pin: D8 185 | model: 2.90in 186 | rotation: 270 187 | update_interval: 10s 188 | full_update_every: 30 189 | pages: 190 | - id: p1 191 | lambda: |- 192 | //time 193 | it.strftime(75,1, id(time_font), TextAlign::TOP_CENTER, "%H",id(ha_time).now()); 194 | 195 | it.printf(150,-6, id(time_font), TextAlign::TOP_CENTER, ":"); 196 | 197 | it.strftime(225,1, id(time_font), TextAlign::TOP_CENTER, "%M",id(ha_time).now()); 198 | 199 | it.strftime(150, 70, id(date_font), TextAlign::TOP_CENTER, "%Y/%m/%d %a", id(ha_time).now()); 200 | 201 | //temp 202 | it.print(25, 97, id(icon_font),TextAlign::TOP_CENTER, "\U000F050F"); 203 | if(id(temp_data).has_state()){ 204 | it.printf(90,100, id(temp_font), TextAlign::TOP_CENTER, "%.1f°C", id(temp_data).state); 205 | } 206 | //hum 207 | it.print(185, 100, id(icon_font),TextAlign::TOP_CENTER, "\U000F058E"); 208 | if(id(hum_data).has_state()){ 209 | it.printf(240, 100, id(temp_font), TextAlign::TOP_CENTER, "%.1f%%", id(hum_data).state); 210 | } 211 | 212 | - id: p2 213 | lambda: |- 214 | it.printf(1,-1, id(book_40), TextAlign::TOP_LEFT, "PP:"); 215 | it.printf(65,10, id(bold_25), TextAlign::TOP_LEFT, "Filter"); 216 | it.printf(30,33, id(bold_40), TextAlign::TOP_LEFT, "%.0f%%",id(pp_filter).state); 217 | 218 | it.printf(1,63, id(book_40), TextAlign::TOP_LEFT, "RO"); 219 | it.printf(30,95, id(bold_40), TextAlign::TOP_LEFT, "%.0f%%",id(ro_filter).state); 220 | 221 | it.printf(160,-1, id(book_40), TextAlign::TOP_LEFT, "IN:"); 222 | it.printf(215,10, id(bold_25), TextAlign::TOP_LEFT, "TDS"); 223 | it.printf(200,33, id(bold_40), TextAlign::TOP_LEFT, "%.0f",id(tds_in).state); 224 | 225 | it.printf(160,63, id(book_40), TextAlign::TOP_LEFT, "OUT:"); 226 | it.printf(200,95, id(bold_40), TextAlign::TOP_LEFT, "%.0f",id(tds_out).state); 227 | --------------------------------------------------------------------------------