├── components ├── bm8563 │ ├── __init__.py │ ├── bm8563.h │ ├── time.py │ └── bm8563.cpp ├── it8951e │ ├── __init__.py │ ├── README.md │ ├── it8951.h │ ├── display.py │ ├── it8951e.h │ └── it8951e.cpp └── m5paper │ ├── m5paper.h │ ├── m5paper.cpp │ └── __init__.py ├── img └── screen_demo.jpg ├── .gitignore ├── README.md ├── test.yaml └── .screen_base.yaml /components/bm8563/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/it8951e/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /img/screen_demo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Passific/m5paper_esphome/HEAD/img/screen_demo.jpg -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Gitignore settings for ESPHome 2 | # This is an example and may include too much for your use-case. 3 | # You can modify this file to suit your needs. 4 | .DS_Store 5 | /.esphome/ 6 | /secrets.yaml 7 | /buildlog.txt 8 | living_room_screen.yaml 9 | fonts 10 | __pycache__ 11 | -------------------------------------------------------------------------------- /components/it8951e/README.md: -------------------------------------------------------------------------------- 1 | ```yaml 2 | # example configuration: 3 | 4 | spi: 5 | clk_pin: GPIO14 6 | mosi_pin: GPIO12 7 | miso_pin: GPIO13 8 | 9 | display: 10 | - platform: it8951e 11 | id: m5paper_display 12 | cs_pin: GPIO15 13 | reset_pin: GPIO23 14 | reset_duration: 100ms 15 | busy_pin: GPIO27 16 | rotation: 0 17 | reversed: False 18 | update_interval: never 19 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # m5paper_esphome 2 | 3 | Based on https://github.com/paveldn/m5paper_esphome 4 | 5 | Himself based on https://github.com/sebirdman/m5paper_esphome 6 | 7 | ![Screen example](./img/screen_demo.jpg) 8 | 9 | Work in progress 10 | 11 | All components are functional, but likely have bugs. 12 | 13 | Please, download font from https://materialdesignicons.com/ and put in 'fonts' folder 14 | 15 | BM8563 work based on: https://github.com/TomG736/esphome-BM8563 16 | -------------------------------------------------------------------------------- /test.yaml: -------------------------------------------------------------------------------- 1 | substitutions: 2 | device_name: Test status screen 3 | device_id: test-screen 4 | # Home assistant sensors 5 | # Weather conditions 6 | weather_conditions: weather.my_home 7 | # Outdoor sensors 8 | outdoor_temperature: sensor.outdoor_temperature 9 | outdoor_humidity: sensor.outdoor_humidity 10 | outdoor_rainfall: sensor.outdoor_rainfall 11 | outdoor_wind_strength: sensor.outdoor_wind_strength 12 | # Indoor sensors 13 | indoor_temperature: sensor.indoor_temperature 14 | indoor_humidity: sensor.indoor_humidity 15 | indoor_pressure: sensor.indoor_pressure 16 | indoor_co2: sensor.indoor_co2 17 | # Alarms 18 | # Any counter, will be shown as number 0..9 19 | lights_counter: sensor.lights_counter 20 | # Alarmo status 21 | alarmo_status: alarm_control_panel.alarmo 22 | # Radiation alarm 23 | radiation_alarm: binary_sensor.radiation_alarm 24 | 25 | web_server: 26 | 27 | # Include main functionality file 28 | <<: !include .screen_base.yaml -------------------------------------------------------------------------------- /components/m5paper/m5paper.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "esphome/core/component.h" 4 | #include "esphome/core/gpio.h" 5 | #include "esphome/core/automation.h" 6 | 7 | namespace esphome { 8 | namespace m5paper { 9 | 10 | class M5PaperComponent : public Component { 11 | void setup() override; 12 | void dump_config() override; 13 | /* Very early setup as takes care of powering other components */ 14 | float get_setup_priority() const override { return setup_priority::BUS; }; 15 | 16 | public: 17 | void set_battery_power_pin(GPIOPin *power) { this->battery_power_pin_ = power; } 18 | void set_main_power_pin(GPIOPin *power) { this->main_power_pin_ = power; } 19 | void shutdown_main_power(); 20 | 21 | private: 22 | GPIOPin *battery_power_pin_{nullptr}; 23 | GPIOPin *main_power_pin_{nullptr}; 24 | 25 | }; 26 | 27 | template class PowerAction : public Action, public Parented { 28 | public: 29 | void play(Ts... x) override { this->parent_->shutdown_main_power(); } 30 | }; 31 | 32 | } //namespace m5paper 33 | } //namespace esphome -------------------------------------------------------------------------------- /components/m5paper/m5paper.cpp: -------------------------------------------------------------------------------- 1 | #include "esphome/core/log.h" 2 | #include "m5paper.h" 3 | #include "driver/gpio.h" 4 | 5 | namespace esphome { 6 | namespace m5paper { 7 | 8 | // hack to hold power lines up in deep sleep mode 9 | // battery life isn't great with deep sleep, recommend bm8563 sleep 10 | #define ALLOW_ESPHOME_DEEP_SLEEP true 11 | 12 | static const char *TAG = "m5paper.component"; 13 | 14 | void M5PaperComponent::setup() { 15 | ESP_LOGCONFIG(TAG, "m5paper starting up!"); 16 | this->main_power_pin_->pin_mode(gpio::FLAG_OUTPUT); 17 | this->main_power_pin_->digital_write(true); 18 | 19 | this->battery_power_pin_->pin_mode(gpio::FLAG_OUTPUT); 20 | this->battery_power_pin_->digital_write(true); 21 | 22 | if (ALLOW_ESPHOME_DEEP_SLEEP) { 23 | gpio_hold_en(GPIO_NUM_2); 24 | gpio_hold_en(GPIO_NUM_5); 25 | } 26 | } 27 | 28 | void M5PaperComponent::shutdown_main_power() { 29 | ESP_LOGE(TAG, "Shutting Down Power"); 30 | if (ALLOW_ESPHOME_DEEP_SLEEP) { 31 | gpio_hold_dis(GPIO_NUM_2); 32 | gpio_hold_dis(GPIO_NUM_5); 33 | } 34 | this->main_power_pin_->digital_write(false); 35 | } 36 | 37 | void M5PaperComponent::dump_config() { 38 | ESP_LOGCONFIG(TAG, "M5Paper"); 39 | } 40 | 41 | } //namespace m5paper 42 | } //namespace esphome -------------------------------------------------------------------------------- /components/m5paper/__init__.py: -------------------------------------------------------------------------------- 1 | import esphome.codegen as cg 2 | from esphome import pins 3 | import esphome.config_validation as cv 4 | from esphome import automation 5 | from esphome.const import ( 6 | CONF_ID, 7 | ) 8 | 9 | m5paper_ns = cg.esphome_ns.namespace('m5paper') 10 | 11 | M5PaperComponent = m5paper_ns.class_('M5PaperComponent', cg.Component) 12 | PowerAction = m5paper_ns.class_("PowerAction", automation.Action) 13 | 14 | CONF_MAIN_POWER_PIN = "main_power_pin" 15 | CONF_BATTERY_POWER_PIN = "battery_power_pin" 16 | 17 | CONFIG_SCHEMA = cv.Schema({ 18 | cv.GenerateID(): cv.declare_id(M5PaperComponent), 19 | cv.Required(CONF_MAIN_POWER_PIN): pins.gpio_output_pin_schema, 20 | cv.Required(CONF_BATTERY_POWER_PIN): pins.gpio_output_pin_schema 21 | }) 22 | 23 | @automation.register_action( 24 | "m5paper.shutdown_main_power", 25 | PowerAction, 26 | automation.maybe_simple_id( 27 | { 28 | cv.GenerateID(): cv.use_id(M5PaperComponent), 29 | } 30 | ), 31 | ) 32 | async def m5paper_shutdown_main_power_to_code(config, action_id, template_arg, args): 33 | var = cg.new_Pvariable(action_id, template_arg) 34 | await cg.register_parented(var, config[CONF_ID]) 35 | return var 36 | 37 | 38 | async def to_code(config): 39 | var = cg.new_Pvariable(config[CONF_ID]) 40 | await cg.register_component(var, config) 41 | 42 | if CONF_MAIN_POWER_PIN in config: 43 | power = await cg.gpio_pin_expression(config[CONF_MAIN_POWER_PIN]) 44 | cg.add(var.set_main_power_pin(power)) 45 | if CONF_BATTERY_POWER_PIN in config: 46 | power = await cg.gpio_pin_expression(config[CONF_BATTERY_POWER_PIN]) 47 | cg.add(var.set_battery_power_pin(power)) -------------------------------------------------------------------------------- /components/bm8563/bm8563.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "esphome/components/sensor/sensor.h" 4 | #include "esphome/components/i2c/i2c.h" 5 | #include "esphome/components/time/real_time_clock.h" 6 | 7 | namespace esphome { 8 | namespace bm8563 { 9 | 10 | typedef struct 11 | { 12 | int8_t hours; 13 | int8_t minutes; 14 | int8_t seconds; 15 | } BM8563_TimeTypeDef; 16 | 17 | typedef struct 18 | { 19 | int8_t day; 20 | int8_t week; 21 | int8_t month; 22 | int16_t year; 23 | } BM8563_DateTypeDef; 24 | 25 | class BM8563 : public time::RealTimeClock, public i2c::I2CDevice { 26 | public: 27 | void setup() override; 28 | void update() override; 29 | void dump_config() override; 30 | 31 | void set_sleep_duration(uint32_t time_ms); 32 | void write_time(); 33 | void read_time(); 34 | void apply_sleep_duration(); 35 | 36 | private: 37 | bool getVoltLow(); 38 | 39 | void getTime(BM8563_TimeTypeDef* BM8563_TimeStruct); 40 | void getDate(BM8563_DateTypeDef* BM8563_DateStruct); 41 | 42 | void setTime(BM8563_TimeTypeDef* BM8563_TimeStruct); 43 | void setDate(BM8563_DateTypeDef* BM8563_DateStruct); 44 | 45 | int SetAlarmIRQ(int afterSeconds); 46 | int SetAlarmIRQ(const BM8563_TimeTypeDef &BM8563_TimeStruct); 47 | int SetAlarmIRQ(const BM8563_DateTypeDef &BM8563_DateStruct, const BM8563_TimeTypeDef &BM8563_TimeStruct); 48 | 49 | void clearIRQ(); 50 | void disableIRQ(); 51 | 52 | void WriteReg(uint8_t reg, uint8_t data); 53 | uint8_t ReadReg(uint8_t reg); 54 | 55 | uint8_t bcd2ToByte(uint8_t value); 56 | uint8_t byteToBcd2(uint8_t value); 57 | 58 | uint8_t trdata[7]; 59 | optional sleep_duration_; 60 | bool setupComplete; 61 | }; 62 | 63 | template class WriteAction : public Action, public Parented { 64 | public: 65 | void play(Ts... x) override { this->parent_->write_time(); } 66 | }; 67 | 68 | template class ReadAction : public Action, public Parented { 69 | public: 70 | void play(Ts... x) override { this->parent_->read_time(); } 71 | }; 72 | 73 | template class SleepAction : public Action, public Parented { 74 | public: 75 | void play(Ts... x) override { this->parent_->apply_sleep_duration(); } 76 | }; 77 | 78 | } // namespace bm8563 79 | } // namespace esphome -------------------------------------------------------------------------------- /components/bm8563/time.py: -------------------------------------------------------------------------------- 1 | import esphome.codegen as cg 2 | import esphome.config_validation as cv 3 | from esphome import automation 4 | from esphome.components import i2c, time 5 | from esphome.const import CONF_ID, CONF_SLEEP_DURATION 6 | 7 | DEPENDENCIES = ['i2c'] 8 | 9 | CONF_I2C_ADDR = 0x51 10 | 11 | bm8563 = cg.esphome_ns.namespace('bm8563') 12 | BM8563 = bm8563.class_('BM8563', cg.Component, i2c.I2CDevice) 13 | WriteAction = bm8563.class_("WriteAction", automation.Action) 14 | ReadAction = bm8563.class_("ReadAction", automation.Action) 15 | SleepAction = bm8563.class_("SleepAction", automation.Action) 16 | 17 | CONFIG_SCHEMA = time.TIME_SCHEMA.extend({ 18 | cv.GenerateID(): cv.declare_id(BM8563), 19 | cv.Optional(CONF_SLEEP_DURATION): cv.positive_time_period_milliseconds, 20 | }).extend(cv.COMPONENT_SCHEMA).extend(i2c.i2c_device_schema(CONF_I2C_ADDR)) 21 | 22 | @automation.register_action( 23 | "bm8563.write_time", 24 | WriteAction, 25 | cv.Schema( 26 | { 27 | cv.GenerateID(): cv.use_id(BM8563), 28 | } 29 | ), 30 | ) 31 | async def bm8563_write_time_to_code(config, action_id, template_arg, args): 32 | var = cg.new_Pvariable(action_id, template_arg) 33 | await cg.register_parented(var, config[CONF_ID]) 34 | return var 35 | 36 | @automation.register_action( 37 | "bm8563.apply_sleep_duration", 38 | SleepAction, 39 | automation.maybe_simple_id( 40 | { 41 | cv.GenerateID(): cv.use_id(BM8563), 42 | } 43 | ), 44 | ) 45 | async def bm8563_apply_sleep_duration_to_code(config, action_id, template_arg, args): 46 | var = cg.new_Pvariable(action_id, template_arg) 47 | await cg.register_parented(var, config[CONF_ID]) 48 | return var 49 | 50 | @automation.register_action( 51 | "bm8563.read_time", 52 | ReadAction, 53 | automation.maybe_simple_id( 54 | { 55 | cv.GenerateID(): cv.use_id(BM8563), 56 | } 57 | ), 58 | ) 59 | async def bm8563_read_time_to_code(config, action_id, template_arg, args): 60 | var = cg.new_Pvariable(action_id, template_arg) 61 | await cg.register_parented(var, config[CONF_ID]) 62 | return var 63 | 64 | async def to_code(config): 65 | var = cg.new_Pvariable(config[CONF_ID]) 66 | await cg.register_component(var, config) 67 | await i2c.register_i2c_device(var, config) 68 | await time.register_time(var, config) 69 | if CONF_SLEEP_DURATION in config: 70 | cg.add(var.set_sleep_duration(config[CONF_SLEEP_DURATION])) 71 | -------------------------------------------------------------------------------- /components/it8951e/it8951.h: -------------------------------------------------------------------------------- 1 | #ifndef _IT8951_DEFINES_H_ 2 | #define _IT8951_DEFINES_H_ 3 | 4 | /*----------------------------------------------------------------------- 5 | IT8951 Command defines 6 | ------------------------------------------------------------------------*/ 7 | 8 | //Built in I80 Command Code 9 | #define IT8951_TCON_SYS_RUN 0x0001 10 | #define IT8951_TCON_STANDBY 0x0002 11 | #define IT8951_TCON_SLEEP 0x0003 12 | #define IT8951_TCON_REG_RD 0x0010 13 | #define IT8951_TCON_REG_WR 0x0011 14 | 15 | #define IT8951_TCON_MEM_BST_RD_T 0x0012 16 | #define IT8951_TCON_MEM_BST_RD_S 0x0013 17 | #define IT8951_TCON_MEM_BST_WR 0x0014 18 | #define IT8951_TCON_MEM_BST_END 0x0015 19 | 20 | #define IT8951_TCON_LD_IMG 0x0020 21 | #define IT8951_TCON_LD_IMG_AREA 0x0021 22 | #define IT8951_TCON_LD_IMG_END 0x0022 23 | 24 | //I80 User defined command code 25 | #define IT8951_I80_CMD_DPY_AREA 0x0034 26 | #define IT8951_I80_CMD_GET_DEV_INFO 0x0302 27 | #define IT8951_I80_CMD_DPY_BUF_AREA 0x0037 28 | #define IT8951_I80_CMD_VCOM 0x0039 29 | 30 | /*----------------------------------------------------------------------- 31 | IT8951 Mode defines 32 | ------------------------------------------------------------------------*/ 33 | //Pixel mode (Bit per Pixel) 34 | #define IT8951_2BPP 0 35 | #define IT8951_3BPP 1 36 | #define IT8951_4BPP 2 37 | #define IT8951_8BPP 3 38 | 39 | //Endian Type 40 | #define IT8951_LDIMG_L_ENDIAN 0 41 | #define IT8951_LDIMG_B_ENDIAN 1 42 | 43 | /*----------------------------------------------------------------------- 44 | IT8951 Registers defines 45 | ------------------------------------------------------------------------*/ 46 | //Register Base Address 47 | #define IT8951_DISPLAY_REG_BASE 0x1000 //Register RW access 48 | 49 | //Base Address of Basic LUT Registers 50 | #define IT8951_LUT0EWHR (IT8951_DISPLAY_REG_BASE + 0x00) //LUT0 Engine Width Height Reg 51 | #define IT8951_LUT0XYR (IT8951_DISPLAY_REG_BASE + 0x40) //LUT0 XY Reg 52 | #define IT8951_LUT0BADDR (IT8951_DISPLAY_REG_BASE + 0x80) //LUT0 Base Address Reg 53 | #define IT8951_LUT0MFN (IT8951_DISPLAY_REG_BASE + 0xC0) //LUT0 Mode and Frame number Reg 54 | #define IT8951_LUT01AF (IT8951_DISPLAY_REG_BASE + 0x114) //LUT0 and LUT1 Active Flag Reg 55 | 56 | //Update Parameter Setting Register 57 | #define IT8951_UP0SR (IT8951_DISPLAY_REG_BASE + 0x134) //Update Parameter0 Setting Reg 58 | #define IT8951_UP1SR (IT8951_DISPLAY_REG_BASE + 0x138) //Update Parameter1 Setting Reg 59 | #define IT8951_LUT0ABFRV (IT8951_DISPLAY_REG_BASE + 0x13C) //LUT0 Alpha blend and Fill rectangle Value 60 | #define IT8951_UPBBADDR (IT8951_DISPLAY_REG_BASE + 0x17C) //Update Buffer Base Address 61 | #define IT8951_LUT0IMXY (IT8951_DISPLAY_REG_BASE + 0x180) //LUT0 Image buffer X/Y offset Reg 62 | #define IT8951_LUTAFSR (IT8951_DISPLAY_REG_BASE + 0x224) //LUT Status Reg (status of All LUT Engines) 63 | #define IT8951_BGVR (IT8951_DISPLAY_REG_BASE + 0x250) //Bitmap (1bpp) image color table 64 | 65 | //System Registers 66 | #define IT8951_SYS_REG_BASE 0x0000 67 | 68 | //Address of System Registers 69 | #define IT8951_I80CPCR (IT8951_SYS_REG_BASE + 0x04) 70 | 71 | //Memory Converter Registers 72 | #define IT8951_MCSR_BASE_ADDR 0x0200 73 | #define IT8951_MCSR (IT8951_MCSR_BASE_ADDR + 0x0000) 74 | #define IT8951_LISAR (IT8951_MCSR_BASE_ADDR + 0x0008) 75 | 76 | #endif 77 | -------------------------------------------------------------------------------- /components/it8951e/display.py: -------------------------------------------------------------------------------- 1 | import esphome.codegen as cg 2 | import esphome.config_validation as cv 3 | from esphome import core, pins 4 | from esphome import automation 5 | from esphome.components import display, spi 6 | from esphome.const import __version__ as ESPHOME_VERSION 7 | from esphome.const import ( 8 | CONF_NAME, 9 | CONF_ID, 10 | CONF_RESET_PIN, 11 | CONF_RESET_DURATION, 12 | CONF_BUSY_PIN, 13 | CONF_PAGES, 14 | CONF_LAMBDA, 15 | CONF_MODEL, 16 | CONF_REVERSED, 17 | CONF_SLEEP_WHEN_DONE, 18 | CONF_FULL_UPDATE_EVERY, 19 | ) 20 | 21 | DEPENDENCIES = ['spi'] 22 | 23 | it8951e_ns = cg.esphome_ns.namespace('it8951e') 24 | IT8951ESensor = it8951e_ns.class_( 25 | 'IT8951ESensor', cg.PollingComponent, spi.SPIDevice, display.DisplayBuffer, display.Display 26 | ) 27 | ClearAction = it8951e_ns.class_("ClearAction", automation.Action) 28 | UpdateSlowAction = it8951e_ns.class_("UpdateSlowAction", automation.Action) 29 | DrawAction = it8951e_ns.class_("DrawAction", automation.Action) 30 | 31 | it8951eModel = it8951e_ns.enum("it8951eModel") 32 | 33 | MODELS = { 34 | "M5EPD": it8951eModel.M5EPD 35 | } 36 | 37 | CONFIG_SCHEMA = cv.All( 38 | display.FULL_DISPLAY_SCHEMA.extend( 39 | { 40 | cv.GenerateID(): cv.declare_id(IT8951ESensor), 41 | cv.Optional(CONF_NAME): cv.string, 42 | cv.Required(CONF_RESET_PIN): pins.gpio_output_pin_schema, 43 | cv.Required(CONF_BUSY_PIN): pins.gpio_input_pin_schema, 44 | cv.Optional(CONF_REVERSED): cv.boolean, 45 | cv.Optional(CONF_RESET_DURATION): cv.All( 46 | cv.positive_time_period_milliseconds, 47 | cv.Range(max=core.TimePeriod(milliseconds=500)), 48 | ), 49 | cv.Optional(CONF_MODEL, default="M5EPD"): cv.enum( 50 | MODELS, upper=True, space="_" 51 | ), 52 | cv.Optional(CONF_SLEEP_WHEN_DONE, default=True): cv.boolean, 53 | cv.Optional(CONF_FULL_UPDATE_EVERY, default=60): cv.int_range(min=0, max=4294967295), 54 | } 55 | ) 56 | .extend(cv.polling_component_schema("1s")) 57 | .extend(spi.spi_device_schema()), 58 | ) 59 | 60 | @automation.register_action( 61 | "it8951e.clear", 62 | ClearAction, 63 | automation.maybe_simple_id( 64 | { 65 | cv.GenerateID(): cv.use_id(IT8951ESensor), 66 | } 67 | ), 68 | ) 69 | @automation.register_action( 70 | "it8951e.updateslow", 71 | UpdateSlowAction, 72 | automation.maybe_simple_id( 73 | { 74 | cv.GenerateID(): cv.use_id(IT8951ESensor), 75 | } 76 | ), 77 | ) 78 | @automation.register_action( 79 | "it8951e.draw", 80 | DrawAction, 81 | automation.maybe_simple_id( 82 | { 83 | cv.GenerateID(): cv.use_id(IT8951ESensor), 84 | } 85 | ), 86 | ) 87 | 88 | async def it8951e_clear_to_code(config, action_id, template_arg, args): 89 | var = cg.new_Pvariable(action_id, template_arg) 90 | await cg.register_parented(var, config[CONF_ID]) 91 | return var 92 | 93 | async def to_code(config): 94 | var = cg.new_Pvariable(config[CONF_ID]) 95 | if cv.Version.parse(ESPHOME_VERSION) < cv.Version.parse("2023.12.0"): 96 | await cg.register_component(var, config) 97 | await display.register_display(var, config) 98 | await spi.register_spi_device(var, config) 99 | 100 | if CONF_MODEL in config: 101 | cg.add(var.set_model(config[CONF_MODEL])) 102 | if CONF_LAMBDA in config: 103 | lambda_ = await cg.process_lambda( 104 | config[CONF_LAMBDA], [(display.DisplayRef, "it")], return_type=cg.void 105 | ) 106 | cg.add(var.set_writer(lambda_)) 107 | if CONF_RESET_PIN in config: 108 | reset = await cg.gpio_pin_expression(config[CONF_RESET_PIN]) 109 | cg.add(var.set_reset_pin(reset)) 110 | if CONF_BUSY_PIN in config: 111 | busy = await cg.gpio_pin_expression(config[CONF_BUSY_PIN]) 112 | cg.add(var.set_busy_pin(busy)) 113 | if CONF_REVERSED in config: 114 | cg.add(var.set_reversed(config[CONF_REVERSED])) 115 | if CONF_RESET_DURATION in config: 116 | cg.add(var.set_reset_duration(config[CONF_RESET_DURATION])) 117 | if CONF_SLEEP_WHEN_DONE in config: 118 | cg.add(var.set_sleep_when_done(config[CONF_SLEEP_WHEN_DONE])) 119 | if CONF_FULL_UPDATE_EVERY in config: 120 | cg.add(var.set_full_update_every(config[CONF_FULL_UPDATE_EVERY])) 121 | -------------------------------------------------------------------------------- /components/bm8563/bm8563.cpp: -------------------------------------------------------------------------------- 1 | #include "esphome/core/log.h" 2 | #include "esphome/components/i2c/i2c_bus.h" 3 | #include "bm8563.h" 4 | 5 | namespace esphome { 6 | namespace bm8563 { 7 | 8 | static const char *TAG = "bm8563.sensor"; 9 | 10 | void BM8563::setup(){ 11 | this->write_byte_16(0,0); 12 | this->setupComplete = true; 13 | } 14 | 15 | void BM8563::update(){ 16 | if(!this->setupComplete){ 17 | return; 18 | } 19 | this->read_time(); 20 | } 21 | 22 | void BM8563::dump_config(){ 23 | ESP_LOGCONFIG(TAG, "BM8563:"); 24 | ESP_LOGCONFIG(TAG, " Address: 0x%02X", this->address_); 25 | ESP_LOGCONFIG(TAG, " setupComplete: %s", this->setupComplete ? "true" : "false"); 26 | if (this->sleep_duration_.has_value()) { 27 | uint32_t duration = *this->sleep_duration_; 28 | ESP_LOGCONFIG(TAG, " Sleep Duration: %u ms", duration); 29 | } 30 | } 31 | 32 | void BM8563::set_sleep_duration(uint32_t time_s) { 33 | ESP_LOGI(TAG, "Sleep Duration Setting to: %u ms", time_s); 34 | this->sleep_duration_ = time_s; 35 | } 36 | 37 | void BM8563::apply_sleep_duration() { 38 | if (this->sleep_duration_.has_value() && this->setupComplete) { 39 | this->clearIRQ(); 40 | this->SetAlarmIRQ(*this->sleep_duration_); 41 | } 42 | } 43 | 44 | void BM8563::write_time() { 45 | auto now = time::RealTimeClock::utcnow(); 46 | if (!now.is_valid()) { 47 | ESP_LOGE(TAG, "Invalid system time, not syncing to RTC."); 48 | return; 49 | } 50 | 51 | BM8563_TimeTypeDef BM8563_TimeStruct = { 52 | hours: int8_t(now.hour), 53 | minutes: int8_t(now.minute), 54 | seconds: int8_t(now.second), 55 | }; 56 | 57 | BM8563_DateTypeDef BM8563_DateStruct = { 58 | day: int8_t(now.day_of_month), 59 | week: int8_t(now.day_of_week), 60 | month: int8_t(now.month), 61 | year: int16_t(now.year) 62 | }; 63 | 64 | this->setTime(&BM8563_TimeStruct); 65 | this->setDate(&BM8563_DateStruct); 66 | } 67 | 68 | void BM8563::read_time() { 69 | 70 | BM8563_TimeTypeDef BM8563_TimeStruct; 71 | BM8563_DateTypeDef BM8563_DateStruct; 72 | getTime(&BM8563_TimeStruct); 73 | getDate(&BM8563_DateStruct); 74 | ESP_LOGD(TAG, "BM8563: %i-%i-%i %i, %i:%i:%i", 75 | BM8563_DateStruct.year, 76 | BM8563_DateStruct.month, 77 | BM8563_DateStruct.day, 78 | BM8563_DateStruct.week, 79 | BM8563_TimeStruct.hours, 80 | BM8563_TimeStruct.minutes, 81 | BM8563_TimeStruct.seconds 82 | ); 83 | 84 | ESPTime rtc_time{.second = uint8_t(BM8563_TimeStruct.seconds), 85 | .minute = uint8_t(BM8563_TimeStruct.minutes), 86 | .hour = uint8_t(BM8563_TimeStruct.hours), 87 | .day_of_week = uint8_t(BM8563_DateStruct.week), 88 | .day_of_month = uint8_t(BM8563_DateStruct.day), 89 | .day_of_year = 1, // ignored by recalc_timestamp_utc(false) 90 | .month = uint8_t(BM8563_DateStruct.month), 91 | .year = uint16_t(BM8563_DateStruct.year), 92 | .is_dst = false, // ignored by recalc_timestamp_utc() 93 | .timestamp = 0 // result 94 | }; 95 | rtc_time.recalc_timestamp_utc(false); 96 | time::RealTimeClock::synchronize_epoch_(rtc_time.timestamp); 97 | } 98 | 99 | bool BM8563::getVoltLow() { 100 | uint8_t data = ReadReg(0x02); 101 | return data & 0x80; // RTCC_VLSEC_MASK 102 | } 103 | 104 | uint8_t BM8563::bcd2ToByte(uint8_t value) { 105 | uint8_t tmp = 0; 106 | tmp = ((uint8_t)(value & (uint8_t)0xF0) >> (uint8_t)0x4) * 10; 107 | return (tmp + (value & (uint8_t)0x0F)); 108 | } 109 | 110 | uint8_t BM8563::byteToBcd2(uint8_t value) { 111 | uint8_t bcdhigh = 0; 112 | 113 | while (value >= 10) { 114 | bcdhigh++; 115 | value -= 10; 116 | } 117 | 118 | return ((uint8_t)(bcdhigh << 4) | value); 119 | } 120 | 121 | void BM8563::getTime(BM8563_TimeTypeDef* BM8563_TimeStruct) { 122 | uint8_t buf[3] = {0}; 123 | 124 | this->read_register(0x02, buf, 3); 125 | 126 | BM8563_TimeStruct->seconds = bcd2ToByte(buf[0] & 0x7f); 127 | BM8563_TimeStruct->minutes = bcd2ToByte(buf[1] & 0x7f); 128 | BM8563_TimeStruct->hours = bcd2ToByte(buf[2] & 0x3f); 129 | } 130 | 131 | void BM8563::setTime(BM8563_TimeTypeDef* BM8563_TimeStruct) { 132 | if (BM8563_TimeStruct == NULL) { 133 | return; 134 | } 135 | uint8_t buf[3] = { 136 | byteToBcd2(BM8563_TimeStruct->seconds), 137 | byteToBcd2(BM8563_TimeStruct->minutes), 138 | byteToBcd2(BM8563_TimeStruct->hours) 139 | }; 140 | 141 | this->write_register(0x02, buf, 3); 142 | } 143 | 144 | void BM8563::getDate(BM8563_DateTypeDef* BM8563_DateStruct) { 145 | uint8_t buf[4] = {0}; 146 | this->read_register(0x05, buf, sizeof(buf)); 147 | 148 | BM8563_DateStruct->day = bcd2ToByte(buf[0] & 0x3f); 149 | BM8563_DateStruct->week = bcd2ToByte(buf[1] & 0x07); 150 | BM8563_DateStruct->month = bcd2ToByte(buf[2] & 0x1f); 151 | 152 | uint8_t year_byte = bcd2ToByte(buf[3] & 0xff); 153 | ESP_LOGD(TAG, "Year byte is %i", year_byte); 154 | if (buf[2] & 0x80) { 155 | BM8563_DateStruct->year = 1900 + year_byte; 156 | } else { 157 | BM8563_DateStruct->year = 2000 + year_byte; 158 | } 159 | } 160 | 161 | void BM8563::setDate(BM8563_DateTypeDef* BM8563_DateStruct) { 162 | if (BM8563_DateStruct == NULL) { 163 | return; 164 | } 165 | uint8_t buf[4] = { 166 | byteToBcd2(BM8563_DateStruct->day), 167 | byteToBcd2(BM8563_DateStruct->week), 168 | byteToBcd2(BM8563_DateStruct->month), 169 | byteToBcd2((uint8_t)(BM8563_DateStruct->year % 100)), 170 | }; 171 | 172 | 173 | if (BM8563_DateStruct->year < 2000) { 174 | buf[2] = byteToBcd2(BM8563_DateStruct->month) | 0x80; 175 | } else { 176 | buf[2] = byteToBcd2(BM8563_DateStruct->month) | 0x00; 177 | } 178 | 179 | ESP_LOGI(TAG, "Writing year is %i", buf[3]); 180 | this->write_register(0x05, buf, 4); 181 | } 182 | 183 | void BM8563::WriteReg(uint8_t reg, uint8_t data) { 184 | this->write_byte(reg, data); 185 | } 186 | 187 | uint8_t BM8563::ReadReg(uint8_t reg) { 188 | uint8_t data; 189 | this->read_register(reg, &data, 1); 190 | return data; 191 | } 192 | 193 | int BM8563::SetAlarmIRQ(int afterSeconds) { 194 | ESP_LOGI(TAG, "Sleep Duration: %u ms", afterSeconds); 195 | uint8_t reg_value = 0; 196 | reg_value = ReadReg(0x01); 197 | 198 | if (afterSeconds < 0) { 199 | reg_value &= ~(1 << 0); 200 | WriteReg(0x01, reg_value); 201 | reg_value = 0x03; 202 | WriteReg(0x0E, reg_value); 203 | return -1; 204 | } 205 | 206 | uint8_t type_value = 2; 207 | uint8_t div = 1; 208 | if (afterSeconds > 255) { 209 | div = 60; 210 | type_value = 0x83; 211 | } else { 212 | type_value = 0x82; 213 | } 214 | 215 | afterSeconds = (afterSeconds / div) & 0xFF; 216 | WriteReg(0x0F, afterSeconds); 217 | WriteReg(0x0E, type_value); 218 | 219 | reg_value |= (1 << 0); 220 | reg_value &= ~(1 << 7); 221 | WriteReg(0x01, reg_value); 222 | return afterSeconds * div; 223 | } 224 | 225 | void BM8563::clearIRQ() { 226 | uint8_t data = ReadReg(0x01); 227 | WriteReg(0x01, data & 0xf3); 228 | } 229 | 230 | void BM8563::disableIRQ() { 231 | clearIRQ(); 232 | uint8_t data = ReadReg(0x01); 233 | WriteReg(0x01, data & 0xfC); 234 | } 235 | 236 | } // namespace bm8563 237 | } // namespace esphome 238 | -------------------------------------------------------------------------------- /components/it8951e/it8951e.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "esphome/core/component.h" 4 | #include "esphome/core/version.h" 5 | #include "esphome/components/spi/spi.h" 6 | #include "esphome/components/display/display_buffer.h" 7 | 8 | namespace esphome { 9 | namespace it8951e { 10 | 11 | enum it8951eModel 12 | { 13 | M5EPD = 0, 14 | it8951eModelsEND // MUST be last 15 | }; 16 | 17 | #if ESPHOME_VERSION_CODE >= VERSION_CODE(2023, 12, 0) 18 | class IT8951ESensor : public display::DisplayBuffer, 19 | #else 20 | class IT8951ESensor : public PollingComponent, public display::DisplayBuffer, 21 | #endif // VERSION_CODE(2023, 12, 0) 22 | public spi::SPIDevice { 24 | public: 25 | float get_loop_priority() const override { return 0.0f; }; 26 | float get_setup_priority() const override { return setup_priority::PROCESSOR; }; 27 | 28 | /* 29 | ---------------------------------------- Refresh mode description 30 | ---------------------------------------- INIT The initialization (INIT) mode is 31 | used to completely erase the display and leave it in the white state. It is 32 | useful for situations where the display information in memory is not a faithful 33 | representation of the optical state of the display, for example, after the 34 | device receives power after it has been fully powered down. This waveform 35 | switches the display several times and leaves it in the white state. 36 | 37 | DU 38 | The direct update (DU) is a very fast, non-flashy update. This mode supports 39 | transitions from any graytone to black or white only. It cannot be used to 40 | update to any graytone other than black or white. The fast update time for this 41 | mode makes it useful for response to touch sensor or pen input or menu selection 42 | indictors. 43 | 44 | GC16 45 | The grayscale clearing (GC16) mode is used to update the full display and 46 | provide a high image quality. When GC16 is used with Full Display Update the 47 | entire display will update as the new image is written. If a Partial Update 48 | command is used the only pixels with changing graytone values will update. The 49 | GC16 mode has 16 unique gray levels. 50 | 51 | GL16 52 | The GL16 waveform is primarily used to update sparse content on a white 53 | background, such as a page of anti-aliased text, with reduced flash. The GL16 54 | waveform has 16 unique gray levels. 55 | 56 | GLR16 57 | The GLR16 mode is used in conjunction with an image preprocessing algorithm to 58 | update sparse content on a white background with reduced flash and reduced image 59 | artifacts. The GLR16 mode supports 16 graytones. If only the even pixel states 60 | are used (0, 2, 4, … 30), the mode will behave exactly as a traditional GL16 61 | waveform mode. If a separately-supplied image preprocessing algorithm is used, 62 | the transitions invoked by the pixel states 29 and 31 are used to improve 63 | display quality. For the AF waveform, it is assured that the GLR16 waveform data 64 | will point to the same voltage lists as the GL16 data and does not need to be 65 | stored in a separate memory. 66 | 67 | GLD16 68 | The GLD16 mode is used in conjunction with an image preprocessing algorithm to 69 | update sparse content on a white background with reduced flash and reduced image 70 | artifacts. It is recommended to be used only with the full display update. The 71 | GLD16 mode supports 16 graytones. If only the even pixel states are used (0, 2, 72 | 4, … 30), the mode will behave exactly as a traditional GL16 waveform mode. If a 73 | separately-supplied image preprocessing algorithm is used, the transitions 74 | invoked by the pixel states 29 and 31 are used to refresh the background with a 75 | lighter flash compared to GC16 mode following a predetermined pixel map as 76 | encoded in the waveform file, and reduce image artifacts even more compared to 77 | the GLR16 mode. For the AF waveform, it is assured that the GLD16 waveform data 78 | will point to the same voltage lists as the GL16 data and does not need to be 79 | stored in a separate memory. 80 | 81 | DU4 82 | The DU4 is a fast update time (similar to DU), non-flashy waveform. This mode 83 | supports transitions from any gray tone to gray tones 1,6,11,16 represented by 84 | pixel states [0 10 20 30]. The combination of fast update time and four gray 85 | tones make it useful for anti-aliased text in menus. There is a moderate 86 | increase in ghosting compared with GC16. 87 | 88 | A2 89 | The A2 mode is a fast, non-flash update mode designed for fast paging turning or 90 | simple black/white animation. This mode supports transitions from and to black 91 | or white only. It cannot be used to update to any graytone other than black or 92 | white. The recommended update sequence to transition into repeated A2 updates is 93 | shown in Figure 1. The use of a white image in the transition from 4-bit to 94 | 1-bit images will reduce ghosting and improve image quality for A2 updates. 95 | 96 | */ 97 | 98 | struct IT8951DevInfo_s 99 | { 100 | int usPanelW; 101 | int usPanelH; 102 | uint16_t usImgBufAddrL; 103 | uint16_t usImgBufAddrH; 104 | char usFWVersion[16]; 105 | char usLUTVersion[16]; 106 | }; 107 | 108 | struct IT8951Dev_s 109 | { 110 | struct IT8951DevInfo_s devInfo; 111 | display::DisplayType displayType; 112 | }; 113 | 114 | enum update_mode_e // Typical 115 | { // Ghosting Update Time Usage 116 | UPDATE_MODE_INIT = 0, // * N/A 2000ms Display initialization, 117 | UPDATE_MODE_DU = 1, // Low 260ms Monochrome menu, text input, and touch screen input 118 | UPDATE_MODE_GC16 = 2, // * Very Low 450ms High quality images 119 | UPDATE_MODE_GL16 = 3, // * Medium 450ms Text with white background 120 | UPDATE_MODE_GLR16 = 4, // Low 450ms Text with white background 121 | UPDATE_MODE_GLD16 = 5, // Low 450ms Text and graphics with white background 122 | UPDATE_MODE_DU4 = 6, // * Medium 120ms Fast page flipping at reduced contrast 123 | UPDATE_MODE_A2 = 7, // Medium 290ms Anti-aliased text in menus / touch and screen input 124 | UPDATE_MODE_NONE = 8 125 | }; // The ones marked with * are more commonly used 126 | 127 | void set_reset_pin(GPIOPin *reset) { this->reset_pin_ = reset; } 128 | void set_busy_pin(GPIOPin *busy) { this->busy_pin_ = busy; } 129 | 130 | void set_reversed(bool reversed) { this->reversed_ = reversed; } 131 | void set_reset_duration(uint32_t reset_duration) { this->reset_duration_ = reset_duration; } 132 | void set_model(it8951eModel model); 133 | void set_sleep_when_done(bool sleep_when_done) { this->sleep_when_done_ = sleep_when_done; } 134 | void set_full_update_every(uint32_t full_update_every) { this->full_update_every_ = full_update_every; } 135 | 136 | void setup() override; 137 | void update() override; 138 | void update_slow(); 139 | void dump_config() override; 140 | 141 | display::DisplayType get_display_type() override { return IT8951DevAll[this->model_].displayType; } 142 | 143 | void clear(bool init); 144 | 145 | void fill(Color color) override; 146 | void draw_pixel_at(int x, int y, Color color) override; 147 | void write_display(update_mode_e mode); 148 | 149 | protected: 150 | void draw_absolute_pixel_internal(int x, int y, Color color) override; 151 | 152 | int get_width_internal() override { return usPanelW_; }; 153 | 154 | int get_height_internal() override { return usPanelH_; }; 155 | 156 | uint32_t get_buffer_length_(); 157 | 158 | 159 | private: 160 | struct IT8951Dev_s IT8951DevAll[it8951eModel::it8951eModelsEND] 161 | { // it8951eModel::M5EPD 162 | 960, // .devInfo.usPanelW 163 | 540, // .devInfo.usPanelH 164 | 0x36E0, // .devInfo.usImgBufAddrL 165 | 0x0012, // .devInfo.usImgBufAddrH 166 | "", // .devInfo.usFWVersion 167 | "", // .devInfo.usFWVersion 168 | display::DisplayType::DISPLAY_TYPE_GRAYSCALE // .displayType (M5EPD supports 16 gray scale levels) 169 | }; 170 | 171 | void get_device_info(struct IT8951DevInfo_s *info); 172 | 173 | int max_x = 0; 174 | int max_y = 0; 175 | int min_x = 960; 176 | int min_y = 540; 177 | uint16_t m_endian_type = 0; 178 | uint16_t m_pix_bpp = 0; 179 | uint8_t _it8951_rotation = 0; 180 | 181 | GPIOPin *reset_pin_{nullptr}; 182 | GPIOPin *busy_pin_{nullptr}; 183 | 184 | int usPanelW_{0}; 185 | int usPanelH_{0}; 186 | bool reversed_{false}; 187 | uint32_t reset_duration_{100}; 188 | bool sleep_when_done_{true}; // If true, the display will go to sleep after each update 189 | uint32_t full_update_every_{60}; // Full screen refresh every 60 updates 190 | uint32_t partial_update_{0}; // Partial update counter 191 | enum it8951eModel model_{it8951eModel::M5EPD}; 192 | 193 | void reset(void); 194 | 195 | /* 1000ms timeout because I've seen it take up to 750ms (and ~310ms on average) 196 | * Mostly for screen sleep and run commands */ 197 | void wait_busy(uint32_t timeout = 1000); 198 | void check_busy(uint32_t timeout = 1000); 199 | 200 | uint16_t get_vcom(); 201 | void set_vcom(uint16_t vcom); 202 | 203 | // comes from ref driver code from waveshare 204 | uint16_t read_word(); 205 | void read_words(void *buf, uint32_t length); 206 | 207 | void write_two_byte16(uint16_t type, uint16_t cmd); 208 | void write_command(uint16_t cmd); 209 | void write_word(uint16_t cmd); 210 | void write_reg(uint16_t addr, uint16_t data); 211 | void set_target_memory_addr(uint16_t tar_addrL, uint16_t tar_addrH); 212 | void write_args(uint16_t cmd, uint16_t *args, uint16_t length); 213 | 214 | void set_area(uint16_t x, uint16_t y, uint16_t w, uint16_t h); 215 | void update_area(uint16_t x, uint16_t y, uint16_t w, 216 | uint16_t h, update_mode_e mode); 217 | 218 | 219 | 220 | void write_buffer_to_display(uint16_t x, uint16_t y, uint16_t w, 221 | uint16_t h, const uint8_t *gram); 222 | }; 223 | 224 | template class ClearAction : public Action, public Parented { 225 | public: 226 | void play(Ts... x) override { this->parent_->clear(true); } 227 | }; 228 | 229 | template class UpdateSlowAction : public Action, public Parented { 230 | public: 231 | void play(Ts... x) override { this->parent_->update_slow(); } 232 | }; 233 | 234 | template class DrawAction : public Action, public Parented { 235 | public: 236 | void play(Ts... x) override { this->parent_->write_display(IT8951ESensor::UPDATE_MODE_DU); } 237 | }; 238 | 239 | } // namespace it8951e 240 | } // namespace esphome 241 | -------------------------------------------------------------------------------- /components/it8951e/it8951e.cpp: -------------------------------------------------------------------------------- 1 | #include "esphome/core/log.h" 2 | #include "it8951e.h" 3 | #include "it8951.h" 4 | #include "esphome/core/application.h" 5 | #include "esphome/core/gpio.h" 6 | 7 | namespace esphome { 8 | namespace it8951e { 9 | 10 | static const char *TAG = "it8951e.display"; 11 | 12 | void IT8951ESensor::write_two_byte16(uint16_t type, uint16_t cmd) { 13 | this->wait_busy(); 14 | this->enable(); 15 | 16 | this->write_byte16(type); 17 | this->wait_busy(); 18 | this->write_byte16(cmd); 19 | 20 | this->disable(); 21 | } 22 | 23 | uint16_t IT8951ESensor::read_word() { 24 | this->wait_busy(); 25 | this->enable(); 26 | this->write_byte16(0x1000); 27 | this->wait_busy(); 28 | 29 | // dummy 30 | this->write_byte16(0x0000); 31 | this->wait_busy(); 32 | 33 | uint8_t recv[2]; 34 | this->read_array(recv, sizeof(recv)); 35 | uint16_t word = encode_uint16(recv[0], recv[1]); 36 | 37 | this->disable(); 38 | return word; 39 | } 40 | 41 | void IT8951ESensor::read_words(void *buf, uint32_t length) { 42 | ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); 43 | uint16_t *buffer = allocator.allocate(length); 44 | if (buffer == nullptr) { 45 | ESP_LOGE(TAG, "Read FAILED to allocate."); 46 | return; 47 | } 48 | 49 | this->wait_busy(); 50 | this->enable(); 51 | this->write_byte16(0x1000); 52 | this->wait_busy(); 53 | 54 | // dummy 55 | this->write_byte16(0x0000); 56 | this->wait_busy(); 57 | 58 | for (size_t i = 0; i < length; i++) { 59 | uint8_t recv[2]; 60 | this->read_array(recv, sizeof(recv)); 61 | buffer[i] = encode_uint16(recv[0], recv[1]); 62 | } 63 | 64 | this->disable(); 65 | 66 | memcpy(buf, buffer, length); 67 | 68 | allocator.deallocate(buffer, length); 69 | } 70 | 71 | void IT8951ESensor:: write_command(uint16_t cmd) { 72 | this->write_two_byte16(0x6000, cmd); 73 | } 74 | 75 | void IT8951ESensor::write_word(uint16_t cmd) { 76 | this->write_two_byte16(0x0000, cmd); 77 | } 78 | 79 | void IT8951ESensor::write_reg(uint16_t addr, uint16_t data) { 80 | this->write_command(0x0011); // tcon write reg command 81 | this->wait_busy(); 82 | this->enable(); 83 | this->write_byte(0x0000); // Preamble 84 | this->wait_busy(); 85 | this->write_byte16(addr); 86 | this->wait_busy(); 87 | this->write_byte16(data); 88 | this->disable(); 89 | } 90 | 91 | void IT8951ESensor::set_target_memory_addr(uint16_t tar_addrL, uint16_t tar_addrH) { 92 | this->write_reg(IT8951_LISAR + 2, tar_addrH); 93 | this->write_reg(IT8951_LISAR, tar_addrL); 94 | } 95 | 96 | void IT8951ESensor::write_args(uint16_t cmd, uint16_t *args, uint16_t length) { 97 | this->write_command(cmd); 98 | for (uint16_t i = 0; i < length; i++) { 99 | this->write_word(args[i]); 100 | } 101 | } 102 | 103 | void IT8951ESensor::set_area(uint16_t x, uint16_t y, uint16_t w, 104 | uint16_t h) { 105 | 106 | if (0 == x && 0 == y && w == this->get_width_internal() && h == this->get_height_internal()) { 107 | // Full screen 108 | uint16_t args[1]; 109 | args[0] = (this->m_endian_type << 8 | this->m_pix_bpp << 4); 110 | this->write_args(IT8951_TCON_LD_IMG, args, sizeof(args)); 111 | } 112 | else { 113 | // Partial update 114 | uint16_t args[5]; 115 | args[0] = (this->m_endian_type << 8 | this->m_pix_bpp << 4); 116 | args[1] = x; 117 | args[2] = y; 118 | args[3] = w; 119 | args[4] = h; 120 | this->write_args(IT8951_TCON_LD_IMG_AREA, args, sizeof(args)); 121 | } 122 | } 123 | 124 | void IT8951ESensor::wait_busy(uint32_t timeout) { 125 | const uint32_t start_time = millis(); 126 | while (1) { 127 | if (this->busy_pin_->digital_read()) { 128 | break; 129 | } 130 | 131 | if (millis() - start_time > timeout) { 132 | ESP_LOGE(TAG, "Pin busy timeout"); 133 | break; 134 | } 135 | } 136 | } 137 | 138 | void IT8951ESensor::check_busy(uint32_t timeout) { 139 | const uint32_t start_time = millis(); 140 | while (1) { 141 | this->write_command(IT8951_TCON_REG_RD); 142 | this->write_word(IT8951_LUTAFSR); 143 | uint16_t word = this->read_word(); 144 | if (word == 0) { 145 | break; 146 | } 147 | 148 | if (millis() - start_time > timeout) { 149 | ESP_LOGE(TAG, "SPI busy timeout %i", word); 150 | break; 151 | } 152 | 153 | } 154 | } 155 | 156 | void IT8951ESensor::update_area(uint16_t x, uint16_t y, uint16_t w, 157 | uint16_t h, update_mode_e mode) { 158 | if (mode == update_mode_e::UPDATE_MODE_NONE) { 159 | return; 160 | } 161 | 162 | this->check_busy(); 163 | 164 | uint16_t args[7]; 165 | args[0] = x; 166 | args[1] = y; 167 | args[2] = w; 168 | args[3] = h; 169 | args[4] = mode; 170 | args[5] = this->IT8951DevAll[this->model_].devInfo.usImgBufAddrL; 171 | args[6] = this->IT8951DevAll[this->model_].devInfo.usImgBufAddrH; 172 | 173 | this->write_args(IT8951_I80_CMD_DPY_BUF_AREA, args, sizeof(args)); 174 | } 175 | 176 | void IT8951ESensor::reset(void) { 177 | this->reset_pin_->digital_write(true); 178 | this->reset_pin_->digital_write(false); 179 | delay(this->reset_duration_); 180 | this->reset_pin_->digital_write(true); 181 | delay(100); 182 | } 183 | 184 | uint32_t IT8951ESensor::get_buffer_length_() { return this->get_width_internal() * this->get_height_internal(); } 185 | 186 | void IT8951ESensor::get_device_info(struct IT8951DevInfo_s *info) { 187 | this->write_command(IT8951_I80_CMD_GET_DEV_INFO); 188 | this->read_words(info, sizeof(struct IT8951DevInfo_s)/2); // Polling HRDY for each words(2-bytes) if possible 189 | } 190 | 191 | uint16_t IT8951ESensor::get_vcom() { 192 | this->write_command(IT8951_I80_CMD_VCOM); // tcon vcom get command 193 | this->write_word(0x0000); 194 | const uint16_t vcom = this->read_word(); 195 | ESP_LOGI(TAG, "VCOM = %.02fV", (float)vcom/1000); 196 | return vcom; 197 | } 198 | 199 | void IT8951ESensor::set_vcom(uint16_t vcom) { 200 | this->write_command(IT8951_I80_CMD_VCOM); // tcon vcom set command 201 | this->write_word(0x0001); 202 | this->write_word(vcom); 203 | } 204 | 205 | void IT8951ESensor::setup() { 206 | ESP_LOGCONFIG(TAG, "Init Starting."); 207 | this->spi_setup(); 208 | 209 | if (nullptr != this->reset_pin_) { 210 | this->reset_pin_->pin_mode(gpio::FLAG_OUTPUT); 211 | this->reset(); 212 | } 213 | 214 | this->busy_pin_->pin_mode(gpio::FLAG_INPUT); 215 | 216 | /* Not reliable, hard-coded in the model device info (same as M5Stack) */ 217 | //this->get_device_info(&(this->IT8951DevAll[this->model_].devInfo)); 218 | this->dump_config(); 219 | 220 | this->write_command(IT8951_TCON_SYS_RUN); 221 | 222 | // enable pack write 223 | this->write_reg(IT8951_I80CPCR, 0x0001); 224 | 225 | // set vcom to -2.30v 226 | const uint16_t vcom = this->get_vcom(); 227 | if (2300 != vcom) { 228 | this->set_vcom(2300); 229 | this->get_vcom(); 230 | } 231 | 232 | // Allocate display buffer 233 | this->init_internal_(this->get_buffer_length_()); 234 | 235 | ESP_LOGCONFIG(TAG, "Init Done."); 236 | } 237 | 238 | /** @brief Write the image at the specified location, Partial update 239 | * @param x Update X coordinate, >>> Must be a multiple of 4 <<< 240 | * @param y Update Y coordinate 241 | * @param w width of gram, >>> Must be a multiple of 4 <<< 242 | * @param h height of gram 243 | * @param gram 4bpp gram data 244 | */ 245 | void IT8951ESensor::write_buffer_to_display(uint16_t x, uint16_t y, uint16_t w, 246 | uint16_t h, const uint8_t *gram) { 247 | this->m_endian_type = IT8951_LDIMG_B_ENDIAN; 248 | this->m_pix_bpp = IT8951_4BPP; 249 | 250 | this->set_target_memory_addr(this->IT8951DevAll[this->model_].devInfo.usImgBufAddrL, this->IT8951DevAll[this->model_].devInfo.usImgBufAddrH); 251 | this->set_area(x, y, w, h); 252 | 253 | const uint16_t bytewidth = this->usPanelW_ >> 1; // bytes per row (2 pixels per byte) 254 | 255 | this->enable(); 256 | /* Send data preamble */ 257 | this->write_byte16(0x0000); 258 | 259 | ESP_LOGW(TAG, "Transfering %d x %d @ %d,%d", w, h, x, y); 260 | 261 | for (uint32_t row = 0; row < h; ++row) { 262 | for (uint32_t col = 0; col < w; col += 2) { 263 | // Calculate buffer index for packed 4bpp (2 pixels per byte) 264 | uint32_t buf_index = (y + row) * bytewidth + ((x + col) >> 1); 265 | uint8_t data = gram[buf_index]; 266 | if (!this->reversed_) { 267 | data = 0xFF - data; 268 | } 269 | this->transfer_byte(data); 270 | } 271 | } 272 | 273 | this->disable(); 274 | 275 | this->write_command(IT8951_TCON_LD_IMG_END); 276 | } 277 | 278 | void IT8951ESensor::write_display(update_mode_e mode) { 279 | const bool sleep_when_done = this->sleep_when_done_; // For consistency 280 | if (sleep_when_done) { 281 | this->write_command(IT8951_TCON_SYS_RUN); 282 | } 283 | this->partial_update_++; 284 | if (this->full_update_every_ > 0 && this->partial_update_ >= this->full_update_every_) { 285 | this->partial_update_ = 0; 286 | mode = update_mode_e::UPDATE_MODE_GC16; 287 | this->min_x = 0; 288 | this->min_y = 0; 289 | this->max_x = this->get_width_internal() - 1; 290 | this->max_y = this->get_height_internal() - 1; 291 | } 292 | else { 293 | // rounded up to be multiple of 4 294 | this->min_x = (this->min_x) & 0xFFFC; 295 | this->min_y = (this->min_y) & 0xFFFC; 296 | 297 | } 298 | const u_int32_t width = this->max_x - this->min_x + 1; 299 | const u_int32_t height = this->max_y - this->min_y + 1; 300 | if (this->min_x > this->get_width_internal() || this->min_y > this->get_height_internal()) { 301 | ESP_LOGE(TAG, "Pos (%d, %d) out of bounds.", this->min_x, this->min_y); 302 | return; 303 | } 304 | if ((this->min_x + width) > this->get_width_internal() || (this->min_y + height) > this->get_height_internal()) { 305 | ESP_LOGE(TAG, "Dim (%d, %d) out of bounds.", this->min_x + width, this->min_y + height); 306 | return; 307 | } 308 | // ESP_LOGE(TAG, "write_buffer_to_display (x %d,y %d,w %d,h %d,rot %d).", this->min_x, this->min_y, width, height, this->rotation_); 309 | 310 | this->write_buffer_to_display(this->min_x, this->min_y, width, height, this->buffer_); 311 | this->update_area(this->min_x, this->min_y, width, height, mode); 312 | this->max_x = 0; 313 | this->max_y = 0; 314 | this->min_x = this->get_width_internal(); 315 | this->min_y = this->get_height_internal(); 316 | if (sleep_when_done) { 317 | this->write_command(IT8951_TCON_SLEEP); 318 | } 319 | } 320 | 321 | /** @brief Clear graphics buffer 322 | * @param init Screen initialization, If is 0, clear the buffer without initializing 323 | */ 324 | void IT8951ESensor::clear(bool init) { 325 | this->m_endian_type = IT8951_LDIMG_L_ENDIAN; 326 | this->m_pix_bpp = IT8951_4BPP; 327 | 328 | Display::clear(); 329 | 330 | this->set_target_memory_addr(this->IT8951DevAll[this->model_].devInfo.usImgBufAddrL, this->IT8951DevAll[this->model_].devInfo.usImgBufAddrH); 331 | this->set_area(0, 0, this->get_width_internal(), this->get_height_internal()); 332 | 333 | const uint32_t maxPos = (this->get_width_internal() * this->get_height_internal()) >> 1; // 2 pixels per byte 334 | 335 | this->enable(); 336 | /* Send data preamble */ 337 | this->write_byte16(0x0000); 338 | 339 | for (uint32_t i = 0; i < maxPos; ++i) { 340 | this->transfer_byte(0xFF); 341 | } 342 | 343 | this->disable(); 344 | 345 | this->write_command(IT8951_TCON_LD_IMG_END); 346 | 347 | if (init) { 348 | this->update_area(0, 0, this->get_width_internal(), this->get_height_internal(), update_mode_e::UPDATE_MODE_INIT); 349 | } 350 | } 351 | 352 | void IT8951ESensor::update() { 353 | if (this->is_ready()) { 354 | this->do_update_(); 355 | this->write_display(update_mode_e::UPDATE_MODE_DU); // 2 level 356 | } 357 | } 358 | 359 | void IT8951ESensor::update_slow() { 360 | if (this->is_ready()) { 361 | this->do_update_(); 362 | this->write_display(update_mode_e::UPDATE_MODE_GC16); 363 | } 364 | } 365 | 366 | void IT8951ESensor::fill(Color color) { 367 | for (uint32_t i = 0; i < this->get_buffer_length_(); i++) { 368 | this->buffer_[i] = color.raw_32 & 0x0F; 369 | } 370 | this->max_x = this->get_width_internal() - 1; 371 | this->max_y = this->get_height_internal() - 1; 372 | this->min_x = 0; 373 | this->min_y = 0; 374 | } 375 | 376 | void HOT IT8951ESensor::draw_pixel_at(int x, int y, Color color) { 377 | if (!Display::get_clipping().inside(x, y)) 378 | return; // NOLINT 379 | 380 | switch (this->rotation_) { 381 | case esphome::display::DisplayRotation::DISPLAY_ROTATION_0_DEGREES: 382 | break; 383 | case esphome::display::DisplayRotation::DISPLAY_ROTATION_90_DEGREES: 384 | std::swap(x, y); 385 | x = this->usPanelW_ - x - 1; 386 | break; 387 | case esphome::display::DisplayRotation::DISPLAY_ROTATION_180_DEGREES: 388 | x = this->usPanelW_ - x - 1; 389 | y = this->usPanelH_ - y - 1; 390 | break; 391 | case esphome::display::DisplayRotation::DISPLAY_ROTATION_270_DEGREES: 392 | std::swap(x, y); 393 | y = this->usPanelH_ - y - 1; 394 | break; 395 | } 396 | this->draw_absolute_pixel_internal(x, y, color); 397 | 398 | // Removed compare to original function to speed up drawing 399 | // App.feed_wdt(); 400 | } 401 | 402 | void HOT IT8951ESensor::draw_absolute_pixel_internal(int x, int y, Color color) { 403 | // Fast path: bounds and buffer check first 404 | if (x < 0 || y < 0 || this->buffer_ == nullptr || x >= this->usPanelW_ || y >= this->usPanelH_) { 405 | return; 406 | } 407 | 408 | // Track min/max for partial updates 409 | if (x > this->max_x) { 410 | this->max_x = x; 411 | } 412 | if (y > this->max_y) { 413 | this->max_y = y; 414 | } 415 | if (x < this->min_x) { 416 | this->min_x = x; 417 | } 418 | if (y < this->min_y) { 419 | this->min_y = y; 420 | } 421 | 422 | uint32_t internal_color = color.raw_32 & 0x0F; 423 | uint16_t _bytewidth = this->usPanelW_ >> 1; 424 | 425 | uint32_t index = static_cast(y) * _bytewidth + (static_cast(x) >> 1); 426 | 427 | uint8_t &buf = this->buffer_[index]; 428 | if (x & 0x1) { 429 | if ((buf & 0x0F) == internal_color) { 430 | return; // No change, skip 431 | } 432 | // Odd pixel: lower nibble 433 | buf = (buf & 0xF0) | internal_color; 434 | } else { 435 | if ((buf & 0xF0) == internal_color) { 436 | return; // No change, skip 437 | } 438 | // Even pixel: upper nibble 439 | buf = (buf & 0x0F) | (internal_color << 4); 440 | } 441 | } 442 | 443 | void IT8951ESensor::set_model(it8951eModel model) { 444 | this->model_ = model; 445 | // Provide fast access to panel width and height 446 | usPanelW_ = IT8951DevAll[model].devInfo.usPanelW; 447 | usPanelH_ = IT8951DevAll[model].devInfo.usPanelH; 448 | } 449 | 450 | void IT8951ESensor::dump_config() { 451 | LOG_DISPLAY("", "IT8951E", this); 452 | switch (this->model_) { 453 | case it8951eModel::M5EPD: 454 | ESP_LOGCONFIG(TAG, " Model: M5EPD"); 455 | break; 456 | default: 457 | ESP_LOGCONFIG(TAG, " Model: unkown"); 458 | break; 459 | } 460 | ESP_LOGCONFIG(TAG, "LUT: %s, FW: %s, Mem:%x (%d x %d)", 461 | this->IT8951DevAll[this->model_].devInfo.usLUTVersion, 462 | this->IT8951DevAll[this->model_].devInfo.usFWVersion, 463 | this->IT8951DevAll[this->model_].devInfo.usImgBufAddrL | (this->IT8951DevAll[this->model_].devInfo.usImgBufAddrH << 16), 464 | this->IT8951DevAll[this->model_].devInfo.usPanelW, 465 | this->IT8951DevAll[this->model_].devInfo.usPanelH 466 | ); 467 | 468 | ESP_LOGCONFIG(TAG, 469 | " Sleep when done: %s\n" 470 | " Partial Updating: %s\n" 471 | " Full Update Every: %u", 472 | YESNO(this->sleep_when_done_), YESNO(this->full_update_every_ > 0), this->full_update_every_); 473 | } 474 | 475 | } // namespace it8951e 476 | } // namespace esphome 477 | -------------------------------------------------------------------------------- /.screen_base.yaml: -------------------------------------------------------------------------------- 1 | esphome: 2 | name: ${device_id} 3 | name_add_mac_suffix: true 4 | on_boot: 5 | - priority: 220.0 6 | then: 7 | - it8951e.clear 8 | - delay: 100ms 9 | - component.update: m5paper_display 10 | - priority: -100.0 11 | then: 12 | - delay: 10s 13 | - component.update: m5paper_display 14 | 15 | esp32: 16 | board: m5stack_paper 17 | flash_size: 16MB 18 | framework: 19 | type: arduino 20 | 21 | external_components: 22 | - source: github://Passific/m5paper_esphome 23 | 24 | # Enable logging 25 | logger: 26 | level: DEBUG 27 | 28 | # Enable psram 29 | psram: 30 | 31 | # Enable Home Assistant API 32 | api: 33 | 34 | ota: 35 | 36 | globals: 37 | - id: material_icons_map 38 | type: std::map 39 | restore_value: no 40 | initial_value: | 41 | { 42 | {"mdi-weather-night", "󰖔"}, 43 | {"mdi-weather-cloudy", "󰖐"}, 44 | {"mdi-weather-cloudy-alert", "󰼯"}, 45 | {"mdi-weather-fog", "󰖑"}, 46 | {"mdi-weather-hail", "󰖒"}, 47 | {"mdi-weather-lightning-rainy", "󰙾"}, 48 | {"mdi-weather-lightning", "󰖓"}, 49 | {"mdi-weather-partly-cloudy", "󰖕"}, 50 | {"mdi-weather-night-partly-cloudy", "󰼱"}, 51 | {"mdi-weather-pouring", "󰖖"}, 52 | {"mdi-weather-rainy", "󰖗"}, 53 | {"mdi-weather-snowy-rainy", "󰙿"}, 54 | {"mdi-weather-snowy", "󰖘"}, 55 | {"mdi-weather-sunny", "󰖙"}, 56 | {"mdi-weather-windy-variant", "󰖞"}, 57 | {"mdi-weather-windy", "󰖝"}, 58 | {"mdi-cloud-question", "󰨹"}, 59 | {"mdi-thermometer", "󰔏"}, 60 | {"mdi-water-percent", "󰖎"}, 61 | {"mdi-molecule-co2", "󰟤"}, 62 | {"mdi-wind-power-outline", "󱪉"}, 63 | {"mdi-home-outline", "󰚡"}, 64 | {"mdi-tree-outline", "󰹩"}, 65 | {"mdi-gauge", "󰊚"}, 66 | {"mdi-battery-high", "󱊣"}, 67 | {"mdi-battery-medium", "󱊢"}, 68 | {"mdi-battery-low", "󱊡"}, 69 | {"mdi-battery-alert-variant-outline", "󱃍"}, 70 | {"mdi-battery-charging-high", "󱊦"}, 71 | {"mdi-battery-unknown", "󰂑"}, 72 | {"mdi-shield-outline", "󰒙"}, 73 | {"mdi-shield-home-outline", "󰳋"}, 74 | {"mdi-shield-lock-outline", "󰳌"}, 75 | {"mdi-shield-moon-outline", "󱠩"}, 76 | {"mdi-shield-alert-outline", "󰻍"}, 77 | {"mdi-molecule-co2", "󰟤"}, 78 | {"mdi-radioactive", "󰐼"}, 79 | {"mdi-numeric-0-circle-outline", "󰲟"}, 80 | {"mdi-numeric-1-circle-outline", "󰲡"}, 81 | {"mdi-numeric-2-circle-outline", "󰲣"}, 82 | {"mdi-numeric-3-circle-outline", "󰲥"}, 83 | {"mdi-numeric-4-circle-outline", "󰲧"}, 84 | {"mdi-numeric-5-circle-outline", "󰲩"}, 85 | {"mdi-numeric-6-circle-outline", "󰲫"}, 86 | {"mdi-numeric-7-circle-outline", "󰲭"}, 87 | {"mdi-numeric-8-circle-outline", "󰲯"}, 88 | {"mdi-numeric-9-plus-circle-outline", "󰲳"}, 89 | } 90 | 91 | font: 92 | - file: 'gfonts://Roboto' 93 | id: normal_font 94 | size: 40 95 | - file: 'gfonts://Roboto@medium' 96 | id: condition_font 97 | size: 80 98 | - file: "gfonts://Orbitron" 99 | id: clock_font 100 | size: 80 101 | - file: "fonts/materialdesignicons-webfont.ttf" 102 | id: small_icons_font 103 | size: 50 104 | glyphs: [ 105 | '󰖖', #mdi-weather-pouring 106 | '󰔏', #mdi-thermometer 107 | '󰖎', #mdi-water-percent 108 | '󰟤', #mdi-molecule-co2 109 | '󱪉', #mdi-wind-power-outline 110 | '󰚡', #mdi-home-outline 111 | '󰹩', #mdi-tree-outline 112 | '󰊚', #mdi-gauge 113 | ] 114 | - file: "fonts/materialdesignicons-webfont.ttf" 115 | id: weather_font 116 | size: 256 117 | glyphs: [ 118 | '󰖔', #mdi-weather-night 119 | '󰖐', #mdi-weather-cloudy 120 | '󰼯', #mdi-weather-cloudy-alert 121 | '󰖑', #mdi-weather-fog 122 | '󰖒', #mdi-weather-hail 123 | '󰙾', #mdi-weather-lightning-rainy 124 | '󰖓', #mdi-weather-lightning 125 | '󰖕', #mdi-weather-partly-cloudy 126 | '󰼱', #mdi-weather-night-partly-cloudy 127 | '󰖖', #mdi-weather-pouring 128 | '󰖗', #mdi-weather-rainy 129 | '󰙿', #mdi-weather-snowy-rainy 130 | '󰖘', #mdi-weather-snowy 131 | '󰖙', #mdi-weather-sunny 132 | '󰖞', #mdi-weather-windy-variant 133 | '󰖝', #mdi-weather-windy 134 | '󰨹', #mdi-cloud-question 135 | ] 136 | - file: "fonts/materialdesignicons-webfont.ttf" 137 | id: battery_font 138 | size: 40 139 | glyphs: [ 140 | '󱊣', #mdi-battery-high 141 | '󱊢', #mdi-battery-medium 142 | '󱊡', #mdi-battery-low 143 | '󱃍', #mdi-battery-alert-variant-outline 144 | '󱊦', #mdi-battery-charging-high 145 | '󰂑', #mdi-battery-unknown 146 | ] 147 | - file: "fonts/materialdesignicons-webfont.ttf" 148 | id: alarms_font 149 | size: 80 150 | glyphs: [ 151 | '󰒙', #mdi-shield-outline 152 | '󰳋', #mdi-shield-home-outline 153 | '󰳌', #mdi-shield-lock-outline 154 | '󱠩', #mdi-shield-moon-outline 155 | '󰻍', #mdi-shield-alert-outline 156 | '󰟤', #mdi-molecule-co2 157 | '󰐼', #mdi-radioactive 158 | '󰲟', #mdi-numeric-0-circle-outline 159 | '󰲡', #mdi-numeric-1-circle-outline 160 | '󰲣', #mdi-numeric-2-circle-outline 161 | '󰲥', #mdi-numeric-3-circle-outline 162 | '󰲧', #mdi-numeric-4-circle-outline 163 | '󰲩', #mdi-numeric-5-circle-outline 164 | '󰲫', #mdi-numeric-6-circle-outline 165 | '󰲭', #mdi-numeric-7-circle-outline 166 | '󰲯', #mdi-numeric-8-circle-outline 167 | '󰲳', #mdi-numeric-9-plus-circle-outline 168 | ] 169 | 170 | spi: 171 | clk_pin: GPIO14 172 | mosi_pin: GPIO12 173 | miso_pin: GPIO13 174 | 175 | i2c: 176 | sda: GPIO21 177 | scl: GPIO22 178 | 179 | display: 180 | - platform: it8951e 181 | id: m5paper_display 182 | model: M5EPD 183 | cs_pin: GPIO15 184 | reset_pin: GPIO23 185 | reset_duration: 100ms 186 | busy_pin: GPIO27 187 | rotation: 0 188 | reversed: False 189 | update_interval: never 190 | lambda: |- 191 | // Clock 192 | it.strftime(25, 420, id(clock_font), "%H:%M", id(rtc_time).now()); 193 | // Weather icon 194 | Color weather_icon_color; 195 | weather_icon_color.raw_32 = 0x0D; 196 | #if 0 // Use for testing 197 | std::string _conditions[] { 198 | "clear-night", 199 | "cloudy", 200 | "exceptional", 201 | "fog", 202 | "hail", 203 | "lightning-rainy", 204 | "lightning", 205 | "partlycloudy", 206 | "pouring", 207 | "rainy", 208 | "snowy-rainy", 209 | "snowy", 210 | "sunny", 211 | "windy", 212 | "windy-variant", 213 | }; 214 | static int _cond_index = 0; 215 | std::string conditions = _conditions[_cond_index]; 216 | _cond_index = (_cond_index + 1) % 15; 217 | #else 218 | std::string conditions = id(weather_conditions).state; 219 | #endif 220 | std::string img = id(material_icons_map)["mdi-cloud-question"]; 221 | if (conditions == "clear-night") 222 | { 223 | conditions = "clear"; 224 | img = id(material_icons_map)["mdi-weather-night"]; 225 | } 226 | else if (conditions == "cloudy") 227 | img = id(material_icons_map)["mdi-weather-cloudy"]; 228 | else if (conditions == "exceptional") 229 | img = id(material_icons_map)["mdi-weather-cloudy-alert"]; 230 | else if (conditions == "fog") 231 | img = id(material_icons_map)["mdi-weather-fog"]; 232 | else if (conditions == "hail") 233 | img = id(material_icons_map)["mdi-weather-hail"]; 234 | else if (conditions == "lightning-rainy") 235 | { 236 | conditions = "thunderstorm"; 237 | img = id(material_icons_map)["mdi-weather-lightning-rainy"]; 238 | } 239 | else if (conditions == "lightning") 240 | img = id(material_icons_map)["mdi-weather-lightning"]; 241 | else if (conditions == "partlycloudy") 242 | { 243 | bool is_night = id(sun_position).state == "below_horizon"; 244 | if (is_night) 245 | img = id(material_icons_map)["mdi-weather-night-partly-cloudy"]; 246 | else 247 | img = id(material_icons_map)["mdi-weather-partly-cloudy"]; 248 | conditions = "partly cloudy"; 249 | } 250 | else if (conditions == "pouring") 251 | { 252 | img = id(material_icons_map)["mdi-weather-pouring"]; 253 | conditions = "shower"; 254 | } 255 | else if (conditions == "rainy") 256 | img = id(material_icons_map)["mdi-weather-rainy"]; 257 | else if (conditions == "snowy-rainy") 258 | { 259 | img = id(material_icons_map)["mdi-weather-snowy-rainy"]; 260 | conditions = "sleet"; 261 | } 262 | else if (conditions == "snowy") 263 | img = id(material_icons_map)["mdi-weather-snowy"]; 264 | else if (conditions == "sunny") 265 | img = id(material_icons_map)["mdi-weather-sunny"]; 266 | else if (conditions == "windy") 267 | img = id(material_icons_map)["mdi-weather-windy"]; 268 | else if (conditions == "windy-variant") 269 | { 270 | img = id(material_icons_map)["mdi-weather-windy-variant"]; 271 | conditions = "windy"; 272 | } 273 | it.print(25, 55, id(weather_font), weather_icon_color, img.c_str()); 274 | uint base_y = 25; 275 | if (!conditions.empty()) 276 | { 277 | conditions[0] = toupper(conditions[0]); 278 | it.printf(295, base_y, id(condition_font), conditions.c_str()); 279 | } 280 | #define NOT_NAN(x) ((x) == (x)) 281 | // Outdoor 282 | it.print(295, base_y + 100, id(small_icons_font), weather_icon_color, id(material_icons_map)["mdi-tree-outline"].c_str()); 283 | it.print(355, base_y + 100, id(normal_font), "Outdoor:"); 284 | // Temperature 285 | it.print(295, base_y + 150, id(small_icons_font), weather_icon_color, id(material_icons_map)["mdi-thermometer"].c_str()); 286 | float temp = id(outdoor_temperature).state; 287 | if (NOT_NAN(temp)) 288 | it.printf(355, base_y + 150, id(normal_font), "%.1f°C", temp); 289 | else 290 | it.print(355, base_y + 150, id(normal_font), "---"); 291 | // Humidity 292 | it.print(295, base_y + 200, id(small_icons_font), weather_icon_color, id(material_icons_map)["mdi-water-percent"].c_str()); 293 | float humidity = id(outdoor_humidity).state; 294 | if (NOT_NAN(humidity)) 295 | it.printf(355, base_y + 200, id(normal_font), "%.1f%%", humidity); 296 | else 297 | it.print(355, base_y + 200, id(normal_font), "---"); 298 | // Rainfall 299 | it.print(295, base_y + 250, id(small_icons_font), weather_icon_color, id(material_icons_map)["mdi-weather-pouring"].c_str()); 300 | float rainfall = id(outdoor_rainfall).state; 301 | if (NOT_NAN(rainfall)) 302 | it.printf(355, base_y + 250, id(normal_font), "%.1f mm", rainfall); 303 | else 304 | it.print(355, base_y + 250, id(normal_font), "---"); 305 | // Wind strength 306 | it.print(295, base_y + 300, id(small_icons_font), weather_icon_color, id(material_icons_map)["mdi-wind-power-outline"].c_str()); 307 | float wind_strength = id(outdoor_wind_strength).state; 308 | if (NOT_NAN(wind_strength)) 309 | it.printf(355, base_y + 300, id(normal_font), "%d km/h", (int)wind_strength); 310 | else 311 | it.print(355, base_y + 300, id(normal_font), "---"); 312 | // Indoor 313 | it.print(560, base_y + 100, id(small_icons_font), weather_icon_color, id(material_icons_map)["mdi-home-outline"].c_str()); 314 | it.print(620, base_y + 100, id(normal_font), "Indoor:"); 315 | // Temperature 316 | it.print(560, base_y + 150, id(small_icons_font), weather_icon_color, id(material_icons_map)["mdi-thermometer"].c_str()); 317 | temp = id(indoor_temperature).state; 318 | if (NOT_NAN(temp)) 319 | it.printf(620, base_y + 150, id(normal_font), "%.1f°C", temp); 320 | else 321 | it.print(620, base_y + 150, id(normal_font), "---"); 322 | // Humidity 323 | it.print(560, base_y + 200, id(small_icons_font), weather_icon_color, id(material_icons_map)["mdi-water-percent"].c_str()); 324 | humidity = id(indoor_humidity).state; 325 | if (NOT_NAN(humidity)) 326 | it.printf(620, base_y + 200, id(normal_font), "%.1f%%", humidity); 327 | else 328 | it.print(620, base_y + 200, id(normal_font), "---"); 329 | // Pressure 330 | it.print(560, base_y + 250, id(small_icons_font), weather_icon_color, id(material_icons_map)["mdi-gauge"].c_str()); 331 | float pressure = id(indoor_pressure).state; 332 | if (NOT_NAN(pressure)) 333 | it.printf(620, base_y + 250, id(normal_font), "%d mbar", (int)pressure); 334 | else 335 | it.print(620, base_y + 250, id(normal_font), "---"); 336 | // CO2 337 | it.print(560, base_y + 300, id(small_icons_font), weather_icon_color, id(material_icons_map)["mdi-molecule-co2"].c_str()); 338 | float co2 = id(indoor_co2).state; 339 | if (NOT_NAN(co2)) 340 | it.printf(620, base_y + 300, id(normal_font), "%d ppm", (int)co2); 341 | else 342 | it.print(620, base_y + 300, id(normal_font), "---"); 343 | // Battery 344 | std::string battery_icon = id(material_icons_map)["mdi-battery-unknown"]; 345 | float voltage = id(m5paper_battery_voltage).state; 346 | if (voltage > 4.10) 347 | battery_icon = id(material_icons_map)["mdi-battery-charging-high"]; 348 | else 349 | { 350 | float battery_level = id(m5paper_battery_level).state; 351 | if (NOT_NAN(battery_level)) 352 | { 353 | 354 | if (battery_level < 10) 355 | battery_icon = id(material_icons_map)["mdi-battery-alert-variant-outline"]; 356 | else if (battery_level < 40) 357 | battery_icon = id(material_icons_map)["mdi-battery-low"]; 358 | else if (battery_level < 70) 359 | battery_icon = id(material_icons_map)["mdi-battery-medium"]; 360 | else 361 | battery_icon = id(material_icons_map)["mdi-battery-high"]; 362 | } 363 | } 364 | it.print(910, 10, id(battery_font), battery_icon.c_str()); 365 | // Alarms 366 | Color alarm_enabled; 367 | alarm_enabled.raw_32 = 0x0E; 368 | Color alarm_disabled; 369 | alarm_disabled.raw_32 = 0x03; 370 | // Alarmo 371 | bool alarmo_enabled = false; 372 | std::string al_icon = id(material_icons_map)["mdi-shield-outline"]; 373 | std::string al_status = id(alarmo_status).state; 374 | if (al_status == "triggered") 375 | { 376 | al_icon = id(material_icons_map)["mdi-shield-alert-outline"]; 377 | alarmo_enabled = true; 378 | } 379 | else 380 | { 381 | std::string al_arm = id(alarmo_mode).state; 382 | if (al_arm == "armed_home") 383 | { 384 | al_icon = id(material_icons_map)["mdi-shield-home-outline"]; 385 | alarmo_enabled = true; 386 | } 387 | else if (al_arm == "armed_home") 388 | { 389 | al_icon = id(material_icons_map)["mdi-shield-home-outline"]; 390 | alarmo_enabled = true; 391 | } 392 | else if (al_arm == "armed_night") 393 | { 394 | al_icon = id(material_icons_map)["mdi-shield-moon-outline"]; 395 | alarmo_enabled = true; 396 | } 397 | else if (al_arm.rfind("armed_", 0) == 0) 398 | { 399 | al_icon = id(material_icons_map)["mdi-shield-lock-outline"]; 400 | alarmo_enabled = true; 401 | } 402 | } 403 | it.print(870, 100, id(alarms_font), alarmo_enabled ? alarm_enabled : alarm_disabled, al_icon.c_str()); 404 | // Radiation 405 | it.print(870, 190, id(alarms_font), id(radiation_alarm).state ? alarm_enabled : alarm_disabled, id(material_icons_map)["mdi-radioactive"].c_str()); 406 | // Lights 407 | std::string lights_icon_id; 408 | float _lights = id(lights_counter).state; 409 | if ((!NOT_NAN(_lights)) || (_lights < 0)) 410 | _lights = 0; 411 | int lights = (int)_lights; 412 | if (lights >= 9) 413 | lights_icon_id = "mdi-numeric-9-plus-circle-outline"; 414 | else 415 | { 416 | lights_icon_id = "mdi-numeric-0-circle-outline"; 417 | lights_icon_id[12] += lights; 418 | } 419 | it.print(870, 280, id(alarms_font), (lights > 0) ? alarm_enabled : alarm_disabled, id(material_icons_map)[lights_icon_id].c_str()); 420 | // Rain 421 | unsigned int g_left = 325; 422 | unsigned int g_top = 395; 423 | unsigned int g_width = 600; 424 | unsigned int g_height = 120; 425 | unsigned int max_items = 12; 426 | float max_value = 4.0; 427 | unsigned int item_width = g_width / max_items; 428 | it.rectangle(g_left, g_top, g_width, g_height); 429 | for (int i = 1; i < max_items; i++) 430 | it.line(g_left + item_width * i, g_top, g_left + item_width * i, g_top + g_height); 431 | unsigned int unit_height = (unsigned int) ((float) g_height / max_value); 432 | for (int i = 1; i < max_value; i++) 433 | it.line(g_left, g_top + g_height - i * unit_height, g_left + g_width, g_top + g_height - i * unit_height); 434 | std::string forecast_str = id(precipitation_forecast).state; 435 | size_t pos = 0; 436 | size_t counter = 0; 437 | bool last_item = false; 438 | Color graph_color; 439 | graph_color.raw_32 = 0x0F; 440 | while (!last_item) 441 | { 442 | size_t found = forecast_str.find(",", pos+1); 443 | std::string value_str = ""; 444 | if (found != std::string::npos) 445 | { 446 | value_str = forecast_str.substr(pos, found - pos); 447 | pos = found + 1; 448 | } 449 | else 450 | { 451 | value_str = forecast_str.substr(pos); 452 | last_item = true; 453 | } 454 | char *pEnd = NULL; 455 | float value = strtof(value_str.c_str(), &pEnd); 456 | //if (pEnd == NULL) 457 | { 458 | if (value > max_value) 459 | value = max_value; 460 | if (value > 0) 461 | { 462 | unsigned int h = ((max_value - value) / max_value) * g_height; 463 | it.filled_rectangle(g_left + item_width * counter, g_top + h, item_width, g_height - h, graph_color); 464 | } 465 | } 466 | counter++; 467 | if (counter >= max_items) 468 | break; 469 | } 470 | 471 | graph: 472 | - id: co2_graph 473 | sensor: indoor_co2 474 | duration: 6h 475 | min_value: 400 476 | max_value: 2400 477 | width: 600 478 | height: 120 479 | x_grid: 1h 480 | y_grid: 500 481 | border: true 482 | 483 | touchscreen: 484 | - platform: gt911 485 | display: m5paper_display 486 | id: gt911_touchscreen 487 | interrupt_pin: GPIO36 488 | update_interval: never # No pulling as interrupt pin is used 489 | calibration: 490 | x_min: 0 491 | x_max: 540 492 | y_min: 0 493 | y_max: 960 494 | 495 | wifi: 496 | ssid: !secret wifi_ssid 497 | password: !secret wifi_password 498 | power_save_mode: "HIGH" 499 | 500 | switch: 501 | - platform: restart 502 | id: restart_switch 503 | name: ${device_name} Restart 504 | 505 | time: 506 | - platform: homeassistant 507 | id: homeassistant_time 508 | timezone: Europe/Paris 509 | on_time_sync: 510 | - bm8563.write_time 511 | - platform: bm8563 512 | id: rtc_time 513 | sleep_duration: 60000ms 514 | on_time: 515 | - seconds: 0 516 | then: 517 | - component.update: m5paper_display 518 | 519 | m5paper: 520 | battery_power_pin: GPIO5 521 | main_power_pin: GPIO2 522 | 523 | sensor: 524 | - platform: adc 525 | disabled_by_default: true 526 | pin: GPIO35 527 | name: ${device_name} battery voltage 528 | id: m5paper_battery_voltage 529 | update_interval: 60s 530 | attenuation: 12db 531 | filters: 532 | - multiply: 2 #1,27272727 533 | - platform: sht3xd 534 | temperature: 535 | name: ${device_name} temperature 536 | id: m5paper_temperature 537 | device_class: "temperature" 538 | state_class: "measurement" 539 | icon: mdi:thermometer 540 | humidity: 541 | name: ${device_name} humidity 542 | id: m5paper_humidity 543 | device_class: "humidity" 544 | state_class: "measurement" 545 | icon: mdi:water-percent 546 | address: 0x44 547 | update_interval: 10s 548 | - platform: template 549 | name: ${device_name} battery level 550 | id: m5paper_battery_level 551 | unit_of_measurement: '%' 552 | device_class: "battery" 553 | state_class: "measurement" 554 | icon: mdi:battery-high 555 | update_interval: 60s 556 | lambda: |- 557 | constexpr float min_level = 3.52; 558 | constexpr float max_level = 4.15; 559 | return ((id(m5paper_battery_voltage).state - min_level) / (max_level - min_level)) * 100.00; 560 | filters: 561 | - clamp: 562 | min_value: 0 563 | max_value: 100 564 | - platform: homeassistant 565 | name: Outdoor temperature 566 | id: outdoor_temperature 567 | entity_id: ${outdoor_temperature} 568 | - platform: homeassistant 569 | name: Outdoor humidity 570 | id: outdoor_humidity 571 | entity_id: ${outdoor_humidity} 572 | - platform: homeassistant 573 | name: Rainfall last hour 574 | id: outdoor_rainfall 575 | entity_id: ${outdoor_rainfall} 576 | - platform: homeassistant 577 | name: Wind strength 578 | id: outdoor_wind_strength 579 | entity_id: ${outdoor_wind_strength} 580 | - platform: homeassistant 581 | name: Indoor temperature 582 | id: indoor_temperature 583 | entity_id: ${indoor_temperature} 584 | - platform: homeassistant 585 | name: Indoor humidity 586 | id: indoor_humidity 587 | entity_id: ${indoor_humidity} 588 | - platform: homeassistant 589 | name: Indoor pressure 590 | id: indoor_pressure 591 | entity_id: ${indoor_pressure} 592 | - platform: homeassistant 593 | name: Indoor CO2 594 | id: indoor_co2 595 | entity_id: ${indoor_co2} 596 | - platform: homeassistant 597 | name: Lights counter 598 | id: lights_counter 599 | entity_id: ${lights_counter} 600 | 601 | text_sensor: 602 | - platform: homeassistant 603 | name: Weather 604 | id: weather_conditions 605 | entity_id: ${weather_conditions} 606 | - platform: homeassistant 607 | name: Sun 608 | id: sun_position 609 | entity_id: sun.sun 610 | - platform: homeassistant 611 | name: Alarmo status 612 | id: alarmo_status 613 | entity_id: ${alarmo_status} 614 | - platform: homeassistant 615 | name: Alarmo mode 616 | id: alarmo_mode 617 | entity_id: ${alarmo_status} 618 | attribute: arm_mode 619 | - platform: homeassistant 620 | name: Forecast 621 | id: precipitation_forecast 622 | entity_id: sensor.precipitation_forecast 623 | attribute: hourly 624 | 625 | binary_sensor: 626 | - platform: gpio 627 | name: ${device_name} right button 628 | id: right_button 629 | icon: mdi:gesture-tap-button 630 | pin: 631 | number: GPIO37 632 | inverted: true 633 | on_release: 634 | - component.update: m5paper_display 635 | - platform: gpio 636 | name: ${device_name} BTN/PWR button 637 | icon: mdi:gesture-tap-button 638 | pin: 639 | number: GPIO38 640 | inverted: true 641 | - platform: gpio 642 | name: ${device_name} left button 643 | icon: mdi:gesture-tap-button 644 | pin: 645 | number: GPIO39 646 | inverted: true 647 | - platform: homeassistant 648 | name: Radiation alarm 649 | id: radiation_alarm 650 | entity_id: ${radiation_alarm} 651 | --------------------------------------------------------------------------------