From dbdcb39af903963efb6ba9e2106f42ad8495ac92 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Mon, 27 Nov 2023 07:48:38 +1100 Subject: [PATCH 01/89] Pull PollingComponent up from individual display drivers to Display. (#5444) --- .../components/addressable_light/addressable_light_display.h | 2 +- esphome/components/addressable_light/display.py | 1 - esphome/components/display/__init__.py | 3 ++- esphome/components/display/display.h | 2 +- esphome/components/ili9xxx/display.py | 1 - esphome/components/ili9xxx/ili9xxx_display.h | 3 +-- esphome/components/inkplate6/display.py | 1 - esphome/components/inkplate6/inkplate.h | 2 +- esphome/components/lcd_base/__init__.py | 1 - esphome/components/max7219/display.py | 1 - esphome/components/max7219digit/display.py | 3 +-- esphome/components/max7219digit/max7219digit.h | 3 +-- esphome/components/nextion/display.py | 1 - esphome/components/pcd8544/display.py | 1 - esphome/components/pcd8544/pcd_8544.h | 3 +-- esphome/components/pvvx_mithermometer/display/__init__.py | 1 - esphome/components/ssd1306_base/__init__.py | 1 - esphome/components/ssd1306_base/ssd1306_base.h | 2 +- esphome/components/ssd1322_base/__init__.py | 1 - esphome/components/ssd1322_base/ssd1322_base.h | 2 +- esphome/components/ssd1325_base/__init__.py | 1 - esphome/components/ssd1325_base/ssd1325_base.h | 2 +- esphome/components/ssd1327_base/__init__.py | 1 - esphome/components/ssd1327_base/ssd1327_base.h | 2 +- esphome/components/ssd1331_base/__init__.py | 1 - esphome/components/ssd1331_base/ssd1331_base.h | 2 +- esphome/components/ssd1351_base/__init__.py | 1 - esphome/components/ssd1351_base/ssd1351_base.h | 2 +- esphome/components/st7735/display.py | 1 - esphome/components/st7735/st7735.h | 3 +-- esphome/components/st7789v/display.py | 1 - esphome/components/st7789v/st7789v.h | 3 +-- esphome/components/st7920/display.py | 1 - esphome/components/st7920/st7920.h | 3 +-- esphome/components/tm1621/display.py | 1 - esphome/components/tm1637/display.py | 1 - esphome/components/tm1638/display.py | 1 - esphome/components/waveshare_epaper/display.py | 1 - esphome/components/waveshare_epaper/waveshare_epaper.h | 3 +-- 39 files changed, 19 insertions(+), 47 deletions(-) diff --git a/esphome/components/addressable_light/addressable_light_display.h b/esphome/components/addressable_light/addressable_light_display.h index 8893c39be6..f47389fd05 100644 --- a/esphome/components/addressable_light/addressable_light_display.h +++ b/esphome/components/addressable_light/addressable_light_display.h @@ -10,7 +10,7 @@ namespace esphome { namespace addressable_light { -class AddressableLightDisplay : public display::DisplayBuffer, public PollingComponent { +class AddressableLightDisplay : public display::DisplayBuffer { public: light::AddressableLight *get_light() const { return this->light_; } diff --git a/esphome/components/addressable_light/display.py b/esphome/components/addressable_light/display.py index 2f9b8cf455..327ec8296a 100644 --- a/esphome/components/addressable_light/display.py +++ b/esphome/components/addressable_light/display.py @@ -45,7 +45,6 @@ async def to_code(config): cg.add(var.set_height(config[CONF_HEIGHT])) cg.add(var.set_light(wrapped_light)) - await cg.register_component(var, config) await display.register_display(var, config) if pixel_mapper := config.get(CONF_PIXEL_MAPPER): diff --git a/esphome/components/display/__init__.py b/esphome/components/display/__init__.py index b7a8508fc8..9f4e922a37 100644 --- a/esphome/components/display/__init__.py +++ b/esphome/components/display/__init__.py @@ -58,7 +58,7 @@ BASIC_DISPLAY_SCHEMA = cv.Schema( { cv.Optional(CONF_LAMBDA): cv.lambda_, } -) +).extend(cv.polling_component_schema("1s")) FULL_DISPLAY_SCHEMA = BASIC_DISPLAY_SCHEMA.extend( { @@ -116,6 +116,7 @@ async def setup_display_core_(var, config): async def register_display(var, config): + await cg.register_component(var, config) await setup_display_core_(var, config) diff --git a/esphome/components/display/display.h b/esphome/components/display/display.h index 350fd40f26..7ce6d179ef 100644 --- a/esphome/components/display/display.h +++ b/esphome/components/display/display.h @@ -163,7 +163,7 @@ class BaseFont { virtual void measure(const char *str, int *width, int *x_offset, int *baseline, int *height) = 0; }; -class Display { +class Display : public PollingComponent { public: /// Fill the entire screen with the given color. virtual void fill(Color color); diff --git a/esphome/components/ili9xxx/display.py b/esphome/components/ili9xxx/display.py index ec96d38cf8..6882d254e1 100644 --- a/esphome/components/ili9xxx/display.py +++ b/esphome/components/ili9xxx/display.py @@ -115,7 +115,6 @@ async def to_code(config): rhs = MODELS[config[CONF_MODEL]].new() var = cg.Pvariable(config[CONF_ID], rhs) - await cg.register_component(var, config) await display.register_display(var, config) await spi.register_spi_device(var, config) dc = await cg.gpio_pin_expression(config[CONF_DC_PIN]) diff --git a/esphome/components/ili9xxx/ili9xxx_display.h b/esphome/components/ili9xxx/ili9xxx_display.h index e43585afeb..ae7c83e61f 100644 --- a/esphome/components/ili9xxx/ili9xxx_display.h +++ b/esphome/components/ili9xxx/ili9xxx_display.h @@ -19,8 +19,7 @@ enum ILI9XXXColorMode { #define ILI9XXXDisplay_DATA_RATE spi::DATA_RATE_40MHZ #endif // ILI9XXXDisplay_DATA_RATE -class ILI9XXXDisplay : public PollingComponent, - public display::DisplayBuffer, +class ILI9XXXDisplay : public display::DisplayBuffer, public spi::SPIDevice { public: diff --git a/esphome/components/inkplate6/display.py b/esphome/components/inkplate6/display.py index f05169ea2e..a1ecfdd1d6 100644 --- a/esphome/components/inkplate6/display.py +++ b/esphome/components/inkplate6/display.py @@ -110,7 +110,6 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - await cg.register_component(var, config) await display.register_display(var, config) await i2c.register_i2c_device(var, config) diff --git a/esphome/components/inkplate6/inkplate.h b/esphome/components/inkplate6/inkplate.h index 565bd74710..307d9671e6 100644 --- a/esphome/components/inkplate6/inkplate.h +++ b/esphome/components/inkplate6/inkplate.h @@ -17,7 +17,7 @@ enum InkplateModel : uint8_t { INKPLATE_6_V2 = 3, }; -class Inkplate6 : public PollingComponent, public display::DisplayBuffer, public i2c::I2CDevice { +class Inkplate6 : public display::DisplayBuffer, public i2c::I2CDevice { public: const uint8_t LUT2[16] = {0xAA, 0xA9, 0xA6, 0xA5, 0x9A, 0x99, 0x96, 0x95, 0x6A, 0x69, 0x66, 0x65, 0x5A, 0x59, 0x56, 0x55}; diff --git a/esphome/components/lcd_base/__init__.py b/esphome/components/lcd_base/__init__.py index 92fd0b5563..693211c6fe 100644 --- a/esphome/components/lcd_base/__init__.py +++ b/esphome/components/lcd_base/__init__.py @@ -52,7 +52,6 @@ LCD_SCHEMA = display.BASIC_DISPLAY_SCHEMA.extend( async def setup_lcd_display(var, config): - await cg.register_component(var, config) await display.register_display(var, config) cg.add(var.set_dimensions(config[CONF_DIMENSIONS][0], config[CONF_DIMENSIONS][1])) if CONF_USER_CHARACTERS in config: diff --git a/esphome/components/max7219/display.py b/esphome/components/max7219/display.py index 391d033f24..13807b0dbd 100644 --- a/esphome/components/max7219/display.py +++ b/esphome/components/max7219/display.py @@ -29,7 +29,6 @@ CONFIG_SCHEMA = ( async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - await cg.register_component(var, config) await spi.register_spi_device(var, config) await display.register_display(var, config) diff --git a/esphome/components/max7219digit/display.py b/esphome/components/max7219digit/display.py index 8db9123a39..779e385ab1 100644 --- a/esphome/components/max7219digit/display.py +++ b/esphome/components/max7219digit/display.py @@ -39,7 +39,7 @@ CHIP_MODES = { max7219_ns = cg.esphome_ns.namespace("max7219digit") MAX7219Component = max7219_ns.class_( - "MAX7219Component", cg.PollingComponent, spi.SPIDevice, display.DisplayBuffer + "MAX7219Component", spi.SPIDevice, display.DisplayBuffer, cg.PollingComponent ) MAX7219ComponentRef = MAX7219Component.operator("ref") @@ -78,7 +78,6 @@ CONFIG_SCHEMA = ( async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - await cg.register_component(var, config) await spi.register_spi_device(var, config) await display.register_display(var, config) diff --git a/esphome/components/max7219digit/max7219digit.h b/esphome/components/max7219digit/max7219digit.h index 93d2af21f9..ead8033803 100644 --- a/esphome/components/max7219digit/max7219digit.h +++ b/esphome/components/max7219digit/max7219digit.h @@ -25,8 +25,7 @@ class MAX7219Component; using max7219_writer_t = std::function; -class MAX7219Component : public PollingComponent, - public display::DisplayBuffer, +class MAX7219Component : public display::DisplayBuffer, public spi::SPIDevice { public: diff --git a/esphome/components/nextion/display.py b/esphome/components/nextion/display.py index 0831b12f8a..1ac0364b8b 100644 --- a/esphome/components/nextion/display.py +++ b/esphome/components/nextion/display.py @@ -71,7 +71,6 @@ CONFIG_SCHEMA = ( async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - await cg.register_component(var, config) await uart.register_uart_device(var, config) if CONF_BRIGHTNESS in config: diff --git a/esphome/components/pcd8544/display.py b/esphome/components/pcd8544/display.py index b4c8f432cf..d7e72d1c81 100644 --- a/esphome/components/pcd8544/display.py +++ b/esphome/components/pcd8544/display.py @@ -39,7 +39,6 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - await cg.register_component(var, config) await display.register_display(var, config) await spi.register_spi_device(var, config) diff --git a/esphome/components/pcd8544/pcd_8544.h b/esphome/components/pcd8544/pcd_8544.h index 9a69a9fec7..cfdb96de61 100644 --- a/esphome/components/pcd8544/pcd_8544.h +++ b/esphome/components/pcd8544/pcd_8544.h @@ -7,8 +7,7 @@ namespace esphome { namespace pcd8544 { -class PCD8544 : public PollingComponent, - public display::DisplayBuffer, +class PCD8544 : public display::DisplayBuffer, public spi::SPIDevice { public: diff --git a/esphome/components/pvvx_mithermometer/display/__init__.py b/esphome/components/pvvx_mithermometer/display/__init__.py index d935638933..70c568c1e3 100644 --- a/esphome/components/pvvx_mithermometer/display/__init__.py +++ b/esphome/components/pvvx_mithermometer/display/__init__.py @@ -38,7 +38,6 @@ CONFIG_SCHEMA = ( async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - await cg.register_component(var, config) await display.register_display(var, config) await ble_client.register_ble_node(var, config) cg.add(var.set_disconnect_delay(config[CONF_DISCONNECT_DELAY].total_milliseconds)) diff --git a/esphome/components/ssd1306_base/__init__.py b/esphome/components/ssd1306_base/__init__.py index 55239dfcb8..1fe74dfcb5 100644 --- a/esphome/components/ssd1306_base/__init__.py +++ b/esphome/components/ssd1306_base/__init__.py @@ -74,7 +74,6 @@ SSD1306_SCHEMA = display.FULL_DISPLAY_SCHEMA.extend( async def setup_ssd1306(var, config): - await cg.register_component(var, config) await display.register_display(var, config) cg.add(var.set_model(config[CONF_MODEL])) diff --git a/esphome/components/ssd1306_base/ssd1306_base.h b/esphome/components/ssd1306_base/ssd1306_base.h index 34b76d284d..2e09755863 100644 --- a/esphome/components/ssd1306_base/ssd1306_base.h +++ b/esphome/components/ssd1306_base/ssd1306_base.h @@ -24,7 +24,7 @@ enum SSD1306Model { SSD1305_MODEL_128_64, }; -class SSD1306 : public PollingComponent, public display::DisplayBuffer { +class SSD1306 : public display::DisplayBuffer { public: void setup() override; diff --git a/esphome/components/ssd1322_base/__init__.py b/esphome/components/ssd1322_base/__init__.py index 97fb0d2a74..471c874986 100644 --- a/esphome/components/ssd1322_base/__init__.py +++ b/esphome/components/ssd1322_base/__init__.py @@ -33,7 +33,6 @@ SSD1322_SCHEMA = display.FULL_DISPLAY_SCHEMA.extend( async def setup_ssd1322(var, config): - await cg.register_component(var, config) await display.register_display(var, config) cg.add(var.set_model(config[CONF_MODEL])) diff --git a/esphome/components/ssd1322_base/ssd1322_base.h b/esphome/components/ssd1322_base/ssd1322_base.h index d672b298d6..9f4d39976c 100644 --- a/esphome/components/ssd1322_base/ssd1322_base.h +++ b/esphome/components/ssd1322_base/ssd1322_base.h @@ -11,7 +11,7 @@ enum SSD1322Model { SSD1322_MODEL_256_64 = 0, }; -class SSD1322 : public PollingComponent, public display::DisplayBuffer { +class SSD1322 : public display::DisplayBuffer { public: void setup() override; diff --git a/esphome/components/ssd1325_base/__init__.py b/esphome/components/ssd1325_base/__init__.py index 1a6f7fb519..e66cfbc684 100644 --- a/esphome/components/ssd1325_base/__init__.py +++ b/esphome/components/ssd1325_base/__init__.py @@ -37,7 +37,6 @@ SSD1325_SCHEMA = display.FULL_DISPLAY_SCHEMA.extend( async def setup_ssd1325(var, config): - await cg.register_component(var, config) await display.register_display(var, config) cg.add(var.set_model(config[CONF_MODEL])) diff --git a/esphome/components/ssd1325_base/ssd1325_base.h b/esphome/components/ssd1325_base/ssd1325_base.h index 8ba6a56c8b..ae033e582b 100644 --- a/esphome/components/ssd1325_base/ssd1325_base.h +++ b/esphome/components/ssd1325_base/ssd1325_base.h @@ -15,7 +15,7 @@ enum SSD1325Model { SSD1327_MODEL_128_128, }; -class SSD1325 : public PollingComponent, public display::DisplayBuffer { +class SSD1325 : public display::DisplayBuffer { public: void setup() override; diff --git a/esphome/components/ssd1327_base/__init__.py b/esphome/components/ssd1327_base/__init__.py index af2eb3489d..7f2259cf32 100644 --- a/esphome/components/ssd1327_base/__init__.py +++ b/esphome/components/ssd1327_base/__init__.py @@ -26,7 +26,6 @@ SSD1327_SCHEMA = display.FULL_DISPLAY_SCHEMA.extend( async def setup_ssd1327(var, config): - await cg.register_component(var, config) await display.register_display(var, config) cg.add(var.set_model(config[CONF_MODEL])) diff --git a/esphome/components/ssd1327_base/ssd1327_base.h b/esphome/components/ssd1327_base/ssd1327_base.h index 5639beb828..207023a3d3 100644 --- a/esphome/components/ssd1327_base/ssd1327_base.h +++ b/esphome/components/ssd1327_base/ssd1327_base.h @@ -11,7 +11,7 @@ enum SSD1327Model { SSD1327_MODEL_128_128 = 0, }; -class SSD1327 : public PollingComponent, public display::DisplayBuffer { +class SSD1327 : public display::DisplayBuffer { public: void setup() override; diff --git a/esphome/components/ssd1331_base/__init__.py b/esphome/components/ssd1331_base/__init__.py index 169c0eed1a..80162979fc 100644 --- a/esphome/components/ssd1331_base/__init__.py +++ b/esphome/components/ssd1331_base/__init__.py @@ -18,7 +18,6 @@ SSD1331_SCHEMA = display.FULL_DISPLAY_SCHEMA.extend( async def setup_ssd1331(var, config): - await cg.register_component(var, config) await display.register_display(var, config) if CONF_RESET_PIN in config: diff --git a/esphome/components/ssd1331_base/ssd1331_base.h b/esphome/components/ssd1331_base/ssd1331_base.h index be5713f208..719bfc1f8b 100644 --- a/esphome/components/ssd1331_base/ssd1331_base.h +++ b/esphome/components/ssd1331_base/ssd1331_base.h @@ -7,7 +7,7 @@ namespace esphome { namespace ssd1331_base { -class SSD1331 : public PollingComponent, public display::DisplayBuffer { +class SSD1331 : public display::DisplayBuffer { public: void setup() override; diff --git a/esphome/components/ssd1351_base/__init__.py b/esphome/components/ssd1351_base/__init__.py index 2988dd4bf3..150d89afed 100644 --- a/esphome/components/ssd1351_base/__init__.py +++ b/esphome/components/ssd1351_base/__init__.py @@ -27,7 +27,6 @@ SSD1351_SCHEMA = display.FULL_DISPLAY_SCHEMA.extend( async def setup_ssd1351(var, config): - await cg.register_component(var, config) await display.register_display(var, config) cg.add(var.set_model(config[CONF_MODEL])) diff --git a/esphome/components/ssd1351_base/ssd1351_base.h b/esphome/components/ssd1351_base/ssd1351_base.h index 2f1e0237cd..62777a60a0 100644 --- a/esphome/components/ssd1351_base/ssd1351_base.h +++ b/esphome/components/ssd1351_base/ssd1351_base.h @@ -12,7 +12,7 @@ enum SSD1351Model { SSD1351_MODEL_128_128, }; -class SSD1351 : public PollingComponent, public display::DisplayBuffer { +class SSD1351 : public display::DisplayBuffer { public: void setup() override; diff --git a/esphome/components/st7735/display.py b/esphome/components/st7735/display.py index 652d31662d..4ff5cafaf8 100644 --- a/esphome/components/st7735/display.py +++ b/esphome/components/st7735/display.py @@ -69,7 +69,6 @@ CONFIG_SCHEMA = cv.All( async def setup_st7735(var, config): - await cg.register_component(var, config) await display.register_display(var, config) if CONF_RESET_PIN in config: diff --git a/esphome/components/st7735/st7735.h b/esphome/components/st7735/st7735.h index 3baa9b083a..37fe673962 100644 --- a/esphome/components/st7735/st7735.h +++ b/esphome/components/st7735/st7735.h @@ -32,8 +32,7 @@ enum ST7735Model { ST7735_INITR_18REDTAB = INITR_18REDTAB }; -class ST7735 : public PollingComponent, - public display::DisplayBuffer, +class ST7735 : public display::DisplayBuffer, public spi::SPIDevice { public: diff --git a/esphome/components/st7789v/display.py b/esphome/components/st7789v/display.py index a4c08974c6..41970afd26 100644 --- a/esphome/components/st7789v/display.py +++ b/esphome/components/st7789v/display.py @@ -158,7 +158,6 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - await cg.register_component(var, config) await display.register_display(var, config) await spi.register_spi_device(var, config) diff --git a/esphome/components/st7789v/st7789v.h b/esphome/components/st7789v/st7789v.h index 22093301e2..29ea315979 100644 --- a/esphome/components/st7789v/st7789v.h +++ b/esphome/components/st7789v/st7789v.h @@ -107,8 +107,7 @@ static const uint8_t ST7789_MADCTL_GS = 0x01; static const uint8_t ST7789_MADCTL_COLOR_ORDER = ST7789_MADCTL_BGR; -class ST7789V : public PollingComponent, - public display::DisplayBuffer, +class ST7789V : public display::DisplayBuffer, public spi::SPIDevice { public: diff --git a/esphome/components/st7920/display.py b/esphome/components/st7920/display.py index 9b544fa644..1267e2ad63 100644 --- a/esphome/components/st7920/display.py +++ b/esphome/components/st7920/display.py @@ -28,7 +28,6 @@ CONFIG_SCHEMA = ( async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - await cg.register_component(var, config) await spi.register_spi_device(var, config) if CONF_LAMBDA in config: diff --git a/esphome/components/st7920/st7920.h b/esphome/components/st7920/st7920.h index c00b7cf5e0..c9fdad454d 100644 --- a/esphome/components/st7920/st7920.h +++ b/esphome/components/st7920/st7920.h @@ -11,8 +11,7 @@ class ST7920; using st7920_writer_t = std::function; -class ST7920 : public PollingComponent, - public display::DisplayBuffer, +class ST7920 : public display::DisplayBuffer, public spi::SPIDevice { public: diff --git a/esphome/components/tm1621/display.py b/esphome/components/tm1621/display.py index edbc5f6928..a82b680f62 100644 --- a/esphome/components/tm1621/display.py +++ b/esphome/components/tm1621/display.py @@ -28,7 +28,6 @@ CONFIG_SCHEMA = display.BASIC_DISPLAY_SCHEMA.extend( async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - await cg.register_component(var, config) await display.register_display(var, config) cs = await cg.gpio_pin_expression(config[CONF_CS_PIN]) diff --git a/esphome/components/tm1637/display.py b/esphome/components/tm1637/display.py index 609c62fd10..dcbc64332a 100644 --- a/esphome/components/tm1637/display.py +++ b/esphome/components/tm1637/display.py @@ -34,7 +34,6 @@ CONFIG_SCHEMA = display.BASIC_DISPLAY_SCHEMA.extend( async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - await cg.register_component(var, config) await display.register_display(var, config) clk = await cg.gpio_pin_expression(config[CONF_CLK_PIN]) diff --git a/esphome/components/tm1638/display.py b/esphome/components/tm1638/display.py index 6339983674..2fb8dc7a55 100644 --- a/esphome/components/tm1638/display.py +++ b/esphome/components/tm1638/display.py @@ -33,7 +33,6 @@ CONFIG_SCHEMA = display.BASIC_DISPLAY_SCHEMA.extend( async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - await cg.register_component(var, config) await display.register_display(var, config) clk = await cg.gpio_pin_expression(config[CONF_CLK_PIN]) diff --git a/esphome/components/waveshare_epaper/display.py b/esphome/components/waveshare_epaper/display.py index eb0faadc02..519b07fca2 100644 --- a/esphome/components/waveshare_epaper/display.py +++ b/esphome/components/waveshare_epaper/display.py @@ -148,7 +148,6 @@ async def to_code(config): else: raise NotImplementedError() - await cg.register_component(var, config) await display.register_display(var, config) await spi.register_spi_device(var, config) diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.h b/esphome/components/waveshare_epaper/waveshare_epaper.h index b3325d69eb..c800d29643 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.h +++ b/esphome/components/waveshare_epaper/waveshare_epaper.h @@ -7,8 +7,7 @@ namespace esphome { namespace waveshare_epaper { -class WaveshareEPaper : public PollingComponent, - public display::DisplayBuffer, +class WaveshareEPaper : public display::DisplayBuffer, public spi::SPIDevice { public: From 2e6d01ddff06e2b5f006b01ca16df740fcf6446c Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Mon, 27 Nov 2023 07:54:12 +1100 Subject: [PATCH 02/89] Implement variable length single word SPI writes. (#5678) --- esphome/components/spi/spi.cpp | 14 +++++++----- esphome/components/spi/spi.h | 16 ++++++++++++++ esphome/components/spi/spi_esp_idf.cpp | 30 ++++++++++++++++++-------- 3 files changed, 46 insertions(+), 14 deletions(-) diff --git a/esphome/components/spi/spi.cpp b/esphome/components/spi/spi.cpp index 935399500f..9d06ac0e45 100644 --- a/esphome/components/spi/spi.cpp +++ b/esphome/components/spi/spi.cpp @@ -77,15 +77,19 @@ void SPIComponent::dump_config() { void SPIDelegateDummy::begin_transaction() { ESP_LOGE(TAG, "SPIDevice not initialised - did you call spi_setup()?"); } -uint8_t SPIDelegateBitBash::transfer(uint8_t data) { +uint8_t SPIDelegateBitBash::transfer(uint8_t data) { return this->transfer_(data, 8); } + +void SPIDelegateBitBash::write(uint16_t data, size_t num_bits) { this->transfer_(data, num_bits); } + +uint16_t SPIDelegateBitBash::transfer_(uint16_t data, size_t num_bits) { // Clock starts out at idle level this->clk_pin_->digital_write(clock_polarity_); uint8_t out_data = 0; - for (uint8_t i = 0; i < 8; i++) { + for (uint8_t i = 0; i != num_bits; i++) { uint8_t shift; if (bit_order_ == BIT_ORDER_MSB_FIRST) { - shift = 7 - i; + shift = num_bits - 1 - i; } else { shift = i; } @@ -94,7 +98,7 @@ uint8_t SPIDelegateBitBash::transfer(uint8_t data) { // sampling on leading edge this->sdo_pin_->digital_write(data & (1 << shift)); this->cycle_clock_(); - out_data |= uint8_t(this->sdi_pin_->digital_read()) << shift; + out_data |= uint16_t(this->sdi_pin_->digital_read()) << shift; this->clk_pin_->digital_write(!this->clock_polarity_); this->cycle_clock_(); this->clk_pin_->digital_write(this->clock_polarity_); @@ -104,7 +108,7 @@ uint8_t SPIDelegateBitBash::transfer(uint8_t data) { this->clk_pin_->digital_write(!this->clock_polarity_); this->sdo_pin_->digital_write(data & (1 << shift)); this->cycle_clock_(); - out_data |= uint8_t(this->sdi_pin_->digital_read()) << shift; + out_data |= uint16_t(this->sdi_pin_->digital_read()) << shift; this->clk_pin_->digital_write(this->clock_polarity_); } } diff --git a/esphome/components/spi/spi.h b/esphome/components/spi/spi.h index 107ffb7cb5..0eb4cd7eb6 100644 --- a/esphome/components/spi/spi.h +++ b/esphome/components/spi/spi.h @@ -199,6 +199,15 @@ class SPIDelegate { rxbuf[i] = this->transfer(txbuf[i]); } + /** + * write a variable length data item, up to 16 bits. + * @param data The data to send. Should be LSB-aligned (i.e. top bits will be discarded.) + * @param num_bits The number of bits to send + */ + virtual void write(uint16_t data, size_t num_bits) { + esph_log_e("spi_device", "variable length write not implemented"); + } + // write 16 bits virtual void write16(uint16_t data) { if (this->bit_order_ == BIT_ORDER_MSB_FIRST) { @@ -270,6 +279,10 @@ class SPIDelegateBitBash : public SPIDelegate { uint8_t transfer(uint8_t data) override; + void write(uint16_t data, size_t num_bits) override; + + void write16(uint16_t data) override { this->write(data, 16); }; + protected: GPIOPin *clk_pin_; GPIOPin *sdo_pin_; @@ -284,6 +297,7 @@ class SPIDelegateBitBash : public SPIDelegate { continue; this->last_transition_ += this->wait_cycle_; } + uint16_t transfer_(uint16_t data, size_t num_bits); }; class SPIBus { @@ -408,6 +422,8 @@ class SPIDevice : public SPIClient { void read_array(uint8_t *data, size_t length) { return this->delegate_->read_array(data, length); } + void write(uint16_t data, size_t num_bits) { this->delegate_->write(data, num_bits); }; + void write_byte(uint8_t data) { this->delegate_->write_array(&data, 1); } void transfer_array(uint8_t *data, size_t length) { this->delegate_->transfer(data, length); } diff --git a/esphome/components/spi/spi_esp_idf.cpp b/esphome/components/spi/spi_esp_idf.cpp index f9e4bfcca6..03ab298019 100644 --- a/esphome/components/spi/spi_esp_idf.cpp +++ b/esphome/components/spi/spi_esp_idf.cpp @@ -72,7 +72,11 @@ class SPIDelegateHw : public SPIDelegate { desc.rxlength = this->write_only_ ? 0 : partial * 8; desc.tx_buffer = txbuf; desc.rx_buffer = rxbuf; - esp_err_t const err = spi_device_transmit(this->handle_, &desc); + // polling is used as it has about 10% less overhead than queuing an interrupt transfer + esp_err_t err = spi_device_polling_start(this->handle_, &desc, portMAX_DELAY); + if (err == ESP_OK) { + err = spi_device_polling_end(this->handle_, portMAX_DELAY); + } if (err != ESP_OK) { ESP_LOGE(TAG, "Transmit failed - err %X", err); break; @@ -85,6 +89,21 @@ class SPIDelegateHw : public SPIDelegate { } } + void write(uint16_t data, size_t num_bits) override { + spi_transaction_ext_t desc = {}; + desc.command_bits = num_bits; + desc.base.flags = SPI_TRANS_VARIABLE_CMD; + desc.base.cmd = data; + esp_err_t err = spi_device_polling_start(this->handle_, (spi_transaction_t *) &desc, portMAX_DELAY); + if (err == ESP_OK) { + err = spi_device_polling_end(this->handle_, portMAX_DELAY); + } + + if (err != ESP_OK) { + ESP_LOGE(TAG, "Transmit failed - err %X", err); + } + } + void transfer(uint8_t *ptr, size_t length) override { this->transfer(ptr, ptr, length); } uint8_t transfer(uint8_t data) override { @@ -93,14 +112,7 @@ class SPIDelegateHw : public SPIDelegate { return rxbuf; } - void write16(uint16_t data) override { - if (this->bit_order_ == BIT_ORDER_MSB_FIRST) { - uint16_t txbuf = SPI_SWAP_DATA_TX(data, 16); - this->transfer((uint8_t *) &txbuf, nullptr, 2); - } else { - this->transfer((uint8_t *) &data, nullptr, 2); - } - } + void write16(uint16_t data) override { this->write(data, 16); } void write_array(const uint8_t *ptr, size_t length) override { this->transfer(ptr, nullptr, length); } From 0a7d3c367b9e494b24baf66d5373370718ec9fca Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Mon, 27 Nov 2023 08:36:52 +1100 Subject: [PATCH 03/89] Add 'enable_at_startup' feature to power_supply (#5826) --- esphome/components/power_supply/__init__.py | 4 +++ .../components/power_supply/power_supply.cpp | 29 ++++++++----------- .../components/power_supply/power_supply.h | 3 +- tests/test1.1.yaml | 13 +++++---- 4 files changed, 25 insertions(+), 24 deletions(-) diff --git a/esphome/components/power_supply/__init__.py b/esphome/components/power_supply/__init__.py index f7dd8bca84..6735eddff3 100644 --- a/esphome/components/power_supply/__init__.py +++ b/esphome/components/power_supply/__init__.py @@ -8,6 +8,8 @@ power_supply_ns = cg.esphome_ns.namespace("power_supply") PowerSupply = power_supply_ns.class_("PowerSupply", cg.Component) MULTI_CONF = True +CONF_ENABLE_ON_BOOT = "enable_on_boot" + CONFIG_SCHEMA = cv.Schema( { cv.Required(CONF_ID): cv.declare_id(PowerSupply), @@ -18,6 +20,7 @@ CONFIG_SCHEMA = cv.Schema( cv.Optional( CONF_KEEP_ON_TIME, default="10s" ): cv.positive_time_period_milliseconds, + cv.Optional(CONF_ENABLE_ON_BOOT, default=False): cv.boolean, } ).extend(cv.COMPONENT_SCHEMA) @@ -30,5 +33,6 @@ async def to_code(config): cg.add(var.set_pin(pin)) cg.add(var.set_enable_time(config[CONF_ENABLE_TIME])) cg.add(var.set_keep_on_time(config[CONF_KEEP_ON_TIME])) + cg.add(var.set_enable_on_boot(config[CONF_ENABLE_ON_BOOT])) cg.add_define("USE_POWER_SUPPLY") diff --git a/esphome/components/power_supply/power_supply.cpp b/esphome/components/power_supply/power_supply.cpp index c4d157615a..7474075302 100644 --- a/esphome/components/power_supply/power_supply.cpp +++ b/esphome/components/power_supply/power_supply.cpp @@ -11,47 +11,42 @@ void PowerSupply::setup() { this->pin_->setup(); this->pin_->digital_write(false); - this->enabled_ = false; + if (this->enable_on_boot_) + this->request_high_power(); } void PowerSupply::dump_config() { ESP_LOGCONFIG(TAG, "Power Supply:"); LOG_PIN(" Pin: ", this->pin_); ESP_LOGCONFIG(TAG, " Time to enable: %" PRIu32 " ms", this->enable_time_); ESP_LOGCONFIG(TAG, " Keep on time: %.1f s", this->keep_on_time_ / 1000.0f); + if (this->enable_on_boot_) + ESP_LOGCONFIG(TAG, " Enabled at startup: True"); } float PowerSupply::get_setup_priority() const { return setup_priority::IO; } -bool PowerSupply::is_enabled() const { return this->enabled_; } +bool PowerSupply::is_enabled() const { return this->active_requests_ != 0; } void PowerSupply::request_high_power() { - this->cancel_timeout("power-supply-off"); - this->pin_->digital_write(true); - if (this->active_requests_ == 0) { - // we need to enable the power supply. - // cancel old timeout if it exists because we now definitely have a high power mode. + this->cancel_timeout("power-supply-off"); ESP_LOGD(TAG, "Enabling power supply."); + this->pin_->digital_write(true); delay(this->enable_time_); } - this->enabled_ = true; - // increase active requests this->active_requests_++; } void PowerSupply::unrequest_high_power() { - this->active_requests_--; - if (this->active_requests_ < 0) { - // we're just going to use 0 as our new counter. - this->active_requests_ = 0; - } - if (this->active_requests_ == 0) { - // set timeout for power supply off + ESP_LOGW(TAG, "Invalid call to unrequest_high_power"); + return; + } + this->active_requests_--; + if (this->active_requests_ == 0) { this->set_timeout("power-supply-off", this->keep_on_time_, [this]() { ESP_LOGD(TAG, "Disabling power supply."); this->pin_->digital_write(false); - this->enabled_ = false; }); } } diff --git a/esphome/components/power_supply/power_supply.h b/esphome/components/power_supply/power_supply.h index 49d905ba3a..0b06105ae9 100644 --- a/esphome/components/power_supply/power_supply.h +++ b/esphome/components/power_supply/power_supply.h @@ -13,6 +13,7 @@ class PowerSupply : public Component { void set_pin(GPIOPin *pin) { pin_ = pin; } void set_enable_time(uint32_t enable_time) { enable_time_ = enable_time; } void set_keep_on_time(uint32_t keep_on_time) { keep_on_time_ = keep_on_time; } + void set_enable_on_boot(bool enable_on_boot) { enable_on_boot_ = enable_on_boot; } /// Is this power supply currently on? bool is_enabled() const; @@ -35,7 +36,7 @@ class PowerSupply : public Component { protected: GPIOPin *pin_; - bool enabled_{false}; + bool enable_on_boot_{false}; uint32_t enable_time_; uint32_t keep_on_time_; int16_t active_requests_{0}; // use signed integer to make catching negative requests easier. diff --git a/tests/test1.1.yaml b/tests/test1.1.yaml index f4ad89897b..e2e7bd5d63 100644 --- a/tests/test1.1.yaml +++ b/tests/test1.1.yaml @@ -44,12 +44,13 @@ network: e131: power_supply: - id: atx_power_supply - enable_time: 20ms - keep_on_time: 10s - pin: - number: 13 - inverted: true + - id: atx_power_supply + enable_time: 20ms + keep_on_time: 10s + enable_on_boot: true + pin: + number: 13 + inverted: true i2c: sda: 21 From 1324d9e39a4f7d850ba9fc013f3d0e412e4fa5b9 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 27 Nov 2023 13:45:26 +1300 Subject: [PATCH 04/89] Voice Assistant improvements (#5827) --- .../i2s_audio/speaker/i2s_audio_speaker.cpp | 2 + .../i2s_audio/speaker/i2s_audio_speaker.h | 2 + esphome/components/speaker/speaker.h | 2 + .../voice_assistant/voice_assistant.cpp | 108 +++++++++++------- .../voice_assistant/voice_assistant.h | 3 + 5 files changed, 78 insertions(+), 39 deletions(-) diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp index ed13e6b458..e729cdf954 100644 --- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp +++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp @@ -220,6 +220,8 @@ size_t I2SAudioSpeaker::play(const uint8_t *data, size_t length) { return index; } +bool I2SAudioSpeaker::has_buffered_data() const { return uxQueueMessagesWaiting(this->buffer_queue_) > 0; } + } // namespace i2s_audio } // namespace esphome diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h index b075722e1b..20c36a69d3 100644 --- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h +++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h @@ -56,6 +56,8 @@ class I2SAudioSpeaker : public Component, public speaker::Speaker, public I2SAud size_t play(const uint8_t *data, size_t length) override; + bool has_buffered_data() const override; + protected: void start_(); // void stop_(); diff --git a/esphome/components/speaker/speaker.h b/esphome/components/speaker/speaker.h index 3f520e3c5e..b494873160 100644 --- a/esphome/components/speaker/speaker.h +++ b/esphome/components/speaker/speaker.h @@ -18,6 +18,8 @@ class Speaker { virtual void start() = 0; virtual void stop() = 0; + virtual bool has_buffered_data() const = 0; + bool is_running() const { return this->state_ == STATE_RUNNING; } protected: diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp index 9b13a71039..c0e706305d 100644 --- a/esphome/components/voice_assistant/voice_assistant.cpp +++ b/esphome/components/voice_assistant/voice_assistant.cpp @@ -273,28 +273,27 @@ void VoiceAssistant::loop() { bool playing = false; #ifdef USE_SPEAKER if (this->speaker_ != nullptr) { + ssize_t received_len = 0; if (this->speaker_buffer_index_ + RECEIVE_SIZE < SPEAKER_BUFFER_SIZE) { - auto len = this->socket_->read(this->speaker_buffer_ + this->speaker_buffer_index_, RECEIVE_SIZE); - if (len > 0) { - this->speaker_buffer_index_ += len; - this->speaker_buffer_size_ += len; + received_len = this->socket_->read(this->speaker_buffer_ + this->speaker_buffer_index_, RECEIVE_SIZE); + if (received_len > 0) { + this->speaker_buffer_index_ += received_len; + this->speaker_buffer_size_ += received_len; + this->speaker_bytes_received_ += received_len; } } else { - ESP_LOGW(TAG, "Receive buffer full"); - } - if (this->speaker_buffer_size_ > 0) { - size_t written = this->speaker_->play(this->speaker_buffer_, this->speaker_buffer_size_); - if (written > 0) { - memmove(this->speaker_buffer_, this->speaker_buffer_ + written, this->speaker_buffer_size_ - written); - this->speaker_buffer_size_ -= written; - this->speaker_buffer_index_ -= written; - this->set_timeout("speaker-timeout", 2000, [this]() { this->speaker_->stop(); }); - } else { - ESP_LOGW(TAG, "Speaker buffer full"); - } + ESP_LOGD(TAG, "Receive buffer full"); } + // Build a small buffer of audio before sending to the speaker + if (this->speaker_bytes_received_ > RECEIVE_SIZE * 4) + this->write_speaker_(); if (this->wait_for_stream_end_) { this->cancel_timeout("playing"); + if (this->stream_ended_ && received_len < 0) { + ESP_LOGD(TAG, "End of audio stream received"); + this->cancel_timeout("speaker-timeout"); + this->set_state_(State::RESPONSE_FINISHED, State::RESPONSE_FINISHED); + } break; // We dont want to timeout here as the STREAM_END event will take care of that. } playing = this->speaker_->is_running(); @@ -316,14 +315,26 @@ void VoiceAssistant::loop() { case State::RESPONSE_FINISHED: { #ifdef USE_SPEAKER if (this->speaker_ != nullptr) { + if (this->speaker_buffer_size_ > 0) { + this->write_speaker_(); + break; + } + if (this->speaker_->has_buffered_data() || this->speaker_->is_running()) { + break; + } + ESP_LOGD(TAG, "Speaker has finished outputting all audio"); this->speaker_->stop(); this->cancel_timeout("speaker-timeout"); this->cancel_timeout("playing"); this->speaker_buffer_size_ = 0; this->speaker_buffer_index_ = 0; + this->speaker_bytes_received_ = 0; memset(this->speaker_buffer_, 0, SPEAKER_BUFFER_SIZE); + this->wait_for_stream_end_ = false; + this->stream_ended_ = false; + + this->tts_stream_end_trigger_->trigger(); } - this->wait_for_stream_end_ = false; #endif this->set_state_(State::IDLE, State::IDLE); break; @@ -333,6 +344,20 @@ void VoiceAssistant::loop() { } } +void VoiceAssistant::write_speaker_() { + if (this->speaker_buffer_size_ > 0) { + size_t written = this->speaker_->play(this->speaker_buffer_, this->speaker_buffer_size_); + if (written > 0) { + memmove(this->speaker_buffer_, this->speaker_buffer_ + written, this->speaker_buffer_size_ - written); + this->speaker_buffer_size_ -= written; + this->speaker_buffer_index_ -= written; + this->set_timeout("speaker-timeout", 5000, [this]() { this->speaker_->stop(); }); + } else { + ESP_LOGD(TAG, "Speaker buffer full, trying again next loop"); + } + } +} + void VoiceAssistant::client_subscription(api::APIConnection *client, bool subscribe) { if (!subscribe) { if (this->api_client_ == nullptr || client != this->api_client_) { @@ -503,21 +528,20 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { switch (msg.event_type) { case api::enums::VOICE_ASSISTANT_RUN_START: ESP_LOGD(TAG, "Assist Pipeline running"); - this->start_trigger_->trigger(); + this->defer([this]() { this->start_trigger_->trigger(); }); break; case api::enums::VOICE_ASSISTANT_WAKE_WORD_START: break; case api::enums::VOICE_ASSISTANT_WAKE_WORD_END: { ESP_LOGD(TAG, "Wake word detected"); - this->wake_word_detected_trigger_->trigger(); + this->defer([this]() { this->wake_word_detected_trigger_->trigger(); }); break; } case api::enums::VOICE_ASSISTANT_STT_START: ESP_LOGD(TAG, "STT started"); - this->listening_trigger_->trigger(); + this->defer([this]() { this->listening_trigger_->trigger(); }); break; case api::enums::VOICE_ASSISTANT_STT_END: { - this->set_state_(State::STOP_MICROPHONE, State::AWAITING_RESPONSE); std::string text; for (auto arg : msg.data) { if (arg.name == "text") { @@ -529,12 +553,12 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { return; } ESP_LOGD(TAG, "Speech recognised as: \"%s\"", text.c_str()); - this->stt_end_trigger_->trigger(text); + this->defer([this, text]() { this->stt_end_trigger_->trigger(text); }); break; } case api::enums::VOICE_ASSISTANT_INTENT_START: ESP_LOGD(TAG, "Intent started"); - this->intent_start_trigger_->trigger(); + this->defer([this]() { this->intent_start_trigger_->trigger(); }); break; case api::enums::VOICE_ASSISTANT_INTENT_END: { for (auto arg : msg.data) { @@ -542,7 +566,7 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { this->conversation_id_ = std::move(arg.value); } } - this->intent_end_trigger_->trigger(); + this->defer([this]() { this->intent_end_trigger_->trigger(); }); break; } case api::enums::VOICE_ASSISTANT_TTS_START: { @@ -557,10 +581,12 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { return; } ESP_LOGD(TAG, "Response: \"%s\"", text.c_str()); - this->tts_start_trigger_->trigger(text); + this->defer([this, text]() { + this->tts_start_trigger_->trigger(text); #ifdef USE_SPEAKER - this->speaker_->start(); + this->speaker_->start(); #endif + }); break; } case api::enums::VOICE_ASSISTANT_TTS_END: { @@ -575,14 +601,16 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { return; } ESP_LOGD(TAG, "Response URL: \"%s\"", url.c_str()); + this->defer([this, url]() { #ifdef USE_MEDIA_PLAYER - if (this->media_player_ != nullptr) { - this->media_player_->make_call().set_media_url(url).perform(); - } + if (this->media_player_ != nullptr) { + this->media_player_->make_call().set_media_url(url).perform(); + } #endif + this->tts_end_trigger_->trigger(url); + }); State new_state = this->local_output_ ? State::STREAMING_RESPONSE : State::IDLE; this->set_state_(new_state, new_state); - this->tts_end_trigger_->trigger(url); break; } case api::enums::VOICE_ASSISTANT_RUN_END: { @@ -599,7 +627,7 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { this->set_state_(State::IDLE, State::IDLE); } } - this->end_trigger_->trigger(); + this->defer([this]() { this->end_trigger_->trigger(); }); break; } case api::enums::VOICE_ASSISTANT_ERROR: { @@ -617,8 +645,10 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { return; } else if (code == "wake-provider-missing" || code == "wake-engine-missing") { // Wake word is not set up or not ready on Home Assistant so stop and do not retry until user starts again. - this->request_stop(); - this->error_trigger_->trigger(code, message); + this->defer([this, code, message]() { + this->request_stop(); + this->error_trigger_->trigger(code, message); + }); return; } ESP_LOGE(TAG, "Error: %s - %s", code.c_str(), message.c_str()); @@ -626,32 +656,32 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { this->signal_stop_(); this->set_state_(State::STOP_MICROPHONE, State::IDLE); } - this->error_trigger_->trigger(code, message); + this->defer([this, code, message]() { this->error_trigger_->trigger(code, message); }); break; } case api::enums::VOICE_ASSISTANT_TTS_STREAM_START: { #ifdef USE_SPEAKER this->wait_for_stream_end_ = true; ESP_LOGD(TAG, "TTS stream start"); - this->tts_stream_start_trigger_->trigger(); + this->defer([this] { this->tts_stream_start_trigger_->trigger(); }); #endif break; } case api::enums::VOICE_ASSISTANT_TTS_STREAM_END: { - this->set_state_(State::RESPONSE_FINISHED, State::IDLE); #ifdef USE_SPEAKER + this->stream_ended_ = true; ESP_LOGD(TAG, "TTS stream end"); - this->tts_stream_end_trigger_->trigger(); #endif break; } case api::enums::VOICE_ASSISTANT_STT_VAD_START: ESP_LOGD(TAG, "Starting STT by VAD"); - this->stt_vad_start_trigger_->trigger(); + this->defer([this]() { this->stt_vad_start_trigger_->trigger(); }); break; case api::enums::VOICE_ASSISTANT_STT_VAD_END: ESP_LOGD(TAG, "STT by VAD end"); - this->stt_vad_end_trigger_->trigger(); + this->set_state_(State::STOP_MICROPHONE, State::AWAITING_RESPONSE); + this->defer([this]() { this->stt_vad_end_trigger_->trigger(); }); break; default: ESP_LOGD(TAG, "Unhandled event type: %d", msg.event_type); diff --git a/esphome/components/voice_assistant/voice_assistant.h b/esphome/components/voice_assistant/voice_assistant.h index f6dcd1c563..66bf4c3c57 100644 --- a/esphome/components/voice_assistant/voice_assistant.h +++ b/esphome/components/voice_assistant/voice_assistant.h @@ -156,11 +156,14 @@ class VoiceAssistant : public Component { microphone::Microphone *mic_{nullptr}; #ifdef USE_SPEAKER + void write_speaker_(); speaker::Speaker *speaker_{nullptr}; uint8_t *speaker_buffer_; size_t speaker_buffer_index_{0}; size_t speaker_buffer_size_{0}; + size_t speaker_bytes_received_{0}; bool wait_for_stream_end_{false}; + bool stream_ended_{false}; #endif #ifdef USE_MEDIA_PLAYER media_player::MediaPlayer *media_player_{nullptr}; From 15180ee1e2f9f7254fa277c37379372dc2b43f0a Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 28 Nov 2023 09:35:31 +1300 Subject: [PATCH 05/89] mcp3008: Tidy up and fix auto load bug (#5842) --- esphome/components/mcp3008/mcp3008.cpp | 22 ++------ esphome/components/mcp3008/mcp3008.h | 25 +-------- esphome/components/mcp3008/sensor.py | 39 -------------- esphome/components/mcp3008/sensor/__init__.py | 53 +++++++++++++++++++ .../mcp3008/sensor/mcp3008_sensor.cpp | 27 ++++++++++ .../mcp3008/sensor/mcp3008_sensor.h | 31 +++++++++++ 6 files changed, 115 insertions(+), 82 deletions(-) delete mode 100644 esphome/components/mcp3008/sensor.py create mode 100644 esphome/components/mcp3008/sensor/__init__.py create mode 100644 esphome/components/mcp3008/sensor/mcp3008_sensor.cpp create mode 100644 esphome/components/mcp3008/sensor/mcp3008_sensor.h diff --git a/esphome/components/mcp3008/mcp3008.cpp b/esphome/components/mcp3008/mcp3008.cpp index 81abc4f012..aed48456b2 100644 --- a/esphome/components/mcp3008/mcp3008.cpp +++ b/esphome/components/mcp3008/mcp3008.cpp @@ -1,4 +1,6 @@ #include "mcp3008.h" + +#include "esphome/core/helpers.h" #include "esphome/core/log.h" namespace esphome { @@ -32,28 +34,10 @@ float MCP3008::read_data(uint8_t pin) { this->disable(); - int data = data_msb << 8 | data_lsb; + uint16_t data = encode_uint16(data_msb, data_lsb); return data / 1023.0f; } -MCP3008Sensor::MCP3008Sensor(MCP3008 *parent, uint8_t pin, float reference_voltage) - : PollingComponent(1000), parent_(parent), pin_(pin), reference_voltage_(reference_voltage) {} - -float MCP3008Sensor::get_setup_priority() const { return setup_priority::DATA; } - -void MCP3008Sensor::setup() { LOG_SENSOR("", "Setting up MCP3008 Sensor '%s'...", this); } -void MCP3008Sensor::dump_config() { - ESP_LOGCONFIG(TAG, "MCP3008Sensor:"); - ESP_LOGCONFIG(TAG, " Pin: %u", this->pin_); - ESP_LOGCONFIG(TAG, " Reference Voltage: %.2fV", this->reference_voltage_); -} -float MCP3008Sensor::sample() { - float value_v = this->parent_->read_data(pin_); - value_v = (value_v * this->reference_voltage_); - return value_v; -} -void MCP3008Sensor::update() { this->publish_state(this->sample()); } - } // namespace mcp3008 } // namespace esphome diff --git a/esphome/components/mcp3008/mcp3008.h b/esphome/components/mcp3008/mcp3008.h index 5d8b823111..baf8d7c152 100644 --- a/esphome/components/mcp3008/mcp3008.h +++ b/esphome/components/mcp3008/mcp3008.h @@ -1,10 +1,8 @@ #pragma once +#include "esphome/components/spi/spi.h" #include "esphome/core/component.h" #include "esphome/core/hal.h" -#include "esphome/components/sensor/sensor.h" -#include "esphome/components/spi/spi.h" -#include "esphome/components/voltage_sampler/voltage_sampler.h" namespace esphome { namespace mcp3008 { @@ -14,31 +12,10 @@ class MCP3008 : public Component, spi::DATA_RATE_75KHZ> { // Running at the slowest max speed supported by the // mcp3008. 2.7v = 75ksps public: - MCP3008() = default; - void setup() override; void dump_config() override; float get_setup_priority() const override; float read_data(uint8_t pin); - - protected: -}; - -class MCP3008Sensor : public PollingComponent, public sensor::Sensor, public voltage_sampler::VoltageSampler { - public: - MCP3008Sensor(MCP3008 *parent, uint8_t pin, float reference_voltage); - - void set_reference_voltage(float reference_voltage) { reference_voltage_ = reference_voltage; } - void setup() override; - void update() override; - void dump_config() override; - float get_setup_priority() const override; - float sample() override; - - protected: - MCP3008 *parent_; - uint8_t pin_; - float reference_voltage_; }; } // namespace mcp3008 diff --git a/esphome/components/mcp3008/sensor.py b/esphome/components/mcp3008/sensor.py deleted file mode 100644 index dd5141484b..0000000000 --- a/esphome/components/mcp3008/sensor.py +++ /dev/null @@ -1,39 +0,0 @@ -import esphome.codegen as cg -import esphome.config_validation as cv -from esphome.components import sensor, voltage_sampler -from esphome.const import CONF_ID, CONF_NUMBER -from . import mcp3008_ns, MCP3008 - -AUTO_LOAD = ["voltage_sampler"] - -DEPENDENCIES = ["mcp3008"] - -MCP3008Sensor = mcp3008_ns.class_( - "MCP3008Sensor", sensor.Sensor, cg.PollingComponent, voltage_sampler.VoltageSampler -) -CONF_REFERENCE_VOLTAGE = "reference_voltage" -CONF_MCP3008_ID = "mcp3008_id" - -CONFIG_SCHEMA = ( - sensor.sensor_schema(MCP3008Sensor) - .extend( - { - cv.GenerateID(CONF_MCP3008_ID): cv.use_id(MCP3008), - cv.Required(CONF_NUMBER): cv.int_, - cv.Optional(CONF_REFERENCE_VOLTAGE, default="3.3V"): cv.voltage, - } - ) - .extend(cv.polling_component_schema("1s")) -) - - -async def to_code(config): - parent = await cg.get_variable(config[CONF_MCP3008_ID]) - var = cg.new_Pvariable( - config[CONF_ID], - parent, - config[CONF_NUMBER], - config[CONF_REFERENCE_VOLTAGE], - ) - await cg.register_component(var, config) - await sensor.register_sensor(var, config) diff --git a/esphome/components/mcp3008/sensor/__init__.py b/esphome/components/mcp3008/sensor/__init__.py new file mode 100644 index 0000000000..c56965d517 --- /dev/null +++ b/esphome/components/mcp3008/sensor/__init__.py @@ -0,0 +1,53 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, voltage_sampler +from esphome.const import ( + CONF_ID, + CONF_NUMBER, + UNIT_VOLT, + STATE_CLASS_MEASUREMENT, + DEVICE_CLASS_VOLTAGE, +) + +from .. import mcp3008_ns, MCP3008 + +AUTO_LOAD = ["voltage_sampler"] + +DEPENDENCIES = ["mcp3008"] + +MCP3008Sensor = mcp3008_ns.class_( + "MCP3008Sensor", + sensor.Sensor, + cg.PollingComponent, + voltage_sampler.VoltageSampler, + cg.Parented.template(MCP3008), +) +CONF_REFERENCE_VOLTAGE = "reference_voltage" +CONF_MCP3008_ID = "mcp3008_id" + +CONFIG_SCHEMA = ( + sensor.sensor_schema( + MCP3008Sensor, + unit_of_measurement=UNIT_VOLT, + state_class=STATE_CLASS_MEASUREMENT, + device_class=DEVICE_CLASS_VOLTAGE, + ) + .extend( + { + cv.GenerateID(CONF_MCP3008_ID): cv.use_id(MCP3008), + cv.Required(CONF_NUMBER): cv.int_, + cv.Optional(CONF_REFERENCE_VOLTAGE, default="3.3V"): cv.voltage, + } + ) + .extend(cv.polling_component_schema("60s")) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_parented(var, config[CONF_MCP3008_ID]) + await cg.register_component(var, config) + await sensor.register_sensor(var, config) + + cg.add(var.set_pin(config[CONF_NUMBER])) + cg.add(var.set_reference_voltage(config[CONF_REFERENCE_VOLTAGE])) diff --git a/esphome/components/mcp3008/sensor/mcp3008_sensor.cpp b/esphome/components/mcp3008/sensor/mcp3008_sensor.cpp new file mode 100644 index 0000000000..df2a8735f8 --- /dev/null +++ b/esphome/components/mcp3008/sensor/mcp3008_sensor.cpp @@ -0,0 +1,27 @@ +#include "mcp3008_sensor.h" + +#include "esphome/core/log.h" + +namespace esphome { +namespace mcp3008 { + +static const char *const TAG = "mcp3008.sensor"; + +float MCP3008Sensor::get_setup_priority() const { return setup_priority::DATA; } + +void MCP3008Sensor::dump_config() { + ESP_LOGCONFIG(TAG, "MCP3008Sensor:"); + ESP_LOGCONFIG(TAG, " Pin: %u", this->pin_); + ESP_LOGCONFIG(TAG, " Reference Voltage: %.2fV", this->reference_voltage_); +} + +float MCP3008Sensor::sample() { + float value_v = this->parent_->read_data(pin_); + value_v = (value_v * this->reference_voltage_); + return value_v; +} + +void MCP3008Sensor::update() { this->publish_state(this->sample()); } + +} // namespace mcp3008 +} // namespace esphome diff --git a/esphome/components/mcp3008/sensor/mcp3008_sensor.h b/esphome/components/mcp3008/sensor/mcp3008_sensor.h new file mode 100644 index 0000000000..ebaeab966f --- /dev/null +++ b/esphome/components/mcp3008/sensor/mcp3008_sensor.h @@ -0,0 +1,31 @@ +#pragma once + +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/voltage_sampler/voltage_sampler.h" +#include "esphome/core/component.h" + +#include "../mcp3008.h" + +namespace esphome { +namespace mcp3008 { + +class MCP3008Sensor : public PollingComponent, + public sensor::Sensor, + public voltage_sampler::VoltageSampler, + public Parented { + public: + void set_reference_voltage(float reference_voltage) { this->reference_voltage_ = reference_voltage; } + void set_pin(uint8_t pin) { this->pin_ = pin; } + + void update() override; + void dump_config() override; + float get_setup_priority() const override; + float sample() override; + + protected: + uint8_t pin_; + float reference_voltage_; +}; + +} // namespace mcp3008 +} // namespace esphome From a15a812466eb1111792e4e5ecf2eb4cfdcf05e3b Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 28 Nov 2023 09:43:41 +1300 Subject: [PATCH 06/89] Fix missing include in remote_base (#5843) --- esphome/components/remote_base/remote_base.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/remote_base/remote_base.cpp b/esphome/components/remote_base/remote_base.cpp index 40c699e8ea..095f95053f 100644 --- a/esphome/components/remote_base/remote_base.cpp +++ b/esphome/components/remote_base/remote_base.cpp @@ -1,6 +1,8 @@ #include "remote_base.h" #include "esphome/core/log.h" +#include + namespace esphome { namespace remote_base { From f63f722afb7fb016dbff1faa32386ccd64ec45fa Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 28 Nov 2023 09:44:09 +1300 Subject: [PATCH 07/89] Create GT911 Touchscreen component (#4027) Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/gt911/__init__.py | 6 + .../gt911/binary_sensor/__init__.py | 31 +++++ .../gt911/binary_sensor/gt911_button.cpp | 27 ++++ .../gt911/binary_sensor/gt911_button.h | 28 ++++ .../components/gt911/touchscreen/__init__.py | 46 +++++++ .../gt911/touchscreen/gt911_touchscreen.cpp | 122 ++++++++++++++++++ .../gt911/touchscreen/gt911_touchscreen.h | 39 ++++++ .../components/touchscreen/touchscreen.cpp | 5 + esphome/components/touchscreen/touchscreen.h | 1 + tests/test4.yaml | 8 ++ 11 files changed, 314 insertions(+) create mode 100644 esphome/components/gt911/__init__.py create mode 100644 esphome/components/gt911/binary_sensor/__init__.py create mode 100644 esphome/components/gt911/binary_sensor/gt911_button.cpp create mode 100644 esphome/components/gt911/binary_sensor/gt911_button.h create mode 100644 esphome/components/gt911/touchscreen/__init__.py create mode 100644 esphome/components/gt911/touchscreen/gt911_touchscreen.cpp create mode 100644 esphome/components/gt911/touchscreen/gt911_touchscreen.h diff --git a/CODEOWNERS b/CODEOWNERS index af23f679c8..2cd08a780e 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -119,6 +119,7 @@ esphome/components/graph/* @synco esphome/components/gree/* @orestismers esphome/components/grove_tb6612fng/* @max246 esphome/components/growatt_solar/* @leeuwte +esphome/components/gt911/* @clydebarrow @jesserockz esphome/components/haier/* @paveldn esphome/components/havells_solar/* @sourabhjaiswal esphome/components/hbridge/fan/* @WeekendWarrior diff --git a/esphome/components/gt911/__init__.py b/esphome/components/gt911/__init__.py new file mode 100644 index 0000000000..1f7ecd1d5e --- /dev/null +++ b/esphome/components/gt911/__init__.py @@ -0,0 +1,6 @@ +import esphome.codegen as cg + +CODEOWNERS = ["@jesserockz", "@clydebarrow"] +DEPENDENCIES = ["i2c"] + +gt911_ns = cg.esphome_ns.namespace("gt911") diff --git a/esphome/components/gt911/binary_sensor/__init__.py b/esphome/components/gt911/binary_sensor/__init__.py new file mode 100644 index 0000000000..18f5c49dbd --- /dev/null +++ b/esphome/components/gt911/binary_sensor/__init__.py @@ -0,0 +1,31 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import binary_sensor +from esphome.const import CONF_INDEX + +from .. import gt911_ns +from ..touchscreen import GT911Touchscreen, GT911ButtonListener + +CONF_GT911_ID = "gt911_id" + +GT911Button = gt911_ns.class_( + "GT911Button", + binary_sensor.BinarySensor, + cg.Component, + GT911ButtonListener, + cg.Parented.template(GT911Touchscreen), +) + +CONFIG_SCHEMA = binary_sensor.binary_sensor_schema(GT911Button).extend( + { + cv.GenerateID(CONF_GT911_ID): cv.use_id(GT911Touchscreen), + cv.Optional(CONF_INDEX, default=0): cv.int_range(min=0, max=3), + } +) + + +async def to_code(config): + var = await binary_sensor.new_binary_sensor(config) + await cg.register_component(var, config) + await cg.register_parented(var, config[CONF_GT911_ID]) + cg.add(var.set_index(config[CONF_INDEX])) diff --git a/esphome/components/gt911/binary_sensor/gt911_button.cpp b/esphome/components/gt911/binary_sensor/gt911_button.cpp new file mode 100644 index 0000000000..35ffaecefc --- /dev/null +++ b/esphome/components/gt911/binary_sensor/gt911_button.cpp @@ -0,0 +1,27 @@ +#include "gt911_button.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace gt911 { + +static const char *const TAG = "GT911.binary_sensor"; + +void GT911Button::setup() { + this->parent_->register_button_listener(this); + this->publish_initial_state(false); +} + +void GT911Button::dump_config() { + LOG_BINARY_SENSOR("", "GT911 Button", this); + ESP_LOGCONFIG(TAG, " Index: %u", this->index_); +} + +void GT911Button::update_button(uint8_t index, bool state) { + if (index != this->index_) + return; + + this->publish_state(state); +} + +} // namespace gt911 +} // namespace esphome diff --git a/esphome/components/gt911/binary_sensor/gt911_button.h b/esphome/components/gt911/binary_sensor/gt911_button.h new file mode 100644 index 0000000000..556ed65f91 --- /dev/null +++ b/esphome/components/gt911/binary_sensor/gt911_button.h @@ -0,0 +1,28 @@ +#pragma once + +#include "esphome/components/binary_sensor/binary_sensor.h" +#include "esphome/components/gt911/touchscreen/gt911_touchscreen.h" +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace gt911 { + +class GT911Button : public binary_sensor::BinarySensor, + public Component, + public GT911ButtonListener, + public Parented { + public: + void setup() override; + void dump_config() override; + + void set_index(uint8_t index) { this->index_ = index; } + + void update_button(uint8_t index, bool state) override; + + protected: + uint8_t index_; +}; + +} // namespace gt911 +} // namespace esphome diff --git a/esphome/components/gt911/touchscreen/__init__.py b/esphome/components/gt911/touchscreen/__init__.py new file mode 100644 index 0000000000..295e32b1b1 --- /dev/null +++ b/esphome/components/gt911/touchscreen/__init__.py @@ -0,0 +1,46 @@ +import esphome.codegen as cg +import esphome.config_validation as cv + +from esphome import pins +from esphome.components import i2c, touchscreen +from esphome.const import CONF_INTERRUPT_PIN, CONF_ID, CONF_ROTATION +from .. import gt911_ns + + +GT911ButtonListener = gt911_ns.class_("GT911ButtonListener") +GT911Touchscreen = gt911_ns.class_( + "GT911Touchscreen", + touchscreen.Touchscreen, + cg.Component, + i2c.I2CDevice, +) + +ROTATIONS = { + 0: touchscreen.TouchRotation.ROTATE_0_DEGREES, + 90: touchscreen.TouchRotation.ROTATE_90_DEGREES, + 180: touchscreen.TouchRotation.ROTATE_180_DEGREES, + 270: touchscreen.TouchRotation.ROTATE_270_DEGREES, +} +CONFIG_SCHEMA = ( + touchscreen.TOUCHSCREEN_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(GT911Touchscreen), + cv.Optional(CONF_ROTATION): cv.enum(ROTATIONS), + cv.Required(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema, + } + ) + .extend(i2c.i2c_device_schema(0x5D)) + .extend(cv.COMPONENT_SCHEMA) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + await touchscreen.register_touchscreen(var, config) + + interrupt_pin = await cg.gpio_pin_expression(config[CONF_INTERRUPT_PIN]) + cg.add(var.set_interrupt_pin(interrupt_pin)) + if CONF_ROTATION in config: + cg.add(var.set_rotation(ROTATIONS[config[CONF_ROTATION]])) diff --git a/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp b/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp new file mode 100644 index 0000000000..4d3e7e7903 --- /dev/null +++ b/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp @@ -0,0 +1,122 @@ +#include "gt911_touchscreen.h" + +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace gt911 { + +static const char *const TAG = "gt911.touchscreen"; + +static const uint8_t GET_TOUCH_STATE[2] = {0x81, 0x4E}; +static const uint8_t CLEAR_TOUCH_STATE[3] = {0x81, 0x4E, 0x00}; +static const uint8_t GET_TOUCHES[2] = {0x81, 0x4F}; +static const uint8_t GET_SWITCHES[2] = {0x80, 0x4D}; +static const size_t MAX_TOUCHES = 5; // max number of possible touches reported + +#define ERROR_CHECK(err) \ + if ((err) != i2c::ERROR_OK) { \ + ESP_LOGE(TAG, "Failed to communicate!"); \ + this->status_set_warning(); \ + return; \ + } + +void IRAM_ATTR HOT Store::gpio_intr(Store *store) { store->available = true; } + +void GT911Touchscreen::setup() { + i2c::ErrorCode err; + ESP_LOGCONFIG(TAG, "Setting up GT911 Touchscreen..."); + // datasheet says NOT to use pullup/down on the int line. + this->interrupt_pin_->pin_mode(gpio::FLAG_INPUT); + this->interrupt_pin_->setup(); + + // check the configuration of the int line. + uint8_t data; + err = this->write(GET_SWITCHES, 2); + if (err == i2c::ERROR_OK) { + err = this->read(&data, 1); + if (err == i2c::ERROR_OK) { + ESP_LOGD(TAG, "Read from switches: 0x%02X", data); + this->interrupt_pin_->attach_interrupt(Store::gpio_intr, &this->store_, + (data & 1) ? gpio::INTERRUPT_FALLING_EDGE : gpio::INTERRUPT_RISING_EDGE); + } + } + if (err != i2c::ERROR_OK) { + ESP_LOGE(TAG, "Failed to communicate!"); + this->mark_failed(); + return; + } + ESP_LOGCONFIG(TAG, "GT911 Touchscreen setup complete"); +} + +void GT911Touchscreen::loop() { + i2c::ErrorCode err; + touchscreen::TouchPoint tp; + uint8_t touch_state = 0; + uint8_t data[MAX_TOUCHES + 1][8]; // 8 bytes each for each point, plus extra space for the key byte + + if (!this->store_.available) + return; + this->store_.available = false; + + err = this->write(GET_TOUCH_STATE, sizeof(GET_TOUCH_STATE), false); + ERROR_CHECK(err); + err = this->read(&touch_state, 1); + ERROR_CHECK(err); + this->write(CLEAR_TOUCH_STATE, sizeof(CLEAR_TOUCH_STATE)); + + if ((touch_state & 0x80) == 0) + return; + uint8_t num_of_touches = touch_state & 0x07; + if (num_of_touches == 0) + this->send_release_(); + if (num_of_touches > MAX_TOUCHES) // should never happen + return; + + err = this->write(GET_TOUCHES, sizeof(GET_TOUCHES), false); + ERROR_CHECK(err); + // num_of_touches is guaranteed to be 0..5. Also read the key data + err = this->read(data[0], sizeof(data[0]) * num_of_touches + 1); + ERROR_CHECK(err); + + for (uint8_t i = 0; i != num_of_touches; i++) { + tp.id = data[i][0]; + uint16_t x = encode_uint16(data[i][2], data[i][1]); + uint16_t y = encode_uint16(data[i][4], data[i][3]); + + switch (this->rotation_) { + case touchscreen::ROTATE_0_DEGREES: + tp.x = x; + tp.y = y; + break; + case touchscreen::ROTATE_90_DEGREES: + tp.x = y; + tp.y = this->display_width_ - x; + break; + case touchscreen::ROTATE_180_DEGREES: + tp.x = this->display_width_ - x; + tp.y = this->display_height_ - y; + break; + case touchscreen::ROTATE_270_DEGREES: + tp.x = this->display_height_ - y; + tp.y = x; + break; + } + this->defer([this, tp]() { this->send_touch_(tp); }); + } + auto keys = data[num_of_touches][0]; + for (size_t i = 0; i != 4; i++) { + for (auto *listener : this->button_listeners_) + listener->update_button(i, (keys & (1 << i)) != 0); + } +} + +void GT911Touchscreen::dump_config() { + ESP_LOGCONFIG(TAG, "GT911 Touchscreen:"); + LOG_I2C_DEVICE(this); + LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_); + ESP_LOGCONFIG(TAG, " Rotation: %d", (int) this->rotation_); +} + +} // namespace gt911 +} // namespace esphome diff --git a/esphome/components/gt911/touchscreen/gt911_touchscreen.h b/esphome/components/gt911/touchscreen/gt911_touchscreen.h new file mode 100644 index 0000000000..dc9248bb4a --- /dev/null +++ b/esphome/components/gt911/touchscreen/gt911_touchscreen.h @@ -0,0 +1,39 @@ +#pragma once + +#include "esphome/components/i2c/i2c.h" +#include "esphome/components/touchscreen/touchscreen.h" +#include "esphome/core/component.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace gt911 { + +struct Store { + volatile bool available; + + static void gpio_intr(Store *store); +}; + +class GT911ButtonListener { + public: + virtual void update_button(uint8_t index, bool state) = 0; +}; + +class GT911Touchscreen : public touchscreen::Touchscreen, public Component, public i2c::I2CDevice { + public: + void setup() override; + void loop() override; + void dump_config() override; + void set_rotation(touchscreen::TouchRotation rotation) { this->rotation_ = rotation; } + + void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; } + void register_button_listener(GT911ButtonListener *listener) { this->button_listeners_.push_back(listener); } + + protected: + InternalGPIOPin *interrupt_pin_; + Store store_; + std::vector button_listeners_; +}; + +} // namespace gt911 +} // namespace esphome diff --git a/esphome/components/touchscreen/touchscreen.cpp b/esphome/components/touchscreen/touchscreen.cpp index 2eaa736171..20828aad8b 100644 --- a/esphome/components/touchscreen/touchscreen.cpp +++ b/esphome/components/touchscreen/touchscreen.cpp @@ -18,6 +18,11 @@ void Touchscreen::set_display(display::Display *display) { } } +void Touchscreen::send_release_() { + for (auto *listener : this->touch_listeners_) + listener->release(); +} + void Touchscreen::send_touch_(TouchPoint tp) { ESP_LOGV(TAG, "Touch (x=%d, y=%d)", tp.x, tp.y); this->touch_trigger_.trigger(tp); diff --git a/esphome/components/touchscreen/touchscreen.h b/esphome/components/touchscreen/touchscreen.h index 24b3191880..6e07bcfea0 100644 --- a/esphome/components/touchscreen/touchscreen.h +++ b/esphome/components/touchscreen/touchscreen.h @@ -41,6 +41,7 @@ class Touchscreen { protected: /// Call this function to send touch points to the `on_touch` listener and the binary_sensors. void send_touch_(TouchPoint tp); + void send_release_(); uint16_t display_width_; uint16_t display_height_; diff --git a/tests/test4.yaml b/tests/test4.yaml index c27dbb65ac..63bf5bf0ae 100644 --- a/tests/test4.yaml +++ b/tests/test4.yaml @@ -405,6 +405,10 @@ binary_sensor: y_max: 100 on_press: - logger.log: Touched + - platform: gt911 + id: touch_key_911 + index: 0 + - platform: gpio name: MaxIn Pin 4 @@ -725,6 +729,10 @@ touchscreen: - logger.log: format: Touch at (%d, %d) args: [touch.x, touch.y] + - platform: gt911 + interrupt_pin: GPIO3 + display: inkplate_display + i2s_audio: i2s_lrclk_pin: GPIO26 From 3b77f05cc9c25c5b51678af112558b124d66a78f Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 28 Nov 2023 09:57:40 +1300 Subject: [PATCH 08/89] Add 'voice_assistant.connected' condition (#5845) --- esphome/components/voice_assistant/__init__.py | 12 ++++++++++++ esphome/components/voice_assistant/voice_assistant.h | 5 +++++ 2 files changed, 17 insertions(+) diff --git a/esphome/components/voice_assistant/__init__.py b/esphome/components/voice_assistant/__init__.py index d05f39072c..59aef901f2 100644 --- a/esphome/components/voice_assistant/__init__.py +++ b/esphome/components/voice_assistant/__init__.py @@ -57,6 +57,9 @@ StopAction = voice_assistant_ns.class_( IsRunningCondition = voice_assistant_ns.class_( "IsRunningCondition", automation.Condition, cg.Parented.template(VoiceAssistant) ) +ConnectedCondition = voice_assistant_ns.class_( + "ConnectedCondition", automation.Condition, cg.Parented.template(VoiceAssistant) +) def tts_stream_validate(config): @@ -298,3 +301,12 @@ async def voice_assistant_is_running_to_code(config, condition_id, template_arg, var = cg.new_Pvariable(condition_id, template_arg) await cg.register_parented(var, config[CONF_ID]) return var + + +@register_condition( + "voice_assistant.connected", ConnectedCondition, VOICE_ASSISTANT_ACTION_SCHEMA +) +async def voice_assistant_connected_to_code(config, condition_id, template_arg, args): + var = cg.new_Pvariable(condition_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var diff --git a/esphome/components/voice_assistant/voice_assistant.h b/esphome/components/voice_assistant/voice_assistant.h index 66bf4c3c57..f9325dff54 100644 --- a/esphome/components/voice_assistant/voice_assistant.h +++ b/esphome/components/voice_assistant/voice_assistant.h @@ -222,6 +222,11 @@ template class IsRunningCondition : public Condition, pub bool check(Ts... x) override { return this->parent_->is_running() || this->parent_->is_continuous(); } }; +template class ConnectedCondition : public Condition, public Parented { + public: + bool check(Ts... x) override { return this->parent_->get_api_connection() != nullptr; } +}; + extern VoiceAssistant *global_voice_assistant; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) } // namespace voice_assistant From 019315afa0191a6c41a3d03274d2b9d5c7b8468d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Nov 2023 21:07:48 +0000 Subject: [PATCH 09/89] Bump aioesphomeapi from 18.5.9 to 19.1.2 (#5844) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 3d096ffd52..09f5c303bb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ platformio==6.1.11 # When updating platformio, also update Dockerfile esptool==4.6.2 click==8.1.7 esphome-dashboard==20231107.0 -aioesphomeapi==18.5.9 +aioesphomeapi==19.1.2 zeroconf==0.127.0 python-magic==0.4.27 From 460362b11f891e4bd316046c6fb7fca9083ff80c Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Mon, 27 Nov 2023 22:09:57 +0100 Subject: [PATCH 10/89] Nextion - Standardizing log messages (#5837) --- esphome/components/nextion/nextion.cpp | 38 +++++++++++++++++++------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index 92afdfe3dc..81e905b979 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -433,8 +433,10 @@ void Nextion::process_nextion_commands_() { uint8_t page_id = to_process[0]; uint8_t component_id = to_process[1]; uint8_t touch_event = to_process[2]; // 0 -> release, 1 -> press - ESP_LOGD(TAG, "Got touch page=%u component=%u type=%s", page_id, component_id, - touch_event ? "PRESS" : "RELEASE"); + ESP_LOGD(TAG, "Got touch event:"); + ESP_LOGD(TAG, " page_id: %u", page_id); + ESP_LOGD(TAG, " component_id: %u", component_id); + ESP_LOGD(TAG, " event type: %s", touch_event ? "PRESS" : "RELEASE"); for (auto *touch : this->touch_) { touch->process_touch(page_id, component_id, touch_event != 0); } @@ -448,7 +450,7 @@ void Nextion::process_nextion_commands_() { } uint8_t page_id = to_process[0]; - ESP_LOGD(TAG, "Got new page=%u", page_id); + ESP_LOGD(TAG, "Got new page: %u", page_id); this->page_callback_.call(page_id); break; } @@ -466,7 +468,10 @@ void Nextion::process_nextion_commands_() { uint16_t x = (uint16_t(to_process[0]) << 8) | to_process[1]; uint16_t y = (uint16_t(to_process[2]) << 8) | to_process[3]; uint8_t touch_event = to_process[4]; // 0 -> release, 1 -> press - ESP_LOGD(TAG, "Got touch at x=%u y=%u type=%s", x, y, touch_event ? "PRESS" : "RELEASE"); + ESP_LOGD(TAG, "Got touch event:"); + ESP_LOGD(TAG, " x: %u", x); + ESP_LOGD(TAG, " y: %u", y); + ESP_LOGD(TAG, " type: %s", touch_event ? "PRESS" : "RELEASE"); break; } @@ -588,7 +593,9 @@ void Nextion::process_nextion_commands_() { variable_name = to_process.substr(0, index); ++index; - ESP_LOGN(TAG, "Got Switch variable_name=%s value=%d", variable_name.c_str(), to_process[0] != 0); + ESP_LOGN(TAG, "Got Switch:"); + ESP_LOGN(TAG, " variable_name: %s", variable_name.c_str()); + ESP_LOGN(TAG, " value: %d", to_process[0] != 0); for (auto *switchtype : this->switchtype_) { switchtype->process_bool(variable_name, to_process[index] != 0); @@ -619,7 +626,9 @@ void Nextion::process_nextion_commands_() { value += to_process[i + index + 1] << (8 * i); } - ESP_LOGN(TAG, "Got sensor variable_name=%s value=%d", variable_name.c_str(), value); + ESP_LOGN(TAG, "Got sensor:"); + ESP_LOGN(TAG, " variable_name: %s", variable_name.c_str()); + ESP_LOGN(TAG, " value: %d", value); for (auto *sensor : this->sensortype_) { sensor->process_sensor(variable_name, value); @@ -651,7 +660,9 @@ void Nextion::process_nextion_commands_() { text_value = to_process.substr(index); - ESP_LOGN(TAG, "Got Text Sensor variable_name=%s value=%s", variable_name.c_str(), text_value.c_str()); + ESP_LOGN(TAG, "Got Text Sensor:"); + ESP_LOGN(TAG, " variable_name: %s", variable_name.c_str()); + ESP_LOGN(TAG, " value: %s", text_value.c_str()); // NextionTextSensorResponseQueue *nq = new NextionTextSensorResponseQueue; // nq->variable_name = variable_name; @@ -682,7 +693,9 @@ void Nextion::process_nextion_commands_() { variable_name = to_process.substr(0, index); ++index; - ESP_LOGN(TAG, "Got Binary Sensor variable_name=%s value=%d", variable_name.c_str(), to_process[index] != 0); + ESP_LOGN(TAG, "Got Binary Sensor:"); + ESP_LOGN(TAG, " variable_name: %s", variable_name.c_str()); + ESP_LOGN(TAG, " value: %d", to_process[index] != 0); for (auto *binarysensortype : this->binarysensortype_) { binarysensortype->process_bool(&variable_name[0], to_process[index] != 0); @@ -772,7 +785,10 @@ void Nextion::set_nextion_sensor_state(int queue_type, const std::string &name, } void Nextion::set_nextion_sensor_state(NextionQueueType queue_type, const std::string &name, float state) { - ESP_LOGN(TAG, "Received state for variable %s, state %lf for queue type %d", name.c_str(), state, queue_type); + ESP_LOGN(TAG, "Received state:"); + ESP_LOGN(TAG, " variable: %s", name.c_str()); + ESP_LOGN(TAG, " state: %lf", state); + ESP_LOGN(TAG, " queue type: %d", queue_type); switch (queue_type) { case NextionQueueType::SENSOR: { @@ -809,7 +825,9 @@ void Nextion::set_nextion_sensor_state(NextionQueueType queue_type, const std::s } void Nextion::set_nextion_text_state(const std::string &name, const std::string &state) { - ESP_LOGD(TAG, "Received state for variable %s, state %s", name.c_str(), state.c_str()); + ESP_LOGD(TAG, "Received state:"); + ESP_LOGD(TAG, " variable: %s", name.c_str()); + ESP_LOGD(TAG, " state: %s", state.c_str()); for (auto *sensor : this->textsensortype_) { if (name == sensor->get_variable_name()) { From 4e6d3729e178999daea09421e42ccf022c768509 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 27 Nov 2023 16:39:24 -0600 Subject: [PATCH 11/89] dashboard: Small cleanups to dashboard (#5841) --- esphome/dashboard/entries.py | 2 +- esphome/dashboard/settings.py | 24 +++--- esphome/dashboard/web_server.py | 135 +++++++++++++++++--------------- 3 files changed, 87 insertions(+), 74 deletions(-) diff --git a/esphome/dashboard/entries.py b/esphome/dashboard/entries.py index 8ccfa795d5..ad139b830b 100644 --- a/esphome/dashboard/entries.py +++ b/esphome/dashboard/entries.py @@ -262,7 +262,7 @@ class DashboardEntry: self.state = EntryState.UNKNOWN self._to_dict: dict[str, Any] | None = None - def __repr__(self): + def __repr__(self) -> str: """Return the representation of this entry.""" return ( f"DashboardEntry(path={self.path} " diff --git a/esphome/dashboard/settings.py b/esphome/dashboard/settings.py index 61718298d2..1a5b1620e8 100644 --- a/esphome/dashboard/settings.py +++ b/esphome/dashboard/settings.py @@ -23,45 +23,45 @@ class DashboardSettings: self.cookie_secret: str | None = None self.absolute_config_dir: Path | None = None - def parse_args(self, args): + def parse_args(self, args: Any) -> None: self.on_ha_addon: bool = args.ha_addon - password: str = args.password or os.getenv("PASSWORD", "") + password = args.password or os.getenv("PASSWORD") or "" if not self.on_ha_addon: - self.username: str = args.username or os.getenv("USERNAME", "") + self.username = args.username or os.getenv("USERNAME") or "" self.using_password = bool(password) if self.using_password: self.password_hash = password_hash(password) - self.config_dir: str = args.configuration - self.absolute_config_dir: Path = Path(self.config_dir).resolve() + self.config_dir = args.configuration + self.absolute_config_dir = Path(self.config_dir).resolve() CORE.config_path = os.path.join(self.config_dir, ".") @property - def relative_url(self): - return os.getenv("ESPHOME_DASHBOARD_RELATIVE_URL", "/") + def relative_url(self) -> str: + return os.getenv("ESPHOME_DASHBOARD_RELATIVE_URL") or "/" @property def status_use_ping(self): return get_bool_env("ESPHOME_DASHBOARD_USE_PING") @property - def status_use_mqtt(self): + def status_use_mqtt(self) -> bool: return get_bool_env("ESPHOME_DASHBOARD_USE_MQTT") @property - def using_ha_addon_auth(self): + def using_ha_addon_auth(self) -> bool: if not self.on_ha_addon: return False return not get_bool_env("DISABLE_HA_AUTHENTICATION") @property - def using_auth(self): + def using_auth(self) -> bool: return self.using_password or self.using_ha_addon_auth @property - def streamer_mode(self): + def streamer_mode(self) -> bool: return get_bool_env("ESPHOME_STREAMER_MODE") - def check_password(self, username, password): + def check_password(self, username: str, password: str) -> bool: if not self.using_auth: return True if username != self.username: diff --git a/esphome/dashboard/web_server.py b/esphome/dashboard/web_server.py index 9a9ccb462b..9bbf0b28dc 100644 --- a/esphome/dashboard/web_server.py +++ b/esphome/dashboard/web_server.py @@ -14,12 +14,14 @@ import shutil import subprocess import threading from pathlib import Path -from typing import Any +from typing import Any, Callable, TypeVar +from collections.abc import Iterable import tornado import tornado.concurrent import tornado.gen import tornado.httpserver +import tornado.httputil import tornado.ioloop import tornado.iostream import tornado.netutil @@ -27,9 +29,9 @@ import tornado.process import tornado.queues import tornado.web import tornado.websocket -import tornado.httputil import yaml from tornado.log import access_log +from yaml.nodes import Node from esphome import const, platformio_api, yaml_util from esphome.helpers import get_bool_env, mkdir_p @@ -54,7 +56,7 @@ cookie_authenticated_yes = b"yes" settings = DASHBOARD.settings -def template_args(): +def template_args() -> dict[str, Any]: version = const.__version__ if "b" in version: docs_link = "https://beta.esphome.io/" @@ -73,9 +75,12 @@ def template_args(): } -def authenticated(func): +T = TypeVar("T", bound=Callable[..., Any]) + + +def authenticated(func: T) -> T: @functools.wraps(func) - def decorator(self, *args, **kwargs): + def decorator(self, *args: Any, **kwargs: Any): if not is_authenticated(self): self.redirect("./login") return None @@ -209,7 +214,7 @@ class EsphomeCommandWebSocket(tornado.websocket.WebSocketHandler): tornado.ioloop.IOLoop.current().spawn_callback(self._redirect_stdout) @property - def is_process_active(self): + def is_process_active(self) -> bool: return self._proc is not None and self._proc.returncode is None @websocket_method("stdin") @@ -398,7 +403,7 @@ class EsphomeUpdateAllHandler(EsphomeCommandWebSocket): class SerialPortRequestHandler(BaseHandler): @authenticated - async def get(self): + async def get(self) -> None: ports = await asyncio.get_running_loop().run_in_executor(None, get_serial_ports) data = [] for port in ports: @@ -418,7 +423,7 @@ class SerialPortRequestHandler(BaseHandler): class WizardRequestHandler(BaseHandler): @authenticated - def post(self): + def post(self) -> None: from esphome import wizard kwargs = { @@ -449,7 +454,7 @@ class WizardRequestHandler(BaseHandler): class ImportRequestHandler(BaseHandler): @authenticated - def post(self): + def post(self) -> None: from esphome.components.dashboard_import import import_config dashboard = DASHBOARD @@ -504,7 +509,7 @@ class ImportRequestHandler(BaseHandler): class DownloadListRequestHandler(BaseHandler): @authenticated @bind_config - def get(self, configuration=None): + def get(self, configuration: str | None = None) -> None: storage_path = ext_storage_path(configuration) storage_json = StorageJSON.load(storage_path) if storage_json is None: @@ -512,26 +517,29 @@ class DownloadListRequestHandler(BaseHandler): return from esphome.components.esp32 import VARIANTS as ESP32_VARIANTS - from esphome.components.esp32 import get_download_types as esp32_types - from esphome.components.esp8266 import get_download_types as esp8266_types - from esphome.components.libretiny import get_download_types as libretiny_types - from esphome.components.rp2040 import get_download_types as rp2040_types downloads = [] - platform = storage_json.target_platform.lower() + platform: str = storage_json.target_platform.lower() if platform == const.PLATFORM_RP2040: + from esphome.components.rp2040 import get_download_types as rp2040_types + downloads = rp2040_types(storage_json) elif platform == const.PLATFORM_ESP8266: + from esphome.components.esp8266 import get_download_types as esp8266_types + downloads = esp8266_types(storage_json) elif platform.upper() in ESP32_VARIANTS: + from esphome.components.esp32 import get_download_types as esp32_types + downloads = esp32_types(storage_json) - elif platform == const.PLATFORM_BK72XX: - downloads = libretiny_types(storage_json) - elif platform == const.PLATFORM_RTL87XX: + elif platform in (const.PLATFORM_RTL87XX, const.PLATFORM_BK72XX): + from esphome.components.libretiny import ( + get_download_types as libretiny_types, + ) + downloads = libretiny_types(storage_json) else: - self.send_error(418) - return + raise ValueError(f"Unknown platform {platform}") self.set_status(200) self.set_header("content-type", "application/json") @@ -551,7 +559,7 @@ class DownloadBinaryRequestHandler(BaseHandler): @authenticated @bind_config - async def get(self, configuration: str | None = None): + async def get(self, configuration: str | None = None) -> None: """Download a binary file.""" loop = asyncio.get_running_loop() compressed = self.get_argument("compressed", "0") == "1" @@ -618,7 +626,7 @@ class DownloadBinaryRequestHandler(BaseHandler): class EsphomeVersionHandler(BaseHandler): @authenticated - def get(self): + def get(self) -> None: self.set_header("Content-Type", "application/json") self.write(json.dumps({"version": const.__version__})) self.finish() @@ -626,7 +634,7 @@ class EsphomeVersionHandler(BaseHandler): class ListDevicesHandler(BaseHandler): @authenticated - async def get(self): + async def get(self) -> None: dashboard = DASHBOARD await dashboard.entries.async_request_update_entries() entries = dashboard.entries.async_all() @@ -656,7 +664,7 @@ class ListDevicesHandler(BaseHandler): class MainRequestHandler(BaseHandler): @authenticated - def get(self): + def get(self) -> None: begin = bool(self.get_argument("begin", False)) self.render( @@ -669,7 +677,7 @@ class MainRequestHandler(BaseHandler): class PrometheusServiceDiscoveryHandler(BaseHandler): @authenticated - async def get(self): + async def get(self) -> None: dashboard = DASHBOARD await dashboard.entries.async_request_update_entries() entries = dashboard.entries.async_all() @@ -698,29 +706,34 @@ class PrometheusServiceDiscoveryHandler(BaseHandler): class BoardsRequestHandler(BaseHandler): @authenticated - def get(self, platform: str): - from esphome.components.bk72xx.boards import BOARDS as BK72XX_BOARDS - from esphome.components.esp32.boards import BOARDS as ESP32_BOARDS - from esphome.components.esp8266.boards import BOARDS as ESP8266_BOARDS - from esphome.components.rp2040.boards import BOARDS as RP2040_BOARDS - from esphome.components.rtl87xx.boards import BOARDS as RTL87XX_BOARDS - - platform_to_boards = { - const.PLATFORM_ESP32: ESP32_BOARDS, - const.PLATFORM_ESP8266: ESP8266_BOARDS, - const.PLATFORM_RP2040: RP2040_BOARDS, - const.PLATFORM_BK72XX: BK72XX_BOARDS, - const.PLATFORM_RTL87XX: RTL87XX_BOARDS, - } + def get(self, platform: str) -> None: # filter all ESP32 variants by requested platform if platform.startswith("esp32"): + from esphome.components.esp32.boards import BOARDS as ESP32_BOARDS + boards = { k: v - for k, v in platform_to_boards[const.PLATFORM_ESP32].items() + for k, v in ESP32_BOARDS.items() if v[const.KEY_VARIANT] == platform.upper() } + elif platform == const.PLATFORM_ESP8266: + from esphome.components.esp8266.boards import BOARDS as ESP8266_BOARDS + + boards = ESP8266_BOARDS + elif platform == const.PLATFORM_RP2040: + from esphome.components.rp2040.boards import BOARDS as RP2040_BOARDS + + boards = RP2040_BOARDS + elif platform == const.PLATFORM_BK72XX: + from esphome.components.bk72xx.boards import BOARDS as BK72XX_BOARDS + + boards = BK72XX_BOARDS + elif platform == const.PLATFORM_RTL87XX: + from esphome.components.rtl87xx.boards import BOARDS as RTL87XX_BOARDS + + boards = RTL87XX_BOARDS else: - boards = platform_to_boards[platform] + raise ValueError(f"Unknown platform {platform}") # map to a {board_name: board_title} dict platform_boards = {key: val[const.KEY_NAME] for key, val in boards.items()} @@ -734,7 +747,7 @@ class BoardsRequestHandler(BaseHandler): class PingRequestHandler(BaseHandler): @authenticated - def get(self): + def get(self) -> None: dashboard = DASHBOARD dashboard.ping_request.set() if settings.status_use_mqtt: @@ -754,7 +767,7 @@ class PingRequestHandler(BaseHandler): class InfoRequestHandler(BaseHandler): @authenticated @bind_config - async def get(self, configuration=None): + async def get(self, configuration: str | None = None) -> None: yaml_path = settings.rel_path(configuration) dashboard = DASHBOARD entry = dashboard.entries.get(yaml_path) @@ -770,7 +783,7 @@ class InfoRequestHandler(BaseHandler): class EditRequestHandler(BaseHandler): @authenticated @bind_config - async def get(self, configuration: str | None = None): + async def get(self, configuration: str | None = None) -> None: """Get the content of a file.""" loop = asyncio.get_running_loop() filename = settings.rel_path(configuration) @@ -788,7 +801,7 @@ class EditRequestHandler(BaseHandler): @authenticated @bind_config - async def post(self, configuration: str | None = None): + async def post(self, configuration: str | None = None) -> None: """Write the content of a file.""" loop = asyncio.get_running_loop() config_file = settings.rel_path(configuration) @@ -805,7 +818,7 @@ class EditRequestHandler(BaseHandler): class DeleteRequestHandler(BaseHandler): @authenticated @bind_config - def post(self, configuration=None): + def post(self, configuration: str | None = None) -> None: config_file = settings.rel_path(configuration) storage_path = ext_storage_path(configuration) @@ -825,20 +838,20 @@ class DeleteRequestHandler(BaseHandler): class UndoDeleteRequestHandler(BaseHandler): @authenticated @bind_config - def post(self, configuration=None): + def post(self, configuration: str | None = None) -> None: config_file = settings.rel_path(configuration) trash_path = trash_storage_path() shutil.move(os.path.join(trash_path, configuration), config_file) class LoginHandler(BaseHandler): - def get(self): + def get(self) -> None: if is_authenticated(self): self.redirect("./") else: self.render_login_page() - def render_login_page(self, error=None): + def render_login_page(self, error: str | None = None) -> None: self.render( "login.template.html", error=error, @@ -847,7 +860,7 @@ class LoginHandler(BaseHandler): **template_args(), ) - def post_ha_addon_login(self): + def post_ha_addon_login(self) -> None: import requests headers = { @@ -874,7 +887,7 @@ class LoginHandler(BaseHandler): self.set_status(401) self.render_login_page(error="Invalid username or password") - def post_native_login(self): + def post_native_login(self) -> None: username = self.get_argument("username", "") password = self.get_argument("password", "") if settings.check_password(username, password): @@ -887,7 +900,7 @@ class LoginHandler(BaseHandler): self.set_status(401) self.render_login_page(error=error_str) - def post(self): + def post(self) -> None: if settings.using_ha_addon_auth: self.post_ha_addon_login() else: @@ -896,14 +909,14 @@ class LoginHandler(BaseHandler): class LogoutHandler(BaseHandler): @authenticated - def get(self): + def get(self) -> None: self.clear_cookie("authenticated") self.redirect("./login") class SecretKeysRequestHandler(BaseHandler): @authenticated - def get(self): + def get(self) -> None: filename = None for secret_filename in const.SECRETS_FILES: @@ -923,10 +936,10 @@ class SecretKeysRequestHandler(BaseHandler): class SafeLoaderIgnoreUnknown(FastestAvailableSafeLoader): - def ignore_unknown(self, node): + def ignore_unknown(self, node: Node) -> str: return f"{node.tag} {node.value}" - def construct_yaml_binary(self, node) -> str: + def construct_yaml_binary(self, node: Node) -> str: return super().construct_yaml_binary(node).decode("ascii") @@ -939,7 +952,7 @@ SafeLoaderIgnoreUnknown.add_constructor( class JsonConfigRequestHandler(BaseHandler): @authenticated @bind_config - async def get(self, configuration=None): + async def get(self, configuration: str | None = None) -> None: filename = settings.rel_path(configuration) if not os.path.isfile(filename): self.send_error(404) @@ -959,7 +972,7 @@ class JsonConfigRequestHandler(BaseHandler): self.finish() -def get_base_frontend_path(): +def get_base_frontend_path() -> str: if ENV_DEV not in os.environ: import esphome_dashboard @@ -973,12 +986,12 @@ def get_base_frontend_path(): return os.path.abspath(os.path.join(os.getcwd(), static_path, "esphome_dashboard")) -def get_static_path(*args): +def get_static_path(*args: Iterable[str]) -> str: return os.path.join(get_base_frontend_path(), "static", *args) @functools.cache -def get_static_file_url(name): +def get_static_file_url(name: str) -> str: base = f"./static/{name}" if ENV_DEV in os.environ: @@ -997,7 +1010,7 @@ def get_static_file_url(name): def make_app(debug=get_bool_env(ENV_DEV)) -> tornado.web.Application: - def log_function(handler): + def log_function(handler: tornado.web.RequestHandler) -> None: if handler.get_status() < 400: log_method = access_log.info From 4b6fbd5db016ee2e50a4b8bb2f416c2d6c4e7e59 Mon Sep 17 00:00:00 2001 From: functionpointer Date: Mon, 27 Nov 2023 23:43:03 +0100 Subject: [PATCH 12/89] Pylontech integration (solar battery bank) (#4688) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/pylontech/__init__.py | 46 +++++++++ esphome/components/pylontech/pylontech.cpp | 91 +++++++++++++++++ esphome/components/pylontech/pylontech.h | 53 ++++++++++ .../components/pylontech/sensor/__init__.py | 97 +++++++++++++++++++ .../pylontech/sensor/pylontech_sensor.cpp | 60 ++++++++++++ .../pylontech/sensor/pylontech_sensor.h | 32 ++++++ .../pylontech/text_sensor/__init__.py | 41 ++++++++ .../text_sensor/pylontech_text_sensor.cpp | 40 ++++++++ .../text_sensor/pylontech_text_sensor.h | 26 +++++ tests/test4.yaml | 41 ++++++++ 11 files changed, 528 insertions(+) create mode 100644 esphome/components/pylontech/__init__.py create mode 100644 esphome/components/pylontech/pylontech.cpp create mode 100644 esphome/components/pylontech/pylontech.h create mode 100644 esphome/components/pylontech/sensor/__init__.py create mode 100644 esphome/components/pylontech/sensor/pylontech_sensor.cpp create mode 100644 esphome/components/pylontech/sensor/pylontech_sensor.h create mode 100644 esphome/components/pylontech/text_sensor/__init__.py create mode 100644 esphome/components/pylontech/text_sensor/pylontech_text_sensor.cpp create mode 100644 esphome/components/pylontech/text_sensor/pylontech_text_sensor.h diff --git a/CODEOWNERS b/CODEOWNERS index 2cd08a780e..e6bc53ee6f 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -240,6 +240,7 @@ esphome/components/preferences/* @esphome/core esphome/components/psram/* @esphome/core esphome/components/pulse_meter/* @TrentHouliston @cstaahl @stevebaxter esphome/components/pvvx_mithermometer/* @pasiz +esphome/components/pylontech/* @functionpointer esphome/components/qmp6988/* @andrewpc esphome/components/qr_code/* @wjtje esphome/components/qwiic_pir/* @kahrendt diff --git a/esphome/components/pylontech/__init__.py b/esphome/components/pylontech/__init__.py new file mode 100644 index 0000000000..56fac92e89 --- /dev/null +++ b/esphome/components/pylontech/__init__.py @@ -0,0 +1,46 @@ +import logging +import esphome.codegen as cg +from esphome.components import uart +import esphome.config_validation as cv +from esphome.const import CONF_ID + +_LOGGER = logging.getLogger(__name__) + +CODEOWNERS = ["@functionpointer"] +DEPENDENCIES = ["uart"] +MULTI_CONF = True + +CONF_PYLONTECH_ID = "pylontech_id" +CONF_BATTERY = "battery" + +pylontech_ns = cg.esphome_ns.namespace("pylontech") +PylontechComponent = pylontech_ns.class_( + "PylontechComponent", cg.PollingComponent, uart.UARTDevice +) +PylontechBattery = pylontech_ns.class_("PylontechBattery") + +CV_NUM_BATTERIES = cv.int_range(1, 6) + +PYLONTECH_COMPONENT_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_PYLONTECH_ID): cv.use_id(PylontechComponent), + cv.Required(CONF_BATTERY): CV_NUM_BATTERIES, + } +) + + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(PylontechComponent), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(uart.UART_DEVICE_SCHEMA) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await uart.register_uart_device(var, config) diff --git a/esphome/components/pylontech/pylontech.cpp b/esphome/components/pylontech/pylontech.cpp new file mode 100644 index 0000000000..4bfa876110 --- /dev/null +++ b/esphome/components/pylontech/pylontech.cpp @@ -0,0 +1,91 @@ +#include "pylontech.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace pylontech { + +static const char *const TAG = "pylontech"; +static const int MAX_DATA_LENGTH_BYTES = 256; +static const uint8_t ASCII_LF = 0x0A; + +PylontechComponent::PylontechComponent() {} + +void PylontechComponent::dump_config() { + this->check_uart_settings(115200, 1, esphome::uart::UART_CONFIG_PARITY_NONE, 8); + ESP_LOGCONFIG(TAG, "pylontech:"); + if (this->is_failed()) { + ESP_LOGE(TAG, "Connection with pylontech failed!"); + } + + for (PylontechListener *listener : this->listeners_) { + listener->dump_config(); + } + + LOG_UPDATE_INTERVAL(this); +} + +void PylontechComponent::setup() { + ESP_LOGCONFIG(TAG, "Setting up pylontech..."); + while (this->available() != 0) { + this->read(); + } +} + +void PylontechComponent::update() { this->write_str("pwr\n"); } + +void PylontechComponent::loop() { + uint8_t data; + + // pylontech sends a lot of data very suddenly + // we need to quickly put it all into our own buffer, otherwise the uart's buffer will overflow + while (this->available() > 0) { + if (this->read_byte(&data)) { + buffer_[buffer_index_write_] += (char) data; + if (buffer_[buffer_index_write_].back() == static_cast(ASCII_LF) || + buffer_[buffer_index_write_].length() >= MAX_DATA_LENGTH_BYTES) { + // complete line received + buffer_index_write_ = (buffer_index_write_ + 1) % NUM_BUFFERS; + } + } + } + + // only process one line per call of loop() to not block esphome for too long + if (buffer_index_read_ != buffer_index_write_) { + this->process_line_(buffer_[buffer_index_read_]); + buffer_[buffer_index_read_].clear(); + buffer_index_read_ = (buffer_index_read_ + 1) % NUM_BUFFERS; + } +} + +void PylontechComponent::process_line_(std::string &buffer) { + ESP_LOGV(TAG, "Read from serial: %s", buffer.substr(0, buffer.size() - 2).c_str()); + // clang-format off + // example line to parse: + // Power Volt Curr Tempr Tlow Thigh Vlow Vhigh Base.St Volt.St Curr.St Temp.St Coulomb Time B.V.St B.T.St MosTempr M.T.St + // 1 50548 8910 25000 24200 25000 3368 3371 Charge Normal Normal Normal 97% 2021-06-30 20:49:45 Normal Normal 22700 Normal + // clang-format on + + PylontechListener::LineContents l{}; + const int parsed = sscanf( // NOLINT + buffer.c_str(), "%d %d %d %d %d %d %d %d %7s %7s %7s %7s %d%% %*d-%*d-%*d %*d:%*d:%*d %*s %*s %d %*s", // NOLINT + &l.bat_num, &l.volt, &l.curr, &l.tempr, &l.tlow, &l.thigh, &l.vlow, &l.vhigh, l.base_st, l.volt_st, // NOLINT + l.curr_st, l.temp_st, &l.coulomb, &l.mostempr); // NOLINT + + if (l.bat_num <= 0) { + ESP_LOGD(TAG, "invalid bat_num in line %s", buffer.substr(0, buffer.size() - 2).c_str()); + return; + } + if (parsed != 14) { + ESP_LOGW(TAG, "invalid line: found only %d items in %s", parsed, buffer.substr(0, buffer.size() - 2).c_str()); + return; + } + + for (PylontechListener *listener : this->listeners_) { + listener->on_line_read(&l); + } +} + +float PylontechComponent::get_setup_priority() const { return setup_priority::DATA; } + +} // namespace pylontech +} // namespace esphome diff --git a/esphome/components/pylontech/pylontech.h b/esphome/components/pylontech/pylontech.h new file mode 100644 index 0000000000..3282cb4d9f --- /dev/null +++ b/esphome/components/pylontech/pylontech.h @@ -0,0 +1,53 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/defines.h" +#include "esphome/components/uart/uart.h" + +namespace esphome { +namespace pylontech { + +static const uint8_t NUM_BUFFERS = 20; +static const uint8_t TEXT_SENSOR_MAX_LEN = 8; + +class PylontechListener { + public: + struct LineContents { + int bat_num = 0, volt, curr, tempr, tlow, thigh, vlow, vhigh, coulomb, mostempr; + char base_st[TEXT_SENSOR_MAX_LEN], volt_st[TEXT_SENSOR_MAX_LEN], curr_st[TEXT_SENSOR_MAX_LEN], + temp_st[TEXT_SENSOR_MAX_LEN]; + }; + + virtual void on_line_read(LineContents *line); + virtual void dump_config(); +}; + +class PylontechComponent : public PollingComponent, public uart::UARTDevice { + public: + PylontechComponent(); + + /// Schedule data readings. + void update() override; + /// Read data once available + void loop() override; + /// Setup the sensor and test for a connection. + void setup() override; + void dump_config() override; + + float get_setup_priority() const override; + + void register_listener(PylontechListener *listener) { this->listeners_.push_back(listener); } + + protected: + void process_line_(std::string &buffer); + + // ring buffer + std::string buffer_[NUM_BUFFERS]; + int buffer_index_write_ = 0; + int buffer_index_read_ = 0; + + std::vector listeners_{}; +}; + +} // namespace pylontech +} // namespace esphome diff --git a/esphome/components/pylontech/sensor/__init__.py b/esphome/components/pylontech/sensor/__init__.py new file mode 100644 index 0000000000..0423f3370c --- /dev/null +++ b/esphome/components/pylontech/sensor/__init__.py @@ -0,0 +1,97 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor +from esphome.const import ( + CONF_VOLTAGE, + CONF_CURRENT, + CONF_TEMPERATURE, + UNIT_VOLT, + UNIT_AMPERE, + DEVICE_CLASS_VOLTAGE, + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_BATTERY, + UNIT_CELSIUS, + UNIT_PERCENT, + CONF_ID, +) + +from .. import ( + CONF_PYLONTECH_ID, + PYLONTECH_COMPONENT_SCHEMA, + CONF_BATTERY, + pylontech_ns, +) + +PylontechSensor = pylontech_ns.class_("PylontechSensor", cg.Component) + +CONF_COULOMB = "coulomb" +CONF_TEMPERATURE_LOW = "temperature_low" +CONF_TEMPERATURE_HIGH = "temperature_high" +CONF_VOLTAGE_LOW = "voltage_low" +CONF_VOLTAGE_HIGH = "voltage_high" +CONF_MOS_TEMPERATURE = "mos_temperature" + +TYPES: dict[str, cv.Schema] = { + CONF_VOLTAGE: sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=3, + device_class=DEVICE_CLASS_VOLTAGE, + ), + CONF_CURRENT: sensor.sensor_schema( + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=3, + device_class=DEVICE_CLASS_CURRENT, + ), + CONF_TEMPERATURE: sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + ), + CONF_TEMPERATURE_LOW: sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + ), + CONF_TEMPERATURE_HIGH: sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + ), + CONF_VOLTAGE_LOW: sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + ), + CONF_VOLTAGE_HIGH: sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + ), + CONF_COULOMB: sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_BATTERY, + ), + CONF_MOS_TEMPERATURE: sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + ), +} + +CONFIG_SCHEMA = PYLONTECH_COMPONENT_SCHEMA.extend( + {cv.GenerateID(): cv.declare_id(PylontechSensor)} +).extend({cv.Optional(marker): schema for marker, schema in TYPES.items()}) + + +async def to_code(config): + paren = await cg.get_variable(config[CONF_PYLONTECH_ID]) + bat = cg.new_Pvariable(config[CONF_ID], config[CONF_BATTERY]) + + for marker in TYPES: + if marker_config := config.get(marker): + sens = await sensor.new_sensor(marker_config) + cg.add(getattr(bat, f"set_{marker}_sensor")(sens)) + + cg.add(paren.register_listener(bat)) diff --git a/esphome/components/pylontech/sensor/pylontech_sensor.cpp b/esphome/components/pylontech/sensor/pylontech_sensor.cpp new file mode 100644 index 0000000000..5b5db0731e --- /dev/null +++ b/esphome/components/pylontech/sensor/pylontech_sensor.cpp @@ -0,0 +1,60 @@ +#include "pylontech_sensor.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace pylontech { + +static const char *const TAG = "pylontech.sensor"; + +PylontechSensor::PylontechSensor(int8_t bat_num) { this->bat_num_ = bat_num; } + +void PylontechSensor::dump_config() { + ESP_LOGCONFIG(TAG, "Pylontech Sensor:"); + ESP_LOGCONFIG(TAG, " Battery %d", this->bat_num_); + LOG_SENSOR(" ", "Voltage", this->voltage_sensor_); + LOG_SENSOR(" ", "Current", this->current_sensor_); + LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); + LOG_SENSOR(" ", "Temperature low", this->temperature_low_sensor_); + LOG_SENSOR(" ", "Temperature high", this->temperature_high_sensor_); + LOG_SENSOR(" ", "Voltage low", this->voltage_low_sensor_); + LOG_SENSOR(" ", "Voltage high", this->voltage_high_sensor_); + LOG_SENSOR(" ", "Coulomb", this->coulomb_sensor_); + LOG_SENSOR(" ", "MOS Temperature", this->mos_temperature_sensor_); +} + +void PylontechSensor::on_line_read(PylontechListener::LineContents *line) { + if (this->bat_num_ != line->bat_num) { + return; + } + if (this->voltage_sensor_ != nullptr) { + this->voltage_sensor_->publish_state(((float) line->volt) / 1000.0f); + } + if (this->current_sensor_ != nullptr) { + this->current_sensor_->publish_state(((float) line->curr) / 1000.0f); + } + if (this->temperature_sensor_ != nullptr) { + this->temperature_sensor_->publish_state(((float) line->tempr) / 1000.0f); + } + if (this->temperature_low_sensor_ != nullptr) { + this->temperature_low_sensor_->publish_state(((float) line->tlow) / 1000.0f); + } + if (this->temperature_high_sensor_ != nullptr) { + this->temperature_high_sensor_->publish_state(((float) line->thigh) / 1000.0f); + } + if (this->voltage_low_sensor_ != nullptr) { + this->voltage_low_sensor_->publish_state(((float) line->vlow) / 1000.0f); + } + if (this->voltage_high_sensor_ != nullptr) { + this->voltage_high_sensor_->publish_state(((float) line->vhigh) / 1000.0f); + } + if (this->coulomb_sensor_ != nullptr) { + this->coulomb_sensor_->publish_state(line->coulomb); + } + if (this->mos_temperature_sensor_ != nullptr) { + this->mos_temperature_sensor_->publish_state(((float) line->mostempr) / 1000.0f); + } +} + +} // namespace pylontech +} // namespace esphome diff --git a/esphome/components/pylontech/sensor/pylontech_sensor.h b/esphome/components/pylontech/sensor/pylontech_sensor.h new file mode 100644 index 0000000000..8986adc26c --- /dev/null +++ b/esphome/components/pylontech/sensor/pylontech_sensor.h @@ -0,0 +1,32 @@ +#pragma once + +#include "../pylontech.h" +#include "esphome/components/sensor/sensor.h" + +namespace esphome { +namespace pylontech { + +class PylontechSensor : public PylontechListener, public Component { + public: + PylontechSensor(int8_t bat_num); + void dump_config() override; + + SUB_SENSOR(voltage) + SUB_SENSOR(current) + SUB_SENSOR(temperature) + SUB_SENSOR(temperature_low) + SUB_SENSOR(temperature_high) + SUB_SENSOR(voltage_low) + SUB_SENSOR(voltage_high) + + SUB_SENSOR(coulomb) + SUB_SENSOR(mos_temperature) + + void on_line_read(LineContents *line) override; + + protected: + int8_t bat_num_; +}; + +} // namespace pylontech +} // namespace esphome diff --git a/esphome/components/pylontech/text_sensor/__init__.py b/esphome/components/pylontech/text_sensor/__init__.py new file mode 100644 index 0000000000..d6ccc678f8 --- /dev/null +++ b/esphome/components/pylontech/text_sensor/__init__.py @@ -0,0 +1,41 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import text_sensor +from esphome.const import CONF_ID + +from .. import ( + CONF_PYLONTECH_ID, + PYLONTECH_COMPONENT_SCHEMA, + CONF_BATTERY, + pylontech_ns, +) + +PylontechTextSensor = pylontech_ns.class_("PylontechTextSensor", cg.Component) + +CONF_BASE_STATE = "base_state" +CONF_VOLTAGE_STATE = "voltage_state" +CONF_CURRENT_STATE = "current_state" +CONF_TEMPERATURE_STATE = "temperature_state" + +MARKERS: list[str] = [ + CONF_BASE_STATE, + CONF_VOLTAGE_STATE, + CONF_CURRENT_STATE, + CONF_TEMPERATURE_STATE, +] + +CONFIG_SCHEMA = PYLONTECH_COMPONENT_SCHEMA.extend( + {cv.GenerateID(): cv.declare_id(PylontechTextSensor)} +).extend({cv.Optional(marker): text_sensor.text_sensor_schema() for marker in MARKERS}) + + +async def to_code(config): + paren = await cg.get_variable(config[CONF_PYLONTECH_ID]) + bat = cg.new_Pvariable(config[CONF_ID], config[CONF_BATTERY]) + + for marker in MARKERS: + if marker_config := config.get(marker): + var = await text_sensor.new_text_sensor(marker_config) + cg.add(getattr(bat, f"set_{marker}_text_sensor")(var)) + + cg.add(paren.register_listener(bat)) diff --git a/esphome/components/pylontech/text_sensor/pylontech_text_sensor.cpp b/esphome/components/pylontech/text_sensor/pylontech_text_sensor.cpp new file mode 100644 index 0000000000..9e894bc570 --- /dev/null +++ b/esphome/components/pylontech/text_sensor/pylontech_text_sensor.cpp @@ -0,0 +1,40 @@ +#include "pylontech_text_sensor.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace pylontech { + +static const char *const TAG = "pylontech.textsensor"; + +PylontechTextSensor::PylontechTextSensor(int8_t bat_num) { this->bat_num_ = bat_num; } + +void PylontechTextSensor::dump_config() { + ESP_LOGCONFIG(TAG, "Pylontech Text Sensor:"); + ESP_LOGCONFIG(TAG, " Battery %d", this->bat_num_); + LOG_TEXT_SENSOR(" ", "Base state", this->base_state_text_sensor_); + LOG_TEXT_SENSOR(" ", "Voltage state", this->voltage_state_text_sensor_); + LOG_TEXT_SENSOR(" ", "Current state", this->current_state_text_sensor_); + LOG_TEXT_SENSOR(" ", "Temperature state", this->temperature_state_text_sensor_); +} + +void PylontechTextSensor::on_line_read(PylontechListener::LineContents *line) { + if (this->bat_num_ != line->bat_num) { + return; + } + if (this->base_state_text_sensor_ != nullptr) { + this->base_state_text_sensor_->publish_state(std::string(line->base_st)); + } + if (this->voltage_state_text_sensor_ != nullptr) { + this->voltage_state_text_sensor_->publish_state(std::string(line->volt_st)); + } + if (this->current_state_text_sensor_ != nullptr) { + this->current_state_text_sensor_->publish_state(std::string(line->curr_st)); + } + if (this->temperature_state_text_sensor_ != nullptr) { + this->temperature_state_text_sensor_->publish_state(std::string(line->temp_st)); + } +} + +} // namespace pylontech +} // namespace esphome diff --git a/esphome/components/pylontech/text_sensor/pylontech_text_sensor.h b/esphome/components/pylontech/text_sensor/pylontech_text_sensor.h new file mode 100644 index 0000000000..a685512ed5 --- /dev/null +++ b/esphome/components/pylontech/text_sensor/pylontech_text_sensor.h @@ -0,0 +1,26 @@ +#pragma once + +#include "../pylontech.h" +#include "esphome/components/text_sensor/text_sensor.h" + +namespace esphome { +namespace pylontech { + +class PylontechTextSensor : public PylontechListener, public Component { + public: + PylontechTextSensor(int8_t bat_num); + void dump_config() override; + + SUB_TEXT_SENSOR(base_state) + SUB_TEXT_SENSOR(voltage_state) + SUB_TEXT_SENSOR(current_state) + SUB_TEXT_SENSOR(temperature_state) + + void on_line_read(LineContents *line) override; + + protected: + int8_t bat_num_; +}; + +} // namespace pylontech +} // namespace esphome diff --git a/tests/test4.yaml b/tests/test4.yaml index 63bf5bf0ae..69d9211969 100644 --- a/tests/test4.yaml +++ b/tests/test4.yaml @@ -99,6 +99,12 @@ pipsolar: id: inverter0 uart_id: uart115200 +pylontech: + - id: pylontech0 + uart_id: uart115200 + - id: pylontech1 + uart_id: uart115200 + sx1509: - id: sx1509_hub address: 0x3E @@ -113,6 +119,30 @@ dac7678: internal_reference: true sensor: + - platform: pylontech + pylontech_id: pylontech0 + battery: 1 + voltage: + id: pyl01_voltage + current: + id: pyl01_current + coulomb: + id: pyl01_soc + mos_temperature: + id: pyl01_mos_temperature + - platform: pylontech + pylontech_id: pylontech1 + battery: 1 + voltage: + id: pyl13_voltage + temperature_low: + id: pyl13_temperature_low + temperature_high: + id: pyl13_temperature_high + voltage_low: + id: pyl13_voltage_low + voltage_high: + id: pyl13_voltage_high - platform: homeassistant entity_id: sensor.hello_world id: ha_hello_world @@ -589,6 +619,17 @@ number: name: Tuya Number Copy text_sensor: + - platform: pylontech + pylontech_id: pylontech0 + battery: 1 + base_state: + id: pyl0_base_state + voltage_state: + id: pyl0_voltage_state + current_state: + id: pyl0_current_state + temperature_state: + id: pyl0_temperature_state - platform: pipsolar pipsolar_id: inverter0 device_mode: From b8ee0dedec521b3edf87ca8bef2dbc1e59dea69a Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 28 Nov 2023 12:28:24 +1300 Subject: [PATCH 13/89] Fix write_speaker without speaker in config (#5847) --- esphome/components/voice_assistant/voice_assistant.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp index c0e706305d..29fc664342 100644 --- a/esphome/components/voice_assistant/voice_assistant.cpp +++ b/esphome/components/voice_assistant/voice_assistant.cpp @@ -344,6 +344,7 @@ void VoiceAssistant::loop() { } } +#ifdef USE_SPEAKER void VoiceAssistant::write_speaker_() { if (this->speaker_buffer_size_ > 0) { size_t written = this->speaker_->play(this->speaker_buffer_, this->speaker_buffer_size_); @@ -357,6 +358,7 @@ void VoiceAssistant::write_speaker_() { } } } +#endif void VoiceAssistant::client_subscription(api::APIConnection *client, bool subscribe) { if (!subscribe) { From a66dec738dba1bc1c4712ac561e731f7c5fedf00 Mon Sep 17 00:00:00 2001 From: Lucas Prim Date: Mon, 27 Nov 2023 20:40:39 -0300 Subject: [PATCH 14/89] Implement deep sleep and clear screen on Waveshare 7.5in B V3 (#5239) --- .../waveshare_epaper/waveshare_epaper.cpp | 20 ++++++++++--------- .../waveshare_epaper/waveshare_epaper.h | 4 ++++ 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.cpp b/esphome/components/waveshare_epaper/waveshare_epaper.cpp index f52808d295..53bfa57f4f 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.cpp +++ b/esphome/components/waveshare_epaper/waveshare_epaper.cpp @@ -1329,6 +1329,7 @@ void WaveshareEPaper7P5InBV2::dump_config() { LOG_UPDATE_INTERVAL(this); } +void WaveshareEPaper7P5InBV3::initialize() { this->init_display_(); } bool WaveshareEPaper7P5InBV3::wait_until_idle_() { if (this->busy_pin_ == nullptr) { return true; @@ -1341,12 +1342,13 @@ bool WaveshareEPaper7P5InBV3::wait_until_idle_() { ESP_LOGI(TAG, "Timeout while displaying image!"); return false; } + App.feed_wdt(); delay(10); } delay(200); // NOLINT return true; }; -void WaveshareEPaper7P5InBV3::initialize() { +void WaveshareEPaper7P5InBV3::init_display_() { this->reset_(); // COMMAND POWER SETTING @@ -1402,8 +1404,6 @@ void WaveshareEPaper7P5InBV3::initialize() { this->data(0x00); this->data(0x00); - this->wait_until_idle_(); - uint8_t lut_vcom_7_i_n5_v2[] = { 0x0, 0xF, 0xF, 0x0, 0x0, 0x1, 0x0, 0xF, 0x1, 0xF, 0x1, 0x2, 0x0, 0xF, 0xF, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, @@ -1449,24 +1449,26 @@ void WaveshareEPaper7P5InBV3::initialize() { this->command(0x24); // LUTBB for (count = 0; count < 42; count++) this->data(lut_bb_7_i_n5_v2[count]); - - this->command(0x10); - for (uint32_t i = 0; i < 800 * 480 / 8; i++) { - this->data(0xFF); - } }; void HOT WaveshareEPaper7P5InBV3::display() { + this->init_display_(); uint32_t buf_len = this->get_buffer_length_(); + this->command(0x10); + for (uint32_t i = 0; i < buf_len; i++) { + this->data(0xFF); + } + this->command(0x13); // Start Transmission delay(2); for (uint32_t i = 0; i < buf_len; i++) { - this->data(~(this->buffer_[i])); + this->data(this->buffer_[i]); } this->command(0x12); // Display Refresh delay(100); // NOLINT this->wait_until_idle_(); + this->deep_sleep(); } int WaveshareEPaper7P5InBV3::get_width_internal() { return 800; } int WaveshareEPaper7P5InBV3::get_height_internal() { return 480; } diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.h b/esphome/components/waveshare_epaper/waveshare_epaper.h index c800d29643..f6ccf90861 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.h +++ b/esphome/components/waveshare_epaper/waveshare_epaper.h @@ -429,6 +429,8 @@ class WaveshareEPaper7P5InBV3 : public WaveshareEPaper { this->data(0xA5); } + void clear_screen(); + protected: int get_width_internal() override; @@ -444,6 +446,8 @@ class WaveshareEPaper7P5InBV3 : public WaveshareEPaper { delay(200); // NOLINT } }; + + void init_display_(); }; class WaveshareEPaper7P5InBC : public WaveshareEPaper { From 496c29aa044bde02ce9081abafbe40a6db3a9798 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Tue, 28 Nov 2023 10:42:22 +1100 Subject: [PATCH 15/89] Fix ESP-IDF uart initialisation sequence to match Espressif docs. (#5838) --- .../uart/uart_component_esp_idf.cpp | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/esphome/components/uart/uart_component_esp_idf.cpp b/esphome/components/uart/uart_component_esp_idf.cpp index 9b519c4568..c78626fa26 100644 --- a/esphome/components/uart/uart_component_esp_idf.cpp +++ b/esphome/components/uart/uart_component_esp_idf.cpp @@ -84,28 +84,9 @@ void IDFUARTComponent::setup() { return; } - err = uart_driver_install(this->uart_num_, /* UART RX ring buffer size. */ this->rx_buffer_size_, - /* UART TX ring buffer size. If set to zero, driver will not use TX buffer, TX function will - block task until all data have been sent out.*/ - 0, - /* UART event queue size/depth. */ 20, &(this->uart_event_queue_), - /* Flags used to allocate the interrupt. */ 0); - if (err != ESP_OK) { - ESP_LOGW(TAG, "uart_driver_install failed: %s", esp_err_to_name(err)); - this->mark_failed(); - return; - } - int8_t tx = this->tx_pin_ != nullptr ? this->tx_pin_->get_pin() : -1; int8_t rx = this->rx_pin_ != nullptr ? this->rx_pin_->get_pin() : -1; - err = uart_set_pin(this->uart_num_, tx, rx, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE); - if (err != ESP_OK) { - ESP_LOGW(TAG, "uart_set_pin failed: %s", esp_err_to_name(err)); - this->mark_failed(); - return; - } - uint32_t invert = 0; if (this->tx_pin_ != nullptr && this->tx_pin_->is_inverted()) invert |= UART_SIGNAL_TXD_INV; @@ -119,6 +100,25 @@ void IDFUARTComponent::setup() { return; } + err = uart_set_pin(this->uart_num_, tx, rx, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE); + if (err != ESP_OK) { + ESP_LOGW(TAG, "uart_set_pin failed: %s", esp_err_to_name(err)); + this->mark_failed(); + return; + } + + err = uart_driver_install(this->uart_num_, /* UART RX ring buffer size. */ this->rx_buffer_size_, + /* UART TX ring buffer size. If set to zero, driver will not use TX buffer, TX function will + block task until all data have been sent out.*/ + 0, + /* UART event queue size/depth. */ 20, &(this->uart_event_queue_), + /* Flags used to allocate the interrupt. */ 0); + if (err != ESP_OK) { + ESP_LOGW(TAG, "uart_driver_install failed: %s", esp_err_to_name(err)); + this->mark_failed(); + return; + } + xSemaphoreGive(this->lock_); } From ab1cc0ed6e26bb332637c2f0ce6d773ece2b37d3 Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Tue, 28 Nov 2023 01:24:43 +0100 Subject: [PATCH 16/89] Nextion - Align strings on `dump_config` (#5824) --- esphome/components/nextion/nextion.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index 81e905b979..02ba94fcab 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -129,15 +129,15 @@ void Nextion::dump_config() { ESP_LOGCONFIG(TAG, " Wake On Touch: %s", this->auto_wake_on_touch_ ? "True" : "False"); if (this->touch_sleep_timeout_ != 0) { - ESP_LOGCONFIG(TAG, " Touch Timeout: %" PRIu32, this->touch_sleep_timeout_); + ESP_LOGCONFIG(TAG, " Touch Timeout: %" PRIu32, this->touch_sleep_timeout_); } if (this->wake_up_page_ != -1) { - ESP_LOGCONFIG(TAG, " Wake Up Page : %d", this->wake_up_page_); + ESP_LOGCONFIG(TAG, " Wake Up Page: %d", this->wake_up_page_); } if (this->start_up_page_ != -1) { - ESP_LOGCONFIG(TAG, " Start Up Page : %d", this->start_up_page_); + ESP_LOGCONFIG(TAG, " Start Up Page: %d", this->start_up_page_); } } From 993cd55b1df3f8c8cb6035e39612dfa6c7781c54 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Tue, 28 Nov 2023 11:42:03 +1100 Subject: [PATCH 17/89] Speed up (and fix) ili9xxx display component. (#5406) --- esphome/components/ili9xxx/display.py | 107 ++++-- .../components/ili9xxx/ili9xxx_display.cpp | 344 +++++++----------- esphome/components/ili9xxx/ili9xxx_display.h | 106 ++++-- esphome/components/ili9xxx/ili9xxx_init.h | 27 ++ tests/test1.yaml | 13 + 5 files changed, 317 insertions(+), 280 deletions(-) diff --git a/esphome/components/ili9xxx/display.py b/esphome/components/ili9xxx/display.py index 6882d254e1..f321b2ed63 100644 --- a/esphome/components/ili9xxx/display.py +++ b/esphome/components/ili9xxx/display.py @@ -2,6 +2,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import core, pins from esphome.components import display, spi, font +from esphome.components.display import validate_rotation from esphome.core import CORE, HexInt from esphome.const import ( CONF_COLOR_PALETTE, @@ -13,6 +14,9 @@ from esphome.const import ( CONF_PAGES, CONF_RESET_PIN, CONF_DIMENSIONS, + CONF_WIDTH, + CONF_HEIGHT, + CONF_ROTATION, ) DEPENDENCIES = ["spi"] @@ -26,28 +30,35 @@ def AUTO_LOAD(): CODEOWNERS = ["@nielsnl68", "@clydebarrow"] -ili9XXX_ns = cg.esphome_ns.namespace("ili9xxx") -ili9XXXSPI = ili9XXX_ns.class_( +ili9xxx_ns = cg.esphome_ns.namespace("ili9xxx") +ILI9XXXDisplay = ili9xxx_ns.class_( "ILI9XXXDisplay", cg.PollingComponent, spi.SPIDevice, display.DisplayBuffer ) -ILI9XXXColorMode = ili9XXX_ns.enum("ILI9XXXColorMode") +ILI9XXXColorMode = ili9xxx_ns.enum("ILI9XXXColorMode") +ColorOrder = display.display_ns.enum("ColorMode") MODELS = { - "M5STACK": ili9XXX_ns.class_("ILI9XXXM5Stack", ili9XXXSPI), - "M5CORE": ili9XXX_ns.class_("ILI9XXXM5CORE", ili9XXXSPI), - "TFT_2.4": ili9XXX_ns.class_("ILI9XXXILI9341", ili9XXXSPI), - "TFT_2.4R": ili9XXX_ns.class_("ILI9XXXILI9342", ili9XXXSPI), - "ILI9341": ili9XXX_ns.class_("ILI9XXXILI9341", ili9XXXSPI), - "ILI9342": ili9XXX_ns.class_("ILI9XXXILI9342", ili9XXXSPI), - "ILI9481": ili9XXX_ns.class_("ILI9XXXILI9481", ili9XXXSPI), - "ILI9481-18": ili9XXX_ns.class_("ILI9XXXILI948118", ili9XXXSPI), - "ILI9486": ili9XXX_ns.class_("ILI9XXXILI9486", ili9XXXSPI), - "ILI9488": ili9XXX_ns.class_("ILI9XXXILI9488", ili9XXXSPI), - "ILI9488_A": ili9XXX_ns.class_("ILI9XXXILI9488A", ili9XXXSPI), - "ST7796": ili9XXX_ns.class_("ILI9XXXST7796", ili9XXXSPI), - "S3BOX": ili9XXX_ns.class_("ILI9XXXS3Box", ili9XXXSPI), - "S3BOX_LITE": ili9XXX_ns.class_("ILI9XXXS3BoxLite", ili9XXXSPI), + "M5STACK": ili9xxx_ns.class_("ILI9XXXM5Stack", ILI9XXXDisplay), + "M5CORE": ili9xxx_ns.class_("ILI9XXXM5CORE", ILI9XXXDisplay), + "TFT_2.4": ili9xxx_ns.class_("ILI9XXXILI9341", ILI9XXXDisplay), + "TFT_2.4R": ili9xxx_ns.class_("ILI9XXXILI9342", ILI9XXXDisplay), + "ILI9341": ili9xxx_ns.class_("ILI9XXXILI9341", ILI9XXXDisplay), + "ILI9342": ili9xxx_ns.class_("ILI9XXXILI9342", ILI9XXXDisplay), + "ILI9481": ili9xxx_ns.class_("ILI9XXXILI9481", ILI9XXXDisplay), + "ILI9481-18": ili9xxx_ns.class_("ILI9XXXILI948118", ILI9XXXDisplay), + "ILI9486": ili9xxx_ns.class_("ILI9XXXILI9486", ILI9XXXDisplay), + "ILI9488": ili9xxx_ns.class_("ILI9XXXILI9488", ILI9XXXDisplay), + "ILI9488_A": ili9xxx_ns.class_("ILI9XXXILI9488A", ILI9XXXDisplay), + "ST7796": ili9xxx_ns.class_("ILI9XXXST7796", ILI9XXXDisplay), + "ST7789V": ili9xxx_ns.class_("ILI9XXXST7789V", ILI9XXXDisplay), + "S3BOX": ili9xxx_ns.class_("ILI9XXXS3Box", ILI9XXXDisplay), + "S3BOX_LITE": ili9xxx_ns.class_("ILI9XXXS3BoxLite", ILI9XXXDisplay), +} + +COLOR_ORDERS = { + "RGB": ColorOrder.COLOR_ORDER_RGB, + "BGR": ColorOrder.COLOR_ORDER_BGR, } COLOR_PALETTE = cv.one_of("NONE", "GRAYSCALE", "IMAGE_ADAPTIVE") @@ -55,6 +66,14 @@ COLOR_PALETTE = cv.one_of("NONE", "GRAYSCALE", "IMAGE_ADAPTIVE") CONF_LED_PIN = "led_pin" CONF_COLOR_PALETTE_IMAGES = "color_palette_images" CONF_INVERT_DISPLAY = "invert_display" +CONF_INVERT_COLORS = "invert_colors" +CONF_MIRROR_X = "mirror_x" +CONF_MIRROR_Y = "mirror_y" +CONF_SWAP_XY = "swap_xy" +CONF_COLOR_ORDER = "color_order" +CONF_OFFSET_HEIGHT = "offset_height" +CONF_OFFSET_WIDTH = "offset_width" +CONF_TRANSFORM = "transform" def _validate(config): @@ -77,6 +96,7 @@ def _validate(config): "TFT_2.4R", "ILI9341", "ILI9342", + "ST7789V", ]: raise cv.Invalid( "Provided model can't run on ESP8266. Use an ESP32 with PSRAM onboard" @@ -88,9 +108,19 @@ CONFIG_SCHEMA = cv.All( font.validate_pillow_installed, display.FULL_DISPLAY_SCHEMA.extend( { - cv.GenerateID(): cv.declare_id(ili9XXXSPI), + cv.GenerateID(): cv.declare_id(ILI9XXXDisplay), cv.Required(CONF_MODEL): cv.enum(MODELS, upper=True, space="_"), - cv.Optional(CONF_DIMENSIONS): cv.dimensions, + cv.Optional(CONF_DIMENSIONS): cv.Any( + cv.dimensions, + cv.Schema( + { + cv.Required(CONF_WIDTH): cv.int_, + cv.Required(CONF_HEIGHT): cv.int_, + cv.Optional(CONF_OFFSET_HEIGHT, default=0): cv.int_, + cv.Optional(CONF_OFFSET_WIDTH, default=0): cv.int_, + } + ), + ), cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema, cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, cv.Optional(CONF_LED_PIN): cv.invalid( @@ -101,7 +131,19 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_COLOR_PALETTE_IMAGES, default=[]): cv.ensure_list( cv.file_ ), - cv.Optional(CONF_INVERT_DISPLAY): cv.boolean, + cv.Optional(CONF_INVERT_DISPLAY): cv.invalid( + "'invert_display' has been replaced by 'invert_colors'" + ), + cv.Optional(CONF_INVERT_COLORS): cv.boolean, + cv.Optional(CONF_COLOR_ORDER): cv.one_of(*COLOR_ORDERS.keys(), upper=True), + cv.Exclusive(CONF_ROTATION, CONF_ROTATION): validate_rotation, + cv.Exclusive(CONF_TRANSFORM, CONF_ROTATION): cv.Schema( + { + cv.Optional(CONF_SWAP_XY, default=False): cv.boolean, + cv.Optional(CONF_MIRROR_X, default=False): cv.boolean, + cv.Optional(CONF_MIRROR_Y, default=False): cv.boolean, + } + ), } ) .extend(cv.polling_component_schema("1s")) @@ -119,6 +161,13 @@ async def to_code(config): await spi.register_spi_device(var, config) dc = await cg.gpio_pin_expression(config[CONF_DC_PIN]) cg.add(var.set_dc_pin(dc)) + if CONF_COLOR_ORDER in config: + cg.add(var.set_color_order(COLOR_ORDERS[config[CONF_COLOR_ORDER]])) + if CONF_TRANSFORM in config: + transform = config[CONF_TRANSFORM] + cg.add(var.set_swap_xy(transform[CONF_SWAP_XY])) + cg.add(var.set_mirror_x(transform[CONF_MIRROR_X])) + cg.add(var.set_mirror_y(transform[CONF_MIRROR_Y])) if CONF_LAMBDA in config: lambda_ = await cg.process_lambda( @@ -131,9 +180,17 @@ async def to_code(config): cg.add(var.set_reset_pin(reset)) if CONF_DIMENSIONS in config: - cg.add( - var.set_dimentions(config[CONF_DIMENSIONS][0], config[CONF_DIMENSIONS][1]) - ) + dimensions = config[CONF_DIMENSIONS] + if isinstance(dimensions, dict): + cg.add(var.set_dimensions(dimensions[CONF_WIDTH], dimensions[CONF_HEIGHT])) + cg.add( + var.set_offsets( + dimensions[CONF_OFFSET_WIDTH], dimensions[CONF_OFFSET_HEIGHT] + ) + ) + else: + (width, height) = dimensions + cg.add(var.set_dimensions(width, height)) rhs = None if config[CONF_COLOR_PALETTE] == "GRAYSCALE": @@ -178,5 +235,5 @@ async def to_code(config): prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) cg.add(var.set_palette(prog_arr)) - if CONF_INVERT_DISPLAY in config: - cg.add(var.invert_display(config[CONF_INVERT_DISPLAY])) + if CONF_INVERT_COLORS in config: + cg.add(var.invert_colors(config[CONF_INVERT_COLORS])) diff --git a/esphome/components/ili9xxx/ili9xxx_display.cpp b/esphome/components/ili9xxx/ili9xxx_display.cpp index 902a9e6245..b315c8be87 100644 --- a/esphome/components/ili9xxx/ili9xxx_display.cpp +++ b/esphome/components/ili9xxx/ili9xxx_display.cpp @@ -8,11 +8,31 @@ namespace esphome { namespace ili9xxx { static const char *const TAG = "ili9xxx"; +static const uint16_t SPI_SETUP_US = 100; // estimated fixed overhead in microseconds for an SPI write +static const uint16_t SPI_MAX_BLOCK_SIZE = 4092; // Max size of continuous SPI transfer + +// store a 16 bit value in a buffer, big endian. +static inline void put16_be(uint8_t *buf, uint16_t value) { + buf[0] = value >> 8; + buf[1] = value; +} void ILI9XXXDisplay::setup() { + ESP_LOGD(TAG, "Setting up ILI9xxx"); + this->setup_pins_(); - this->initialize(); - this->command(this->pre_invertdisplay_ ? ILI9XXX_INVON : ILI9XXX_INVOFF); + this->init_lcd_(); + + this->command(this->pre_invertcolors_ ? ILI9XXX_INVON : ILI9XXX_INVOFF); + // custom x/y transform and color order + uint8_t mad = this->color_order_ == display::COLOR_ORDER_BGR ? MADCTL_BGR : MADCTL_RGB; + if (this->swap_xy_) + mad |= MADCTL_MV; + if (this->mirror_x_) + mad |= MADCTL_MX; + if (this->mirror_y_) + mad |= MADCTL_MY; + this->send_command(ILI9XXX_MADCTL, &mad, 1); this->x_low_ = this->width_; this->y_low_ = this->height_; @@ -47,6 +67,8 @@ void ILI9XXXDisplay::setup_pins_() { void ILI9XXXDisplay::dump_config() { LOG_DISPLAY("", "ili9xxx", this); + ESP_LOGCONFIG(TAG, " Width Offset: %u", this->offset_x_); + ESP_LOGCONFIG(TAG, " Height Offset: %u", this->offset_y_); switch (this->buffer_color_mode_) { case BITS_8_INDEXED: ESP_LOGCONFIG(TAG, " Color mode: 8bit Indexed"); @@ -64,8 +86,12 @@ void ILI9XXXDisplay::dump_config() { ESP_LOGCONFIG(TAG, " Data rate: %dMHz", (unsigned) (this->data_rate_ / 1000000)); LOG_PIN(" Reset Pin: ", this->reset_pin_); + LOG_PIN(" CS Pin: ", this->cs_); LOG_PIN(" DC Pin: ", this->dc_pin_); LOG_PIN(" Busy Pin: ", this->busy_pin_); + ESP_LOGCONFIG(TAG, " Swap_xy: %s", YESNO(this->swap_xy_)); + ESP_LOGCONFIG(TAG, " Mirror_x: %s", YESNO(this->mirror_x_)); + ESP_LOGCONFIG(TAG, " Mirror_y: %s", YESNO(this->mirror_y_)); if (this->is_failed()) { ESP_LOGCONFIG(TAG, " => Failed to init Memory: YES!"); @@ -141,12 +167,14 @@ void HOT ILI9XXXDisplay::draw_absolute_pixel_internal(int x, int y, Color color) } if (updated) { // low and high watermark may speed up drawing from buffer - this->x_low_ = (x < this->x_low_) ? x : this->x_low_; - this->y_low_ = (y < this->y_low_) ? y : this->y_low_; - this->x_high_ = (x > this->x_high_) ? x : this->x_high_; - this->y_high_ = (y > this->y_high_) ? y : this->y_high_; - // ESP_LOGVV(TAG, "=>>> pixel (x:%d, y:%d) (xl:%d, xh:%d, yl:%d, yh:%d", x, y, this->x_low_, this->x_high_, - // this->y_low_, this->y_high_); + if (x < this->x_low_) + this->x_low_ = x; + if (y < this->y_low_) + this->y_low_ = y; + if (x > this->x_high_) + this->x_high_ = x; + if (y > this->y_high_) + this->y_high_ = y; } } @@ -165,59 +193,82 @@ void ILI9XXXDisplay::update() { } void ILI9XXXDisplay::display_() { - // we will only update the changed window to the display - uint16_t w = this->x_high_ - this->x_low_ + 1; // NOLINT - uint16_t h = this->y_high_ - this->y_low_ + 1; // NOLINT - uint32_t start_pos = ((this->y_low_ * this->width_) + x_low_); - + uint8_t transfer_buffer[ILI9XXX_TRANSFER_BUFFER_SIZE]; // check if something was displayed if ((this->x_high_ < this->x_low_) || (this->y_high_ < this->y_low_)) { ESP_LOGV(TAG, "Nothing to display"); return; } - set_addr_window_(this->x_low_, this->y_low_, w, h); + // we will only update the changed rows to the display + size_t const w = this->x_high_ - this->x_low_ + 1; + size_t const h = this->y_high_ - this->y_low_ + 1; + size_t mhz = this->data_rate_ / 1000000; + // estimate time for a single write + size_t sw_time = this->width_ * h * 16 / mhz + this->width_ * h * 2 / SPI_MAX_BLOCK_SIZE * SPI_SETUP_US * 2; + // estimate time for multiple writes + size_t mw_time = (w * h * 16) / mhz + w * h * 2 / ILI9XXX_TRANSFER_BUFFER_SIZE * SPI_SETUP_US; ESP_LOGV(TAG, "Start display(xlow:%d, ylow:%d, xhigh:%d, yhigh:%d, width:%d, " - "heigth:%d, start_pos:%" PRId32 ")", - this->x_low_, this->y_low_, this->x_high_, this->y_high_, w, h, start_pos); - - this->start_data_(); - for (uint16_t row = 0; row < h; row++) { - uint32_t pos = start_pos + (row * width_); - uint32_t rem = w; - - while (rem > 0) { - uint32_t sz = std::min(rem, ILI9XXX_TRANSFER_BUFFER_SIZE); - // ESP_LOGVV(TAG, "Send to display(pos:%d, rem:%d, zs:%d)", pos, rem, sz); - buffer_to_transfer_(pos, sz); - if (this->is_18bitdisplay_) { - for (uint32_t i = 0; i < sz; ++i) { - uint16_t color_val = transfer_buffer_[i]; - - uint8_t red = color_val & 0x1F; - uint8_t green = (color_val & 0x7E0) >> 5; - uint8_t blue = (color_val & 0xF800) >> 11; - - uint8_t pass_buff[3]; - - pass_buff[2] = (uint8_t) ((red / 32.0) * 64) << 2; - pass_buff[1] = (uint8_t) green << 2; - pass_buff[0] = (uint8_t) ((blue / 32.0) * 64) << 2; - - this->write_array(pass_buff, sizeof(pass_buff)); - } - } else { - this->write_array16(transfer_buffer_, sz); + "height:%d, mode=%d, 18bit=%d, sw_time=%dus, mw_time=%dus)", + this->x_low_, this->y_low_, this->x_high_, this->y_high_, w, h, this->buffer_color_mode_, + this->is_18bitdisplay_, sw_time, mw_time); + auto now = millis(); + this->enable(); + if (this->buffer_color_mode_ == BITS_16 && !this->is_18bitdisplay_ && sw_time < mw_time) { + // 16 bit mode maps directly to display format + ESP_LOGV(TAG, "Doing single write of %d bytes", this->width_ * h * 2); + set_addr_window_(0, this->y_low_, this->width_ - 1, this->y_high_); + this->write_array(this->buffer_ + this->y_low_ * this->width_ * 2, h * this->width_ * 2); + } else { + ESP_LOGV(TAG, "Doing multiple write"); + size_t rem = h * w; // remaining number of pixels to write + set_addr_window_(this->x_low_, this->y_low_, this->x_high_, this->y_high_); + size_t idx = 0; // index into transfer_buffer + size_t pixel = 0; // pixel number offset + size_t pos = this->y_low_ * this->width_ + this->x_low_; + while (rem-- != 0) { + uint16_t color_val; + switch (this->buffer_color_mode_) { + case BITS_8: + color_val = display::ColorUtil::color_to_565(display::ColorUtil::rgb332_to_color(this->buffer_[pos++])); + break; + case BITS_8_INDEXED: + color_val = display::ColorUtil::color_to_565( + display::ColorUtil::index8_to_color_palette888(this->buffer_[pos++], this->palette_)); + break; + default: // case BITS_16: + color_val = (buffer_[pos * 2] << 8) + buffer_[pos * 2 + 1]; + pos++; + break; + } + if (this->is_18bitdisplay_) { + transfer_buffer[idx++] = (uint8_t) ((color_val & 0xF800) >> 8); // Blue + transfer_buffer[idx++] = (uint8_t) ((color_val & 0x7E0) >> 3); // Green + transfer_buffer[idx++] = (uint8_t) (color_val << 3); // Red + } else { + put16_be(transfer_buffer + idx, color_val); + idx += 2; + } + if (idx == ILI9XXX_TRANSFER_BUFFER_SIZE) { + this->write_array(transfer_buffer, idx); + idx = 0; + App.feed_wdt(); + } + // end of line? Skip to the next. + if (++pixel == w) { + pixel = 0; + pos += this->width_ - w; } - pos += sz; - rem -= sz; } - App.feed_wdt(); + // flush any balance. + if (idx != 0) { + this->write_array(transfer_buffer, idx); + } } - this->end_data_(); - + this->disable(); + ESP_LOGV(TAG, "Data write took %dms", (unsigned) (millis() - now)); // invalidate watermarks this->x_low_ = this->width_; this->y_low_ = this->height_; @@ -225,26 +276,6 @@ void ILI9XXXDisplay::display_() { this->y_high_ = 0; } -uint32_t ILI9XXXDisplay::buffer_to_transfer_(uint32_t pos, uint32_t sz) { - for (uint32_t i = 0; i < sz; ++i) { - switch (this->buffer_color_mode_) { - case BITS_8_INDEXED: - transfer_buffer_[i] = display::ColorUtil::color_to_565( - display::ColorUtil::index8_to_color_palette888(this->buffer_[pos + i], this->palette_)); - break; - case BITS_16: - transfer_buffer_[i] = ((uint16_t) this->buffer_[(pos + i) * 2] << 8) | this->buffer_[((pos + i) * 2) + 1]; - continue; - break; - default: - transfer_buffer_[i] = - display::ColorUtil::color_to_565(display::ColorUtil::rgb332_to_color(this->buffer_[pos + i])); - break; - } - } - return sz; -} - // should return the total size: return this->get_width_internal() * this->get_height_internal() * 2 // 16bit color // values per bit is huge uint32_t ILI9XXXDisplay::get_buffer_length_() { return this->get_width_internal() * this->get_height_internal(); } @@ -303,11 +334,11 @@ void ILI9XXXDisplay::reset_() { } } -void ILI9XXXDisplay::init_lcd_(const uint8_t *init_cmd) { +void ILI9XXXDisplay::init_lcd_() { uint8_t cmd, x, num_args; - const uint8_t *addr = init_cmd; - while ((cmd = progmem_read_byte(addr++)) > 0) { - x = progmem_read_byte(addr++); + const uint8_t *addr = this->init_sequence_; + while ((cmd = *addr++) > 0) { + x = *addr++; num_args = x & 0x7F; send_command(cmd, addr, num_args); addr += num_args; @@ -316,27 +347,29 @@ void ILI9XXXDisplay::init_lcd_(const uint8_t *init_cmd) { } } -void ILI9XXXDisplay::set_addr_window_(uint16_t x1, uint16_t y1, uint16_t w, uint16_t h) { - uint16_t x2 = (x1 + w - 1), y2 = (y1 + h - 1); - this->command(ILI9XXX_CASET); // Column address set - this->start_data_(); - this->write_byte(x1 >> 8); - this->write_byte(x1); - this->write_byte(x2 >> 8); - this->write_byte(x2); - this->end_data_(); - this->command(ILI9XXX_PASET); // Row address set - this->start_data_(); - this->write_byte(y1 >> 8); - this->write_byte(y1); - this->write_byte(y2 >> 8); - this->write_byte(y2); - this->end_data_(); - this->command(ILI9XXX_RAMWR); // Write to RAM +// Tell the display controller where we want to draw pixels. +// when called, the SPI should have already been enabled, only the D/C pin will be toggled here. +void ILI9XXXDisplay::set_addr_window_(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2) { + uint8_t buf[4]; + this->dc_pin_->digital_write(false); + this->write_byte(ILI9XXX_CASET); // Column address set + put16_be(buf, x1 + this->offset_x_); + put16_be(buf + 2, x2 + this->offset_x_); + this->dc_pin_->digital_write(true); + this->write_array(buf, sizeof buf); + this->dc_pin_->digital_write(false); + this->write_byte(ILI9XXX_PASET); // Row address set + put16_be(buf, y1 + this->offset_y_); + put16_be(buf + 2, y2 + this->offset_y_); + this->dc_pin_->digital_write(true); + this->write_array(buf, sizeof buf); + this->dc_pin_->digital_write(false); + this->write_byte(ILI9XXX_RAMWR); // Write to RAM + this->dc_pin_->digital_write(true); } -void ILI9XXXDisplay::invert_display(bool invert) { - this->pre_invertdisplay_ = invert; +void ILI9XXXDisplay::invert_colors(bool invert) { + this->pre_invertcolors_ = invert; if (is_ready()) { this->command(invert ? ILI9XXX_INVON : ILI9XXX_INVOFF); } @@ -345,132 +378,5 @@ void ILI9XXXDisplay::invert_display(bool invert) { int ILI9XXXDisplay::get_width_internal() { return this->width_; } int ILI9XXXDisplay::get_height_internal() { return this->height_; } -// M5Stack display -void ILI9XXXM5Stack::initialize() { - this->init_lcd_(INITCMD_M5STACK); - if (this->width_ == 0) - this->width_ = 320; - if (this->height_ == 0) - this->height_ = 240; - this->pre_invertdisplay_ = true; -} - -// M5CORE display // Based on the configuration settings of M5stact's M5GFX code. -void ILI9XXXM5CORE::initialize() { - this->init_lcd_(INITCMD_M5CORE); - if (this->width_ == 0) - this->width_ = 320; - if (this->height_ == 0) - this->height_ = 240; - this->pre_invertdisplay_ = true; -} - -// 24_TFT display -void ILI9XXXILI9341::initialize() { - this->init_lcd_(INITCMD_ILI9341); - if (this->width_ == 0) - this->width_ = 240; - if (this->height_ == 0) - this->height_ = 320; -} -// 24_TFT rotated display -void ILI9XXXILI9342::initialize() { - this->init_lcd_(INITCMD_ILI9341); - if (this->width_ == 0) { - this->width_ = 320; - } - if (this->height_ == 0) { - this->height_ = 240; - } -} - -// 35_TFT display -void ILI9XXXILI9481::initialize() { - this->init_lcd_(INITCMD_ILI9481); - if (this->width_ == 0) { - this->width_ = 480; - } - if (this->height_ == 0) { - this->height_ = 320; - } -} - -void ILI9XXXILI948118::initialize() { - this->init_lcd_(INITCMD_ILI9481_18); - if (this->width_ == 0) { - this->width_ = 320; - } - if (this->height_ == 0) { - this->height_ = 480; - } - this->is_18bitdisplay_ = true; -} - -// 35_TFT display -void ILI9XXXILI9486::initialize() { - this->init_lcd_(INITCMD_ILI9486); - if (this->width_ == 0) { - this->width_ = 480; - } - if (this->height_ == 0) { - this->height_ = 320; - } -} -// 40_TFT display -void ILI9XXXILI9488::initialize() { - this->init_lcd_(INITCMD_ILI9488); - if (this->width_ == 0) { - this->width_ = 480; - } - if (this->height_ == 0) { - this->height_ = 320; - } - this->is_18bitdisplay_ = true; -} -// 40_TFT display -void ILI9XXXILI9488A::initialize() { - this->init_lcd_(INITCMD_ILI9488_A); - if (this->width_ == 0) { - this->width_ = 480; - } - if (this->height_ == 0) { - this->height_ = 320; - } - this->is_18bitdisplay_ = true; -} -// 40_TFT display -void ILI9XXXST7796::initialize() { - this->init_lcd_(INITCMD_ST7796); - if (this->width_ == 0) { - this->width_ = 320; - } - if (this->height_ == 0) { - this->height_ = 480; - } -} - -// 24_TFT rotated display -void ILI9XXXS3Box::initialize() { - this->init_lcd_(INITCMD_S3BOX); - if (this->width_ == 0) { - this->width_ = 320; - } - if (this->height_ == 0) { - this->height_ = 240; - } -} - -// 24_TFT rotated display -void ILI9XXXS3BoxLite::initialize() { - this->init_lcd_(INITCMD_S3BOXLITE); - if (this->width_ == 0) { - this->width_ = 320; - } - if (this->height_ == 0) { - this->height_ = 240; - } - this->pre_invertdisplay_ = true; -} - } // namespace ili9xxx } // namespace esphome diff --git a/esphome/components/ili9xxx/ili9xxx_display.h b/esphome/components/ili9xxx/ili9xxx_display.h index ae7c83e61f..db1274450a 100644 --- a/esphome/components/ili9xxx/ili9xxx_display.h +++ b/esphome/components/ili9xxx/ili9xxx_display.h @@ -7,7 +7,7 @@ namespace esphome { namespace ili9xxx { -const uint32_t ILI9XXX_TRANSFER_BUFFER_SIZE = 64; +const size_t ILI9XXX_TRANSFER_BUFFER_SIZE = 126; // ensure this is divisible by 6 enum ILI9XXXColorMode { BITS_8 = 0x08, @@ -23,20 +23,47 @@ class ILI9XXXDisplay : public display::DisplayBuffer, public spi::SPIDevice { public: + ILI9XXXDisplay() = default; + ILI9XXXDisplay(uint8_t const *init_sequence, int16_t width, int16_t height, bool invert_colors) + : init_sequence_{init_sequence}, width_{width}, height_{height}, pre_invertcolors_{invert_colors} { + uint8_t cmd, num_args, bits; + const uint8_t *addr = init_sequence; + while ((cmd = *addr++) != 0) { + num_args = *addr++ & 0x7F; + if (cmd == ILI9XXX_MADCTL) { + bits = *addr; + this->swap_xy_ = (bits & MADCTL_MV) != 0; + this->mirror_x_ = (bits & MADCTL_MX) != 0; + this->mirror_y_ = (bits & MADCTL_MY) != 0; + this->color_order_ = (bits & MADCTL_BGR) ? display::COLOR_ORDER_BGR : display::COLOR_ORDER_RGB; + break; + } + addr += num_args; + } + } + void set_dc_pin(GPIOPin *dc_pin) { dc_pin_ = dc_pin; } float get_setup_priority() const override; void set_reset_pin(GPIOPin *reset) { this->reset_pin_ = reset; } void set_palette(const uint8_t *palette) { this->palette_ = palette; } void set_buffer_color_mode(ILI9XXXColorMode color_mode) { this->buffer_color_mode_ = color_mode; } - void set_dimentions(int16_t width, int16_t height) { + void set_dimensions(int16_t width, int16_t height) { this->height_ = height; this->width_ = width; } - void invert_display(bool invert); + void set_offsets(int16_t offset_x, int16_t offset_y) { + this->offset_x_ = offset_x; + this->offset_y_ = offset_y; + } + void invert_colors(bool invert); void command(uint8_t value); void data(uint8_t value); void send_command(uint8_t command_byte, const uint8_t *data_bytes, uint8_t num_data_bytes); uint8_t read_command(uint8_t command_byte, uint8_t index); + void set_color_order(display::ColorOrder color_order) { this->color_order_ = color_order; } + void set_swap_xy(bool swap_xy) { this->swap_xy_ = swap_xy; } + void set_mirror_x(bool mirror_x) { this->mirror_x_ = mirror_x; } + void set_mirror_y(bool mirror_y) { this->mirror_y_ = mirror_y; } void update() override; @@ -50,16 +77,17 @@ class ILI9XXXDisplay : public display::DisplayBuffer, protected: void draw_absolute_pixel_internal(int x, int y, Color color) override; void setup_pins_(); - virtual void initialize() = 0; void display_(); - void init_lcd_(const uint8_t *init_cmd); - void set_addr_window_(uint16_t x, uint16_t y, uint16_t w, uint16_t h); - + void init_lcd_(); + void set_addr_window_(uint16_t x, uint16_t y, uint16_t x2, uint16_t y2); void reset_(); + uint8_t const *init_sequence_{}; int16_t width_{0}; ///< Display width as modified by current rotation int16_t height_{0}; ///< Display height as modified by current rotation + int16_t offset_x_{0}; + int16_t offset_y_{0}; uint16_t x_low_{0}; uint16_t y_low_{0}; uint16_t x_high_{0}; @@ -77,10 +105,6 @@ class ILI9XXXDisplay : public display::DisplayBuffer, void start_data_(); void end_data_(); - uint16_t transfer_buffer_[ILI9XXX_TRANSFER_BUFFER_SIZE]; - - uint32_t buffer_to_transfer_(uint32_t pos, uint32_t sz); - GPIOPin *reset_pin_{nullptr}; GPIOPin *dc_pin_{nullptr}; GPIOPin *busy_pin_{nullptr}; @@ -88,77 +112,87 @@ class ILI9XXXDisplay : public display::DisplayBuffer, bool prossing_update_ = false; bool need_update_ = false; bool is_18bitdisplay_ = false; - bool pre_invertdisplay_ = false; + bool pre_invertcolors_ = false; + display::ColorOrder color_order_{}; + bool swap_xy_{}; + bool mirror_x_{}; + bool mirror_y_{}; }; //----------- M5Stack display -------------- class ILI9XXXM5Stack : public ILI9XXXDisplay { - protected: - void initialize() override; + public: + ILI9XXXM5Stack() : ILI9XXXDisplay(INITCMD_M5STACK, 320, 240, true) {} }; //----------- M5Stack display -------------- class ILI9XXXM5CORE : public ILI9XXXDisplay { - protected: - void initialize() override; + public: + ILI9XXXM5CORE() : ILI9XXXDisplay(INITCMD_M5CORE, 320, 240, true) {} +}; + +//----------- ST7789V display -------------- +class ILI9XXXST7789V : public ILI9XXXDisplay { + public: + ILI9XXXST7789V() : ILI9XXXDisplay(INITCMD_ST7789V, 240, 320, false) {} }; //----------- ILI9XXX_24_TFT display -------------- class ILI9XXXILI9341 : public ILI9XXXDisplay { - protected: - void initialize() override; + public: + ILI9XXXILI9341() : ILI9XXXDisplay(INITCMD_ILI9341, 240, 320, false) {} }; //----------- ILI9XXX_24_TFT rotated display -------------- class ILI9XXXILI9342 : public ILI9XXXDisplay { - protected: - void initialize() override; + public: + ILI9XXXILI9342() : ILI9XXXDisplay(INITCMD_ILI9341, 320, 240, false) {} }; //----------- ILI9XXX_??_TFT rotated display -------------- class ILI9XXXILI9481 : public ILI9XXXDisplay { - protected: - void initialize() override; + public: + ILI9XXXILI9481() : ILI9XXXDisplay(INITCMD_ILI9481, 480, 320, false) {} }; //----------- ILI9481 in 18 bit mode -------------- class ILI9XXXILI948118 : public ILI9XXXDisplay { - protected: - void initialize() override; + public: + ILI9XXXILI948118() : ILI9XXXDisplay(INITCMD_ILI9481_18, 320, 480, true) {} }; //----------- ILI9XXX_35_TFT rotated display -------------- class ILI9XXXILI9486 : public ILI9XXXDisplay { - protected: - void initialize() override; + public: + ILI9XXXILI9486() : ILI9XXXDisplay(INITCMD_ILI9486, 480, 320, false) {} }; //----------- ILI9XXX_35_TFT rotated display -------------- class ILI9XXXILI9488 : public ILI9XXXDisplay { - protected: - void initialize() override; + public: + ILI9XXXILI9488() : ILI9XXXDisplay(INITCMD_ILI9488, 480, 320, true) {} }; //----------- ILI9XXX_35_TFT origin colors rotated display -------------- class ILI9XXXILI9488A : public ILI9XXXDisplay { - protected: - void initialize() override; + public: + ILI9XXXILI9488A() : ILI9XXXDisplay(INITCMD_ILI9488_A, 480, 320, true) {} }; //----------- ILI9XXX_35_TFT rotated display -------------- class ILI9XXXST7796 : public ILI9XXXDisplay { - protected: - void initialize() override; + public: + ILI9XXXST7796() : ILI9XXXDisplay(INITCMD_ST7796, 320, 480, false) {} }; class ILI9XXXS3Box : public ILI9XXXDisplay { - protected: - void initialize() override; + public: + ILI9XXXS3Box() : ILI9XXXDisplay(INITCMD_S3BOX, 320, 240, false) {} }; class ILI9XXXS3BoxLite : public ILI9XXXDisplay { - protected: - void initialize() override; + public: + ILI9XXXS3BoxLite() : ILI9XXXDisplay(INITCMD_S3BOXLITE, 320, 240, true) {} }; } // namespace ili9xxx diff --git a/esphome/components/ili9xxx/ili9xxx_init.h b/esphome/components/ili9xxx/ili9xxx_init.h index e3be9389b7..a74824052f 100644 --- a/esphome/components/ili9xxx/ili9xxx_init.h +++ b/esphome/components/ili9xxx/ili9xxx_init.h @@ -289,6 +289,33 @@ static const uint8_t PROGMEM INITCMD_S3BOXLITE[] = { 0x00 // End of list }; +static const uint8_t PROGMEM INITCMD_ST7789V[] = { + ILI9XXX_SLPOUT , 0x80, // Exit Sleep + ILI9XXX_DISPON , 0x80, // Display on + ILI9XXX_MADCTL , 1, 0x08, // Memory Access Control, BGR + ILI9XXX_DFUNCTR, 2, 0x0A, 0x82, + ILI9XXX_PIXFMT , 1, 0x55, + ILI9XXX_FRMCTR2, 5, 0x0C, 0x0C, 0x00, 0x33, 0x33, + ILI9XXX_ETMOD, 1, 0x35, 0xBB, 1, 0x28, + ILI9XXX_PWCTR1 , 1, 0x0C, // Power control VRH[5:0] + ILI9XXX_PWCTR3 , 2, 0x01, 0xFF, + ILI9XXX_PWCTR4 , 1, 0x10, + ILI9XXX_PWCTR5 , 1, 0x20, + ILI9XXX_IFCTR , 1, 0x0F, + ILI9XXX_PWSET, 2, 0xA4, 0xA1, + ILI9XXX_GMCTRP1 , 14, + 0xd0, 0x00, 0x02, 0x07, 0x0a, + 0x28, 0x32, 0x44, 0x42, 0x06, 0x0e, + 0x12, 0x14, 0x17, + ILI9XXX_GMCTRN1 , 14, + 0xd0, 0x00, 0x02, 0x07, 0x0a, + 0x28, 0x31, 0x54, 0x47, + 0x0e, 0x1c, 0x17, 0x1b, + 0x1e, + ILI9XXX_DISPON , 0x80, // Display on + 0x00 // End of list +}; + // clang-format on } // namespace ili9xxx } // namespace esphome diff --git a/tests/test1.yaml b/tests/test1.yaml index 61d28faf73..0849d8aeb6 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -1554,6 +1554,8 @@ sensor: memory_address: 0x7d name: Adres sensor +psram: + esp32_touch: setup_mode: false iir_filter: 10ms @@ -2992,6 +2994,12 @@ display: lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); - platform: ili9xxx + invert_colors: true + dimensions: 320x240 + transform: + swap_xy: true + mirror_x: true + mirror_y: false model: TFT 2.4 cs_pin: GPIO5 dc_pin: GPIO4 @@ -3000,6 +3008,11 @@ display: lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); - platform: ili9xxx + dimensions: + width: 320 + height: 240 + offset_width: 20 + offset_height: 10 model: TFT 2.4 cs_pin: GPIO5 dc_pin: GPIO4 From 1e772718580754ddf3ad3958f49e94859bdc5ed6 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 28 Nov 2023 16:07:29 +1300 Subject: [PATCH 18/89] Fix regex for 'byte' custom CI check (#5851) --- script/ci-custom.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/ci-custom.py b/script/ci-custom.py index d8c2f3053f..cc9bdcadbb 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -458,7 +458,7 @@ def lint_no_removed_in_idf_conversions(fname, match): @lint_re_check( - r"[^\w\d]byte\s+[\w\d]+\s*=", + r"[^\w\d]byte +[\w\d]+\s*=", include=cpp_include, exclude={ "esphome/components/tuya/tuya.h", From a8bc5ef46f24ce0d6f10614cbf7b2f1b045cce50 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Tue, 28 Nov 2023 14:50:02 +1100 Subject: [PATCH 19/89] Pass through additional arguments to create number (#5849) --- esphome/components/number/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/number/__init__.py b/esphome/components/number/__init__.py index e6ad545d70..07164be5ce 100644 --- a/esphome/components/number/__init__.py +++ b/esphome/components/number/__init__.py @@ -257,8 +257,8 @@ async def register_number( ) -async def new_number(config, *, min_value: float, max_value: float, step: float): - var = cg.new_Pvariable(config[CONF_ID]) +async def new_number(config, *args, min_value: float, max_value: float, step: float): + var = cg.new_Pvariable(config[CONF_ID], *args) await register_number( var, config, min_value=min_value, max_value=max_value, step=step ) From d1be686c54afa1fcf2b46b9f615e168252c7730e Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Tue, 28 Nov 2023 05:14:59 +0100 Subject: [PATCH 20/89] Nextion `on_touch` trigger (#5833) --- esphome/components/nextion/automation.h | 9 +++++++++ esphome/components/nextion/display.py | 19 +++++++++++++++++++ esphome/components/nextion/nextion.cpp | 5 +++++ esphome/components/nextion/nextion.h | 7 +++++++ 4 files changed, 40 insertions(+) diff --git a/esphome/components/nextion/automation.h b/esphome/components/nextion/automation.h index 210d7b2e2b..f51fe6b4f8 100644 --- a/esphome/components/nextion/automation.h +++ b/esphome/components/nextion/automation.h @@ -33,5 +33,14 @@ class PageTrigger : public Trigger { } }; +class TouchTrigger : public Trigger { + public: + explicit TouchTrigger(Nextion *nextion) { + nextion->add_touch_event_callback([this](uint8_t page_id, uint8_t component_id, bool touch_event) { + this->trigger(page_id, component_id, touch_event); + }); + } +}; + } // namespace nextion } // namespace esphome diff --git a/esphome/components/nextion/display.py b/esphome/components/nextion/display.py index 1ac0364b8b..92e85ad28a 100644 --- a/esphome/components/nextion/display.py +++ b/esphome/components/nextion/display.py @@ -7,6 +7,7 @@ from esphome.const import ( CONF_LAMBDA, CONF_BRIGHTNESS, CONF_TRIGGER_ID, + CONF_ON_TOUCH, ) from esphome.core import CORE from . import Nextion, nextion_ns, nextion_ref @@ -31,6 +32,7 @@ SetupTrigger = nextion_ns.class_("SetupTrigger", automation.Trigger.template()) SleepTrigger = nextion_ns.class_("SleepTrigger", automation.Trigger.template()) WakeTrigger = nextion_ns.class_("WakeTrigger", automation.Trigger.template()) PageTrigger = nextion_ns.class_("PageTrigger", automation.Trigger.template()) +TouchTrigger = nextion_ns.class_("TouchTrigger", automation.Trigger.template()) CONFIG_SCHEMA = ( display.BASIC_DISPLAY_SCHEMA.extend( @@ -58,6 +60,11 @@ CONFIG_SCHEMA = ( cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PageTrigger), } ), + cv.Optional(CONF_ON_TOUCH): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(TouchTrigger), + } + ), cv.Optional(CONF_TOUCH_SLEEP_TIMEOUT): cv.int_range(min=3, max=65535), cv.Optional(CONF_WAKE_UP_PAGE): cv.positive_int, cv.Optional(CONF_START_UP_PAGE): cv.positive_int, @@ -119,3 +126,15 @@ async def to_code(config): for conf in config.get(CONF_ON_PAGE, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation(trigger, [(cg.uint8, "x")], conf) + + for conf in config.get(CONF_ON_TOUCH, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation( + trigger, + [ + (cg.uint8, "page_id"), + (cg.uint8, "component_id"), + (cg.bool_, "touch_event"), + ], + conf, + ) diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index 02ba94fcab..fcc0d97655 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -167,6 +167,10 @@ void Nextion::add_new_page_callback(std::function &&callback) { this->page_callback_.add(std::move(callback)); } +void Nextion::add_touch_event_callback(std::function &&callback) { + this->touch_callback_.add(std::move(callback)); +} + void Nextion::update_all_components() { if ((!this->is_setup() && !this->ignore_is_setup_) || this->is_sleeping()) return; @@ -440,6 +444,7 @@ void Nextion::process_nextion_commands_() { for (auto *touch : this->touch_) { touch->process_touch(page_id, component_id, touch_event != 0); } + this->touch_callback_.call(page_id, component_id, touch_event != 0); break; } case 0x66: { // Nextion initiated new page event return data. diff --git a/esphome/components/nextion/nextion.h b/esphome/components/nextion/nextion.h index f3a13d2170..a90c91db8a 100644 --- a/esphome/components/nextion/nextion.h +++ b/esphome/components/nextion/nextion.h @@ -732,6 +732,12 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe */ void add_new_page_callback(std::function &&callback); + /** Add a callback to be notified when Nextion has a touch event. + * + * @param callback The void() callback. + */ + void add_touch_event_callback(std::function &&callback); + void update_all_components(); /** @@ -885,6 +891,7 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe CallbackManager sleep_callback_{}; CallbackManager wake_callback_{}; CallbackManager page_callback_{}; + CallbackManager touch_callback_{}; optional writer_; float brightness_{1.0}; From 2f888ff7c5737246bf277b5b71406cfe72804914 Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Tue, 28 Nov 2023 05:50:14 +0100 Subject: [PATCH 21/89] Nextion colors parameters (#5699) * Add `foreground` color - Adds `set_component_foreground_color` and `set_component_pressed_foreground_color` which does the same as `set_component_font_color` and `set_component_pressed_font_color` but with a more intuitive name, as this can be used for any component and not only the ones with a text (font). - I've also reviewed some docstring when related to colors. * Add numeric color to drawing methods Should I've used uint32_t instead? In order to keep consistency? * component color support to uint6_t This is the right format and is now consistent with colors on drawings. I'm keeping uint32_t also to avoid breaking changes. * Enforces uint16_t for colors uint32_t is incorrect for Nextion display colors. * Fix clang-format --- esphome/components/nextion/nextion.h | 305 ++++++++++++++---- esphome/components/nextion/nextion_base.h | 2 + .../components/nextion/nextion_commands.cpp | 80 ++++- .../components/nextion/nextion_component.cpp | 4 +- 4 files changed, 307 insertions(+), 84 deletions(-) diff --git a/esphome/components/nextion/nextion.h b/esphome/components/nextion/nextion.h index a90c91db8a..f188708f35 100644 --- a/esphome/components/nextion/nextion.h +++ b/esphome/components/nextion/nextion.h @@ -95,16 +95,18 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe /** * Set the background color of a component. * @param component The component name. - * @param color The color (as a uint32_t). + * @param color The color (as a uint16_t). * * Example: * ```cpp - * it.set_component_background_color("button", 0xFF0000); + * it.set_component_background_color("button", 63488); * ``` * * This will change the background color of the component `button` to red. + * Use this [color picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to convert color codes to + * Nextion HMI colors. */ - void set_component_background_color(const char *component, uint32_t color); + void set_component_background_color(const char *component, uint16_t color); /** * Set the background color of a component. * @param component The component name. @@ -115,9 +117,8 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * it.set_component_background_color("button", "RED"); * ``` * - * This will change the background color of the component `button` to blue. - * Use this [color picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to convert color codes to - * Nextion HMI colors. + * This will change the background color of the component `button` to red. + * Use [Nextion Instruction Set](https://nextion.tech/instruction-set/#s5) for a list of Nextion HMI colors constants. */ void set_component_background_color(const char *component, const char *color); /** @@ -127,26 +128,29 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * * Example: * ```cpp - * it.set_component_background_color("button", color); + * auto blue = Color(0, 0, 255); + * it.set_component_background_color("button", blue); * ``` * - * This will change the background color of the component `button` to what color contains. + * This will change the background color of the component `button` to blue. */ void set_component_background_color(const char *component, Color color) override; /** * Set the pressed background color of a component. * @param component The component name. - * @param color The color (as a int). + * @param color The color (as a uint16_t). * * Example: * ```cpp - * it.set_component_pressed_background_color("button", 0xFF0000 ); + * it.set_component_pressed_background_color("button", 63488); * ``` * * This will change the pressed background color of the component `button` to red. This is the background color that * is shown when the component is pressed. + * Use this [color picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to convert color codes to + * Nextion HMI colors. */ - void set_component_pressed_background_color(const char *component, uint32_t color); + void set_component_pressed_background_color(const char *component, uint16_t color); /** * Set the pressed background color of a component. * @param component The component name. @@ -157,10 +161,9 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * it.set_component_pressed_background_color("button", "RED"); * ``` * - * This will change the pressed background color of the component `button` to blue. This is the background color that - * is shown when the component is pressed. Use this [color - * picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to convert color codes to Nextion HMI - * colors. + * This will change the pressed background color of the component `button` to red. This is the background color that + * is shown when the component is pressed. + * Use [Nextion Instruction Set](https://nextion.tech/instruction-set/#s5) for a list of Nextion HMI colors constants. */ void set_component_pressed_background_color(const char *component, const char *color); /** @@ -170,15 +173,102 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * * Example: * ```cpp - * it.set_component_pressed_background_color("button", color); + * auto red = Color(255, 0, 0); + * it.set_component_pressed_background_color("button", red); * ``` * - * This will change the pressed background color of the component `button` to blue. This is the background color that - * is shown when the component is pressed. Use this [color - * picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to convert color codes to Nextion HMI - * colors. + * This will change the pressed background color of the component `button` to red. This is the background color that + * is shown when the component is pressed. */ void set_component_pressed_background_color(const char *component, Color color) override; + /** + * Set the foreground color of a component. + * @param component The component name. + * @param color The color (as a uint16_t). + * + * Example: + * ```cpp + * it.set_component_foreground_color("button", 63488); + * ``` + * + * This will change the foreground color of the component `button` to red. + * Use this [color picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to convert color codes to + * Nextion HMI colors. + */ + void set_component_foreground_color(const char *component, uint16_t color); + /** + * Set the foreground color of a component. + * @param component The component name. + * @param color The color (as a string). + * + * Example: + * ```cpp + * it.set_component_foreground_color("button", "RED"); + * ``` + * + * This will change the foreground color of the component `button` to red. + * Use [Nextion Instruction Set](https://nextion.tech/instruction-set/#s5) for a list of Nextion HMI colors constants. + */ + void set_component_foreground_color(const char *component, const char *color); + /** + * Set the foreground color of a component. + * @param component The component name. + * @param color The color (as Color). + * + * Example: + * ```cpp + * it.set_component_foreground_color("button", Color::BLACK); + * ``` + * + * This will change the foreground color of the component `button` to black. + */ + void set_component_foreground_color(const char *component, Color color) override; + /** + * Set the pressed foreground color of a component. + * @param component The component name. + * @param color The color (as a uint16_t). + * + * Example: + * ```cpp + * it.set_component_pressed_foreground_color("button", 63488 ); + * ``` + * + * This will change the pressed foreground color of the component `button` to red. This is the foreground color that + * is shown when the component is pressed. + * Use this [color picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to convert color codes to + * Nextion HMI colors. + */ + void set_component_pressed_foreground_color(const char *component, uint16_t color); + /** + * Set the pressed foreground color of a component. + * @param component The component name. + * @param color The color (as a string). + * + * Example: + * ```cpp + * it.set_component_pressed_foreground_color("button", "RED"); + * ``` + * + * This will change the pressed foreground color of the component `button` to red. This is the foreground color that + * is shown when the component is pressed. + * Use [Nextion Instruction Set](https://nextion.tech/instruction-set/#s5) for a list of Nextion HMI colors constants. + */ + void set_component_pressed_foreground_color(const char *component, const char *color); + /** + * Set the pressed foreground color of a component. + * @param component The component name. + * @param color The color (as Color). + * + * Example: + * ```cpp + * auto blue = Color(0, 0, 255); + * it.set_component_pressed_foreground_color("button", blue); + * ``` + * + * This will change the pressed foreground color of the component `button` to blue. This is the foreground color that + * is shown when the component is pressed. + */ + void set_component_pressed_foreground_color(const char *component, Color color) override; /** * Set the picture id of a component. @@ -210,16 +300,18 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe /** * Set the font color of a component. * @param component The component name. - * @param color The color (as a uint32_t ). + * @param color The color (as a uint16_t). * * Example: * ```cpp - * it.set_component_font_color("textview", 0xFF0000); + * it.set_component_font_color("textview", 63488); * ``` * * This will change the font color of the component `textview` to a red color. + * Use this [color picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to convert color codes to + * Nextion HMI colors. */ - void set_component_font_color(const char *component, uint32_t color); + void set_component_font_color(const char *component, uint16_t color); /** * Set the font color of a component. * @param component The component name. @@ -230,9 +322,8 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * it.set_component_font_color("textview", "RED"); * ``` * - * This will change the font color of the component `textview` to a blue color. - * Use this [color picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to convert color codes to - * Nextion HMI colors. + * This will change the font color of the component `textview` to a red color. + * Use [Nextion Instruction Set](https://nextion.tech/instruction-set/#s5) for a list of Nextion HMI colors constants. */ void set_component_font_color(const char *component, const char *color); /** @@ -242,27 +333,27 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * * Example: * ```cpp - * it.set_component_font_color("textview", color); + * it.set_component_font_color("textview", Color::BLACK); * ``` * - * This will change the font color of the component `textview` to a blue color. - * Use this [color picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to convert color codes to - * Nextion HMI colors. + * This will change the font color of the component `textview` to black. */ void set_component_font_color(const char *component, Color color) override; /** * Set the pressed font color of a component. * @param component The component name. - * @param color The color (as a uint32_t). + * @param color The color (as a uint16_t). * * Example: * ```cpp - * it.set_component_pressed_font_color("button", 0xFF0000); + * it.set_component_pressed_font_color("button", 63488); * ``` * * This will change the pressed font color of the component `button` to a red. + * Use this [color picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to convert color codes to + * Nextion HMI colors. */ - void set_component_pressed_font_color(const char *component, uint32_t color); + void set_component_pressed_font_color(const char *component, uint16_t color); /** * Set the pressed font color of a component. * @param component The component name. @@ -273,9 +364,8 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * it.set_component_pressed_font_color("button", "RED"); * ``` * - * This will change the pressed font color of the component `button` to a blue color. - * Use this [color picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to convert color codes to - * Nextion HMI colors. + * This will change the pressed font color of the component `button` to a red color. + * Use [Nextion Instruction Set](https://nextion.tech/instruction-set/#s5) for a list of Nextion HMI colors constants. */ void set_component_pressed_font_color(const char *component, const char *color); /** @@ -285,12 +375,10 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * * Example: * ```cpp - * it.set_component_pressed_font_color("button", color); + * it.set_component_pressed_font_color("button", Color::BLACK); * ``` * - * This will change the pressed font color of the component `button` to a blue color. - * Use this [color picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to convert color codes to - * Nextion HMI colors. + * This will change the pressed font color of the component `button` to black. */ void set_component_pressed_font_color(const char *component, Color color) override; /** @@ -420,6 +508,25 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * Displays the picture who has the id `2` at the x coordinates `15` and y coordinates `25`. */ void display_picture(int picture_id, int x_start, int y_start); + /** + * Fill a rectangle with a color. + * @param x1 The starting x coordinate. + * @param y1 The starting y coordinate. + * @param width The width to draw. + * @param height The height to draw. + * @param color The color to draw with (number). + * + * Example: + * ```cpp + * fill_area(50, 50, 100, 100, 63488); + * ``` + * + * Fills an area that starts at x coordinate `50` and y coordinate `50` with a height of `100` and width of `100` with + * the red color. + * Use this [color picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to convert color codes to + * Nextion HMI colors. + */ + void fill_area(int x1, int y1, int width, int height, uint16_t color); /** * Fill a rectangle with a color. * @param x1 The starting x coordinate. @@ -434,8 +541,8 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * ``` * * Fills an area that starts at x coordinate `50` and y coordinate `50` with a height of `100` and width of `100` with - * the color of blue. Use this [color picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to - * convert color codes to Nextion HMI colors + * the red color. + * Use [Nextion Instruction Set](https://nextion.tech/instruction-set/#s5) for a list of Nextion HMI colors constants. */ void fill_area(int x1, int y1, int width, int height, const char *color); /** @@ -448,14 +555,33 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * * Example: * ```cpp - * fill_area(50, 50, 100, 100, color); + * auto blue = Color(0, 0, 255); + * fill_area(50, 50, 100, 100, blue); * ``` * * Fills an area that starts at x coordinate `50` and y coordinate `50` with a height of `100` and width of `100` with - * the color of blue. Use this [color picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to - * convert color codes to Nextion HMI colors + * blue color. */ void fill_area(int x1, int y1, int width, int height, Color color); + /** + * Draw a line on the screen. + * @param x1 The starting x coordinate. + * @param y1 The starting y coordinate. + * @param x2 The ending x coordinate. + * @param y2 The ending y coordinate. + * @param color The color to draw with (number). + * + * Example: + * ```cpp + * it.line(50, 50, 75, 75, 63488); + * ``` + * + * Makes a line that starts at x coordinate `50` and y coordinate `50` and ends at x coordinate `75` and y coordinate + * `75` with the red color. + * Use this [color picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to convert color codes to + * Nextion HMI colors. + */ + void line(int x1, int y1, int x2, int y2, uint16_t color); /** * Draw a line on the screen. * @param x1 The starting x coordinate. @@ -466,13 +592,12 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * * Example: * ```cpp - * it.line(50, 50, 75, 75, "17013"); + * it.line(50, 50, 75, 75, "BLUE"); * ``` * * Makes a line that starts at x coordinate `50` and y coordinate `50` and ends at x coordinate `75` and y coordinate - * `75` with the color of blue. Use this [color - * picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to convert color codes to Nextion HMI - * colors. + * `75` with the blue color. + * Use [Nextion Instruction Set](https://nextion.tech/instruction-set/#s5) for a list of Nextion HMI colors constants. */ void line(int x1, int y1, int x2, int y2, const char *color); /** @@ -485,15 +610,33 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * * Example: * ```cpp - * it.line(50, 50, 75, 75, "17013"); + * auto blue = Color(0, 0, 255); + * it.line(50, 50, 75, 75, blue); * ``` * * Makes a line that starts at x coordinate `50` and y coordinate `50` and ends at x coordinate `75` and y coordinate - * `75` with the color of blue. Use this [color - * picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to convert color codes to Nextion HMI - * colors. + * `75` with blue color. */ void line(int x1, int y1, int x2, int y2, Color color); + /** + * Draw a rectangle outline. + * @param x1 The starting x coordinate. + * @param y1 The starting y coordinate. + * @param width The width of the rectangle. + * @param height The height of the rectangle. + * @param color The color to draw with (number). + * + * Example: + * ```cpp + * it.rectangle(25, 35, 40, 50, 63488); + * ``` + * + * Makes a outline of a rectangle that starts at x coordinate `25` and y coordinate `35` and has a width of `40` and a + * length of `50` with the red color. + * Use this [color picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to convert color codes to + * Nextion HMI colors. + */ + void rectangle(int x1, int y1, int width, int height, uint16_t color); /** * Draw a rectangle outline. * @param x1 The starting x coordinate. @@ -504,13 +647,12 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * * Example: * ```cpp - * it.rectangle(25, 35, 40, 50, "17013"); + * it.rectangle(25, 35, 40, 50, "BLUE"); * ``` * * Makes a outline of a rectangle that starts at x coordinate `25` and y coordinate `35` and has a width of `40` and a - * length of `50` with color of blue. Use this [color - * picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to convert color codes to Nextion HMI - * colors. + * length of `50` with the blue color. + * Use [Nextion Instruction Set](https://nextion.tech/instruction-set/#s5) for a list of Nextion HMI colors constants. */ void rectangle(int x1, int y1, int width, int height, const char *color); /** @@ -523,21 +665,31 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * * Example: * ```cpp - * it.rectangle(25, 35, 40, 50, "17013"); + * auto blue = Color(0, 0, 255); + * it.rectangle(25, 35, 40, 50, blue); * ``` * * Makes a outline of a rectangle that starts at x coordinate `25` and y coordinate `35` and has a width of `40` and a - * length of `50` with color of blue. Use this [color - * picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to convert color codes to Nextion HMI - * colors. + * length of `50` with blue color. */ void rectangle(int x1, int y1, int width, int height, Color color); + /** + * Draw a circle outline + * @param center_x The center x coordinate. + * @param center_y The center y coordinate. + * @param radius The circle radius. + * @param color The color to draw with (number). + * Use this [color picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to convert color codes to + * Nextion HMI colors. + */ + void circle(int center_x, int center_y, int radius, uint16_t color); /** * Draw a circle outline * @param center_x The center x coordinate. * @param center_y The center y coordinate. * @param radius The circle radius. * @param color The color to draw with (as a string). + * Use [Nextion Instruction Set](https://nextion.tech/instruction-set/#s5) for a list of Nextion HMI colors constants. */ void circle(int center_x, int center_y, int radius, const char *color); /** @@ -548,6 +700,23 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * @param color The color to draw with (as Color). */ void circle(int center_x, int center_y, int radius, Color color); + /** + * Draw a filled circled. + * @param center_x The center x coordinate. + * @param center_y The center y coordinate. + * @param radius The circle radius. + * @param color The color to draw with (number). + * + * Example: + * ```cpp + * it.filled_cricle(25, 25, 10, 63488); + * ``` + * + * Makes a filled circle at the x coordinate `25` and y coordinate `25` with a radius of `10` with the red color. + * Use this [color picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to convert color codes to + * Nextion HMI colors. + */ + void filled_circle(int center_x, int center_y, int radius, uint16_t color); /** * Draw a filled circled. * @param center_x The center x coordinate. @@ -557,12 +726,11 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * * Example: * ```cpp - * it.filled_cricle(25, 25, 10, "17013"); + * it.filled_cricle(25, 25, 10, "BLUE"); * ``` * - * Makes a filled circle at the x coordinate `25` and y coordinate `25` with a radius of `10` with a color of blue. - * Use this [color picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to convert color codes to - * Nextion HMI colors. + * Makes a filled circle at the x coordinate `25` and y coordinate `25` with a radius of `10` with the blue color. + * Use [Nextion Instruction Set](https://nextion.tech/instruction-set/#s5) for a list of Nextion HMI colors constants. */ void filled_circle(int center_x, int center_y, int radius, const char *color); /** @@ -574,12 +742,11 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * * Example: * ```cpp - * it.filled_cricle(25, 25, 10, color); + * auto blue = Color(0, 0, 255); + * it.filled_cricle(25, 25, 10, blue); * ``` * - * Makes a filled circle at the x coordinate `25` and y coordinate `25` with a radius of `10` with a color of blue. - * Use this [color picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to convert color codes to - * Nextion HMI colors. + * Makes a filled circle at the x coordinate `25` and y coordinate `25` with a radius of `10` with blue color. */ void filled_circle(int center_x, int center_y, int radius, Color color); diff --git a/esphome/components/nextion/nextion_base.h b/esphome/components/nextion/nextion_base.h index ffc431ed13..b5729a1df1 100644 --- a/esphome/components/nextion/nextion_base.h +++ b/esphome/components/nextion/nextion_base.h @@ -39,6 +39,8 @@ class NextionBase { virtual void set_component_background_color(const char *component, Color color) = 0; virtual void set_component_pressed_background_color(const char *component, Color color) = 0; + virtual void set_component_foreground_color(const char *component, Color color) = 0; + virtual void set_component_pressed_foreground_color(const char *component, Color color) = 0; virtual void set_component_font_color(const char *component, Color color) = 0; virtual void set_component_pressed_font_color(const char *component, Color color) = 0; virtual void set_component_font(const char *component, uint8_t font_id) = 0; diff --git a/esphome/components/nextion/nextion_commands.cpp b/esphome/components/nextion/nextion_commands.cpp index 35530d1a7f..0722ed6f4e 100644 --- a/esphome/components/nextion/nextion_commands.cpp +++ b/esphome/components/nextion/nextion_commands.cpp @@ -53,9 +53,9 @@ void Nextion::set_protocol_reparse_mode(bool active_mode) { this->write_array(to_send, sizeof(to_send)); } -// Set Colors -void Nextion::set_component_background_color(const char *component, uint32_t color) { - this->add_no_result_to_queue_with_printf_("set_component_background_color", "%s.bco=%" PRIu32, component, color); +// Set Colors - Background +void Nextion::set_component_background_color(const char *component, uint16_t color) { + this->add_no_result_to_queue_with_printf_("set_component_background_color", "%s.bco=%" PRIu16, component, color); } void Nextion::set_component_background_color(const char *component, const char *color) { @@ -67,8 +67,9 @@ void Nextion::set_component_background_color(const char *component, Color color) display::ColorUtil::color_to_565(color)); } -void Nextion::set_component_pressed_background_color(const char *component, uint32_t color) { - this->add_no_result_to_queue_with_printf_("set_component_pressed_background_color", "%s.bco2=%" PRIu32, component, +// Set Colors - Background (pressed) +void Nextion::set_component_pressed_background_color(const char *component, uint16_t color) { + this->add_no_result_to_queue_with_printf_("set_component_pressed_background_color", "%s.bco2=%" PRIu16, component, color); } @@ -81,16 +82,38 @@ void Nextion::set_component_pressed_background_color(const char *component, Colo display::ColorUtil::color_to_565(color)); } -void Nextion::set_component_pic(const char *component, uint8_t pic_id) { - this->add_no_result_to_queue_with_printf_("set_component_pic", "%s.pic=%d", component, pic_id); +// Set Colors - Foreground +void Nextion::set_component_foreground_color(const char *component, uint16_t color) { + this->add_no_result_to_queue_with_printf_("set_component_foreground_color", "%s.pco=%" PRIu16, component, color); } -void Nextion::set_component_picc(const char *component, uint8_t pic_id) { - this->add_no_result_to_queue_with_printf_("set_component_pic", "%s.picc=%d", component, pic_id); +void Nextion::set_component_foreground_color(const char *component, const char *color) { + this->add_no_result_to_queue_with_printf_("set_component_foreground_color", "%s.pco=%s", component, color); } -void Nextion::set_component_font_color(const char *component, uint32_t color) { - this->add_no_result_to_queue_with_printf_("set_component_font_color", "%s.pco=%" PRIu32, component, color); +void Nextion::set_component_foreground_color(const char *component, Color color) { + this->add_no_result_to_queue_with_printf_("set_component_foreground_color", "%s.pco=%d", component, + display::ColorUtil::color_to_565(color)); +} + +// Set Colors - Foreground (pressed) +void Nextion::set_component_pressed_foreground_color(const char *component, uint16_t color) { + this->add_no_result_to_queue_with_printf_("set_component_pressed_foreground_color", "%s.pco2=%" PRIu16, component, + color); +} + +void Nextion::set_component_pressed_foreground_color(const char *component, const char *color) { + this->add_no_result_to_queue_with_printf_("set_component_pressed_foreground_color", " %s.pco2=%s", component, color); +} + +void Nextion::set_component_pressed_foreground_color(const char *component, Color color) { + this->add_no_result_to_queue_with_printf_("set_component_pressed_foreground_color", "%s.pco2=%d", component, + display::ColorUtil::color_to_565(color)); +} + +// Set Colors - Font +void Nextion::set_component_font_color(const char *component, uint16_t color) { + this->add_no_result_to_queue_with_printf_("set_component_font_color", "%s.pco=%" PRIu16, component, color); } void Nextion::set_component_font_color(const char *component, const char *color) { @@ -102,8 +125,9 @@ void Nextion::set_component_font_color(const char *component, Color color) { display::ColorUtil::color_to_565(color)); } -void Nextion::set_component_pressed_font_color(const char *component, uint32_t color) { - this->add_no_result_to_queue_with_printf_("set_component_pressed_font_color", "%s.pco2=%" PRIu32, component, color); +// Set Colors - Font (pressed) +void Nextion::set_component_pressed_font_color(const char *component, uint16_t color) { + this->add_no_result_to_queue_with_printf_("set_component_pressed_font_color", "%s.pco2=%" PRIu16, component, color); } void Nextion::set_component_pressed_font_color(const char *component, const char *color) { @@ -115,6 +139,15 @@ void Nextion::set_component_pressed_font_color(const char *component, Color colo display::ColorUtil::color_to_565(color)); } +// Set picture +void Nextion::set_component_pic(const char *component, uint8_t pic_id) { + this->add_no_result_to_queue_with_printf_("set_component_pic", "%s.pic=%d", component, pic_id); +} + +void Nextion::set_component_picc(const char *component, uint8_t pic_id) { + this->add_no_result_to_queue_with_printf_("set_component_pic", "%s.picc=%d", component, pic_id); +} + void Nextion::set_component_text_printf(const char *component, const char *format, ...) { va_list arg; va_start(arg, format); @@ -193,6 +226,10 @@ void Nextion::display_picture(int picture_id, int x_start, int y_start) { this->add_no_result_to_queue_with_printf_("display_picture", "pic %d, %d, %d", x_start, y_start, picture_id); } +void Nextion::fill_area(int x1, int y1, int width, int height, uint16_t color) { + this->add_no_result_to_queue_with_printf_("fill_area", "fill %d,%d,%d,%d,%" PRIu16, x1, y1, width, height, color); +} + void Nextion::fill_area(int x1, int y1, int width, int height, const char *color) { this->add_no_result_to_queue_with_printf_("fill_area", "fill %d,%d,%d,%d,%s", x1, y1, width, height, color); } @@ -202,6 +239,10 @@ void Nextion::fill_area(int x1, int y1, int width, int height, Color color) { display::ColorUtil::color_to_565(color)); } +void Nextion::line(int x1, int y1, int x2, int y2, uint16_t color) { + this->add_no_result_to_queue_with_printf_("line", "line %d,%d,%d,%d,%" PRIu16, x1, y1, x2, y2, color); +} + void Nextion::line(int x1, int y1, int x2, int y2, const char *color) { this->add_no_result_to_queue_with_printf_("line", "line %d,%d,%d,%d,%s", x1, y1, x2, y2, color); } @@ -211,6 +252,11 @@ void Nextion::line(int x1, int y1, int x2, int y2, Color color) { display::ColorUtil::color_to_565(color)); } +void Nextion::rectangle(int x1, int y1, int width, int height, uint16_t color) { + this->add_no_result_to_queue_with_printf_("draw", "draw %d,%d,%d,%d,%" PRIu16, x1, y1, x1 + width, y1 + height, + color); +} + void Nextion::rectangle(int x1, int y1, int width, int height, const char *color) { this->add_no_result_to_queue_with_printf_("draw", "draw %d,%d,%d,%d,%s", x1, y1, x1 + width, y1 + height, color); } @@ -220,6 +266,10 @@ void Nextion::rectangle(int x1, int y1, int width, int height, Color color) { display::ColorUtil::color_to_565(color)); } +void Nextion::circle(int center_x, int center_y, int radius, uint16_t color) { + this->add_no_result_to_queue_with_printf_("cir", "cir %d,%d,%d,%" PRIu16, center_x, center_y, radius, color); +} + void Nextion::circle(int center_x, int center_y, int radius, const char *color) { this->add_no_result_to_queue_with_printf_("cir", "cir %d,%d,%d,%s", center_x, center_y, radius, color); } @@ -229,6 +279,10 @@ void Nextion::circle(int center_x, int center_y, int radius, Color color) { display::ColorUtil::color_to_565(color)); } +void Nextion::filled_circle(int center_x, int center_y, int radius, uint16_t color) { + this->add_no_result_to_queue_with_printf_("cirs", "cirs %d,%d,%d,%" PRIu16, center_x, center_y, radius, color); +} + void Nextion::filled_circle(int center_x, int center_y, int radius, const char *color) { this->add_no_result_to_queue_with_printf_("cirs", "cirs %d,%d,%d,%s", center_x, center_y, radius, color); } diff --git a/esphome/components/nextion/nextion_component.cpp b/esphome/components/nextion/nextion_component.cpp index bbb2cf6cb2..cfb4e3600c 100644 --- a/esphome/components/nextion/nextion_component.cpp +++ b/esphome/components/nextion/nextion_component.cpp @@ -99,11 +99,11 @@ void NextionComponent::update_component_settings(bool force_update) { this->bco2_needs_update_ = false; } if (this->pco_needs_update_ || (force_update && this->pco_is_set_)) { - this->nextion_->set_component_font_color(this->variable_name_.c_str(), this->pco_); + this->nextion_->set_component_foreground_color(this->variable_name_.c_str(), this->pco_); this->pco_needs_update_ = false; } if (this->pco2_needs_update_ || (force_update && this->pco2_is_set_)) { - this->nextion_->set_component_pressed_font_color(this->variable_name_.c_str(), this->pco2_); + this->nextion_->set_component_pressed_foreground_color(this->variable_name_.c_str(), this->pco2_); this->pco2_needs_update_ = false; } From ad5f6b5687b90aad4e1fb667a8eb7ef5e16ecc79 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 27 Nov 2023 23:11:17 -0600 Subject: [PATCH 22/89] dashboard: fix supervisor auth doing I/O in the event loop (#5807) --- esphome/dashboard/web_server.py | 62 ++++++++++++++++++++------------- 1 file changed, 37 insertions(+), 25 deletions(-) diff --git a/esphome/dashboard/web_server.py b/esphome/dashboard/web_server.py index 9bbf0b28dc..4552aebf7b 100644 --- a/esphome/dashboard/web_server.py +++ b/esphome/dashboard/web_server.py @@ -13,9 +13,9 @@ import secrets import shutil import subprocess import threading -from pathlib import Path -from typing import Any, Callable, TypeVar from collections.abc import Iterable +from pathlib import Path +from typing import TYPE_CHECKING, Any, Callable, TypeVar import tornado import tornado.concurrent @@ -45,12 +45,17 @@ from .util.file import write_file from .util.subprocess import async_run_system_command from .util.text import friendly_name_slugify +if TYPE_CHECKING: + from requests import Response + + _LOGGER = logging.getLogger(__name__) ENV_DEV = "ESPHOME_DASHBOARD_DEV" +COOKIE_AUTHENTICATED_YES = b"yes" -cookie_authenticated_yes = b"yes" +AUTH_COOKIE_NAME = "authenticated" settings = DASHBOARD.settings @@ -89,18 +94,18 @@ def authenticated(func: T) -> T: return decorator -def is_authenticated(request_handler): +def is_authenticated(handler: BaseHandler) -> bool: + """Check if the request is authenticated.""" if settings.on_ha_addon: # Handle ingress - disable auth on ingress port # X-HA-Ingress is automatically stripped on the non-ingress server in nginx - header = request_handler.request.headers.get("X-HA-Ingress", "NO") + header = handler.request.headers.get("X-HA-Ingress", "NO") if str(header) == "YES": return True + if settings.using_auth: - return ( - request_handler.get_secure_cookie("authenticated") - == cookie_authenticated_yes - ) + return handler.get_secure_cookie(AUTH_COOKIE_NAME) == COOKIE_AUTHENTICATED_YES + return True @@ -860,38 +865,45 @@ class LoginHandler(BaseHandler): **template_args(), ) - def post_ha_addon_login(self) -> None: + def _make_supervisor_auth_request(self) -> Response: + """Make a request to the supervisor auth endpoint.""" import requests - headers = { - "X-Supervisor-Token": os.getenv("SUPERVISOR_TOKEN"), - } - + headers = {"X-Supervisor-Token": os.getenv("SUPERVISOR_TOKEN")} data = { "username": self.get_argument("username", ""), "password": self.get_argument("password", ""), } + return requests.post( + "http://supervisor/auth", headers=headers, json=data, timeout=30 + ) + + async def post_ha_addon_login(self) -> None: + loop = asyncio.get_running_loop() try: - req = requests.post( - "http://supervisor/auth", headers=headers, json=data, timeout=30 - ) - if req.status_code == 200: - self.set_secure_cookie("authenticated", cookie_authenticated_yes) - self.redirect("/") - return + req = await loop.run_in_executor(None, self._make_supervisor_auth_request) except Exception as err: # pylint: disable=broad-except _LOGGER.warning("Error during Hass.io auth request: %s", err) self.set_status(500) self.render_login_page(error="Internal server error") return + + if req.status_code == 200: + self._set_authenticated() + self.redirect("/") + return self.set_status(401) self.render_login_page(error="Invalid username or password") + def _set_authenticated(self) -> None: + """Set the authenticated cookie.""" + self.set_secure_cookie(AUTH_COOKIE_NAME, COOKIE_AUTHENTICATED_YES) + def post_native_login(self) -> None: username = self.get_argument("username", "") password = self.get_argument("password", "") if settings.check_password(username, password): - self.set_secure_cookie("authenticated", cookie_authenticated_yes) + self._set_authenticated() self.redirect("./") return error_str = ( @@ -900,9 +912,9 @@ class LoginHandler(BaseHandler): self.set_status(401) self.render_login_page(error=error_str) - def post(self) -> None: + async def post(self): if settings.using_ha_addon_auth: - self.post_ha_addon_login() + await self.post_ha_addon_login() else: self.post_native_login() @@ -910,7 +922,7 @@ class LoginHandler(BaseHandler): class LogoutHandler(BaseHandler): @authenticated def get(self) -> None: - self.clear_cookie("authenticated") + self.clear_cookie(AUTH_COOKIE_NAME) self.redirect("./login") From 3940c6ac4ef5c91685b0e4320750f63369e2a4ab Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Tue, 28 Nov 2023 06:25:20 +0100 Subject: [PATCH 23/89] Improve reliability of Nextion TFT uploads (Arduino) (#5683) Co-authored-by: Keith Burzinski --- .../nextion/nextion_upload_arduino.cpp | 61 ++++++++++++------- 1 file changed, 39 insertions(+), 22 deletions(-) diff --git a/esphome/components/nextion/nextion_upload_arduino.cpp b/esphome/components/nextion/nextion_upload_arduino.cpp index d1f9f44c2b..3337ff9b50 100644 --- a/esphome/components/nextion/nextion_upload_arduino.cpp +++ b/esphome/components/nextion/nextion_upload_arduino.cpp @@ -41,7 +41,7 @@ int Nextion::upload_by_chunks_(HTTPClient *http, int range_start) { #if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 6, 0) http->setRedirectLimit(3); #endif -#endif +#endif // USE_ESP8266 char range_header[64]; sprintf(range_header, "bytes=%d-%d", range_start, range_end); @@ -62,6 +62,7 @@ int Nextion::upload_by_chunks_(HTTPClient *http, int range_start) { ++tries; if (!begin_status) { ESP_LOGD(TAG, "upload_by_chunks_: connection failed"); + delay(500); // NOLINT continue; } @@ -84,10 +85,10 @@ int Nextion::upload_by_chunks_(HTTPClient *http, int range_start) { std::string recv_string; size_t size = 0; - int sent = 0; + int fetched = 0; int range = range_end - range_start; - while (sent < range) { + while (fetched < range) { size = http->getStreamPtr()->available(); if (!size) { App.feed_wdt(); @@ -95,28 +96,38 @@ int Nextion::upload_by_chunks_(HTTPClient *http, int range_start) { continue; } int c = http->getStreamPtr()->readBytes( - &this->transfer_buffer_[sent], ((size > this->transfer_buffer_size_) ? this->transfer_buffer_size_ : size)); - sent += c; + &this->transfer_buffer_[fetched], ((size > this->transfer_buffer_size_) ? this->transfer_buffer_size_ : size)); + fetched += c; } http->end(); - ESP_LOGN(TAG, "this->content_length_ %d sent %d", this->content_length_, sent); + ESP_LOGN(TAG, "Fetched %d of %d bytes", fetched, this->content_length_); + + // upload fetched segments to the display in 4KB chunks + int write_len; for (int i = 0; i < range; i += 4096) { - this->write_array(&this->transfer_buffer_[i], 4096); - this->content_length_ -= 4096; - ESP_LOGN(TAG, "this->content_length_ %d range %d range_end %d range_start %d", this->content_length_, range, - range_end, range_start); + App.feed_wdt(); + write_len = this->content_length_ < 4096 ? this->content_length_ : 4096; + this->write_array(&this->transfer_buffer_[i], write_len); + this->content_length_ -= write_len; + ESP_LOGD(TAG, "Uploaded %0.2f %%; %d bytes remaining", + 100.0 * (this->tft_size_ - this->content_length_) / this->tft_size_, this->content_length_); if (!this->upload_first_chunk_sent_) { this->upload_first_chunk_sent_ = true; delay(500); // NOLINT - App.feed_wdt(); } - this->recv_ret_string_(recv_string, 2048, true); - if (recv_string[0] == 0x08) { + this->recv_ret_string_(recv_string, 4096, true); + if (recv_string[0] != 0x05) { // 0x05 == "ok" + ESP_LOGD(TAG, "recv_string [%s]", + format_hex_pretty(reinterpret_cast(recv_string.data()), recv_string.size()).c_str()); + } + + // handle partial upload request + if (recv_string[0] == 0x08 && recv_string.size() == 5) { uint32_t result = 0; - for (int i = 0; i < 4; ++i) { - result += static_cast(recv_string[i + 1]) << (8 * i); + for (int j = 0; j < 4; ++j) { + result += static_cast(recv_string[j + 1]) << (8 * j); } if (result > 0) { ESP_LOGD(TAG, "Nextion reported new range %d", result); @@ -126,10 +137,14 @@ int Nextion::upload_by_chunks_(HTTPClient *http, int range_start) { } recv_string.clear(); } + return range_end + 1; } bool Nextion::upload_tft() { + ESP_LOGD(TAG, "Nextion TFT upload requested"); + ESP_LOGD(TAG, "URL: %s", this->tft_url_.c_str()); + if (this->is_updating_) { ESP_LOGD(TAG, "Currently updating"); return false; @@ -162,7 +177,7 @@ bool Nextion::upload_tft() { if (!begin_status) { this->is_updating_ = false; - ESP_LOGD(TAG, "connection failed"); + ESP_LOGD(TAG, "Connection failed"); ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); allocator.deallocate(this->transfer_buffer_, this->transfer_buffer_size_); return false; @@ -237,7 +252,9 @@ bool Nextion::upload_tft() { this->recv_ret_string_(response, 2000, true); // This can take some time to return // The Nextion display will, if it's ready to accept data, send a 0x05 byte. - ESP_LOGD(TAG, "Upgrade response is %s %zu", response.c_str(), response.length()); + ESP_LOGD(TAG, "Upgrade response is [%s] - %zu bytes", + format_hex_pretty(reinterpret_cast(response.data()), response.size()).c_str(), + response.length()); for (size_t i = 0; i < response.length(); i++) { ESP_LOGD(TAG, "Available %d : 0x%02X", i, response[i]); @@ -256,17 +273,17 @@ bool Nextion::upload_tft() { if (heap_caps_get_free_size(MALLOC_CAP_SPIRAM) > 0) { chunk_size = this->content_length_; } else { - if (ESP.getFreeHeap() > 40960) { // 32K to keep on hand - int chunk = int((ESP.getFreeHeap() - 32768) / 4096); - chunk_size = chunk * 4096; + if (ESP.getFreeHeap() > 81920) { // Ensure some FreeHeap to other things and limit chunk size + chunk_size = ESP.getFreeHeap() - 65536; + chunk_size = int(chunk_size / 4096) * 4096; chunk_size = chunk_size > 65536 ? 65536 : chunk_size; - } else if (ESP.getFreeHeap() < 10240) { + } else if (ESP.getFreeHeap() < 32768) { chunk_size = 4096; } } #else // NOLINTNEXTLINE(readability-static-accessed-through-instance) - uint32_t chunk_size = ESP.getFreeHeap() < 10240 ? 4096 : 8192; + uint32_t chunk_size = ESP.getFreeHeap() < 16384 ? 4096 : 8192; #endif if (this->transfer_buffer_ == nullptr) { From af8258168bb4e82a78bcd623ef72c83b59bf1a0e Mon Sep 17 00:00:00 2001 From: Sean Brogan Date: Mon, 27 Nov 2023 23:00:48 -0800 Subject: [PATCH 24/89] Add Pro Check Universal sensor support. (#5798) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/mopeka_pro_check/mopeka_pro_check.cpp | 3 ++- esphome/components/mopeka_pro_check/mopeka_pro_check.h | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/esphome/components/mopeka_pro_check/mopeka_pro_check.cpp b/esphome/components/mopeka_pro_check/mopeka_pro_check.cpp index e543ceb864..f79e40bb4e 100644 --- a/esphome/components/mopeka_pro_check/mopeka_pro_check.cpp +++ b/esphome/components/mopeka_pro_check/mopeka_pro_check.cpp @@ -54,7 +54,8 @@ bool MopekaProCheck::parse_device(const esp32_ble_tracker::ESPBTDevice &device) if (static_cast(manu_data.data[0]) != STANDARD_BOTTOM_UP && static_cast(manu_data.data[0]) != LIPPERT_BOTTOM_UP && - static_cast(manu_data.data[0]) != PLUS_BOTTOM_UP) { + static_cast(manu_data.data[0]) != PLUS_BOTTOM_UP && + static_cast(manu_data.data[0]) != PRO_UNIVERSAL) { ESP_LOGE(TAG, "Unsupported Sensor Type (0x%X)", manu_data.data[0]); return false; } diff --git a/esphome/components/mopeka_pro_check/mopeka_pro_check.h b/esphome/components/mopeka_pro_check/mopeka_pro_check.h index b5dff153e7..8b4d47e4c6 100644 --- a/esphome/components/mopeka_pro_check/mopeka_pro_check.h +++ b/esphome/components/mopeka_pro_check/mopeka_pro_check.h @@ -17,7 +17,9 @@ enum SensorType { TOP_DOWN_AIR_ABOVE = 0x04, BOTTOM_UP_WATER = 0x05, LIPPERT_BOTTOM_UP = 0x06, - PLUS_BOTTOM_UP = 0x08 + PLUS_BOTTOM_UP = 0x08, + PRO_UNIVERSAL = 0xC // Pro Check Universal + // all other values are reserved }; From 087733c2fd87746d471f75e55e9a3aadb8cbc796 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 28 Nov 2023 19:23:55 +0000 Subject: [PATCH 25/89] Bump aioesphomeapi from 19.1.2 to 19.1.7 (#5859) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 09f5c303bb..2af36b1339 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ platformio==6.1.11 # When updating platformio, also update Dockerfile esptool==4.6.2 click==8.1.7 esphome-dashboard==20231107.0 -aioesphomeapi==19.1.2 +aioesphomeapi==19.1.7 zeroconf==0.127.0 python-magic==0.4.27 From 6424f831e278e984f46da0d11953fb99613c600e Mon Sep 17 00:00:00 2001 From: Alexander Dimitrov Date: Tue, 28 Nov 2023 23:17:16 +0200 Subject: [PATCH 26/89] Pn532 non blocking scan (#5191) --- esphome/components/pn532/pn532.cpp | 58 +++++++++++++++++++++- esphome/components/pn532/pn532.h | 11 ++++ esphome/components/pn532_i2c/pn532_i2c.cpp | 23 ++++----- esphome/components/pn532_i2c/pn532_i2c.h | 1 + esphome/components/pn532_spi/pn532_spi.cpp | 46 +++++------------ esphome/components/pn532_spi/pn532_spi.h | 1 + 6 files changed, 92 insertions(+), 48 deletions(-) diff --git a/esphome/components/pn532/pn532.cpp b/esphome/components/pn532/pn532.cpp index cc28d7078b..8088e6c022 100644 --- a/esphome/components/pn532/pn532.cpp +++ b/esphome/components/pn532/pn532.cpp @@ -127,8 +127,18 @@ void PN532::loop() { if (!this->requested_read_) return; + auto ready = this->read_ready_(false); + if (ready == WOULDBLOCK) + return; + + bool success = false; std::vector read; - bool success = this->read_response(PN532_COMMAND_INLISTPASSIVETARGET, read); + + if (ready == READY) { + success = this->read_response(PN532_COMMAND_INLISTPASSIVETARGET, read); + } else { + this->send_ack_(); // abort still running InListPassiveTarget + } this->requested_read_ = false; @@ -286,12 +296,58 @@ bool PN532::read_ack_() { return matches; } +void PN532::send_ack_() { + ESP_LOGV(TAG, "Sending ACK for abort"); + this->write_data({0x00, 0x00, 0xFF, 0x00, 0xFF, 0x00}); + delay(10); +} void PN532::send_nack_() { ESP_LOGV(TAG, "Sending NACK for retransmit"); this->write_data({0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00}); delay(10); } +enum PN532ReadReady PN532::read_ready_(bool block) { + if (this->rd_ready_ == READY) { + if (block) { + this->rd_start_time_ = 0; + this->rd_ready_ = WOULDBLOCK; + } + return READY; + } + + if (!this->rd_start_time_) { + this->rd_start_time_ = millis(); + } + + while (true) { + if (this->is_read_ready()) { + this->rd_ready_ = READY; + break; + } + + if (millis() - this->rd_start_time_ > 100) { + ESP_LOGV(TAG, "Timed out waiting for readiness from PN532!"); + this->rd_ready_ = TIMEOUT; + break; + } + + if (!block) { + this->rd_ready_ = WOULDBLOCK; + break; + } + + yield(); + } + + auto rdy = this->rd_ready_; + if (block || rdy == TIMEOUT) { + this->rd_start_time_ = 0; + this->rd_ready_ = WOULDBLOCK; + } + return rdy; +} + void PN532::turn_off_rf_() { ESP_LOGV(TAG, "Turning RF field OFF"); this->write_command_({ diff --git a/esphome/components/pn532/pn532.h b/esphome/components/pn532/pn532.h index 8ae215dfd9..8194d86477 100644 --- a/esphome/components/pn532/pn532.h +++ b/esphome/components/pn532/pn532.h @@ -20,6 +20,12 @@ static const uint8_t PN532_COMMAND_INDATAEXCHANGE = 0x40; static const uint8_t PN532_COMMAND_INLISTPASSIVETARGET = 0x4A; static const uint8_t PN532_COMMAND_POWERDOWN = 0x16; +enum PN532ReadReady { + WOULDBLOCK = 0, + TIMEOUT, + READY, +}; + class PN532BinarySensor; class PN532 : public PollingComponent { @@ -54,8 +60,11 @@ class PN532 : public PollingComponent { void turn_off_rf_(); bool write_command_(const std::vector &data); bool read_ack_(); + void send_ack_(); void send_nack_(); + enum PN532ReadReady read_ready_(bool block); + virtual bool is_read_ready() = 0; virtual bool write_data(const std::vector &data) = 0; virtual bool read_data(std::vector &data, uint8_t len) = 0; virtual bool read_response(uint8_t command, std::vector &data) = 0; @@ -91,6 +100,8 @@ class PN532 : public PollingComponent { std::vector triggers_ontagremoved_; std::vector current_uid_; nfc::NdefMessage *next_task_message_to_write_; + uint32_t rd_start_time_{0}; + enum PN532ReadReady rd_ready_ { WOULDBLOCK }; enum NfcTask { READ = 0, CLEAN, diff --git a/esphome/components/pn532_i2c/pn532_i2c.cpp b/esphome/components/pn532_i2c/pn532_i2c.cpp index e7c99e94b0..b306222a21 100644 --- a/esphome/components/pn532_i2c/pn532_i2c.cpp +++ b/esphome/components/pn532_i2c/pn532_i2c.cpp @@ -12,6 +12,14 @@ namespace pn532_i2c { static const char *const TAG = "pn532_i2c"; +bool PN532I2C::is_read_ready() { + uint8_t ready; + if (!this->read_bytes_raw(&ready, 1)) { + return false; + } + return ready == 0x01; +} + bool PN532I2C::write_data(const std::vector &data) { return this->write(data.data(), data.size()) == i2c::ERROR_OK; } @@ -19,19 +27,8 @@ bool PN532I2C::write_data(const std::vector &data) { bool PN532I2C::read_data(std::vector &data, uint8_t len) { delay(1); - std::vector ready; - ready.resize(1); - uint32_t start_time = millis(); - while (true) { - if (this->read_bytes_raw(ready.data(), 1)) { - if (ready[0] == 0x01) - break; - } - - if (millis() - start_time > 100) { - ESP_LOGV(TAG, "Timed out waiting for readiness from PN532!"); - return false; - } + if (this->read_ready_(true) != pn532::PN532ReadReady::READY) { + return false; } data.resize(len + 1); diff --git a/esphome/components/pn532_i2c/pn532_i2c.h b/esphome/components/pn532_i2c/pn532_i2c.h index 95cf8eeb36..00c0df206d 100644 --- a/esphome/components/pn532_i2c/pn532_i2c.h +++ b/esphome/components/pn532_i2c/pn532_i2c.h @@ -14,6 +14,7 @@ class PN532I2C : public pn532::PN532, public i2c::I2CDevice { void dump_config() override; protected: + bool is_read_ready() override; bool write_data(const std::vector &data) override; bool read_data(std::vector &data, uint8_t len) override; bool read_response(uint8_t command, std::vector &data) override; diff --git a/esphome/components/pn532_spi/pn532_spi.cpp b/esphome/components/pn532_spi/pn532_spi.cpp index be58f265b9..d55d8161d8 100644 --- a/esphome/components/pn532_spi/pn532_spi.cpp +++ b/esphome/components/pn532_spi/pn532_spi.cpp @@ -21,6 +21,14 @@ void PN532Spi::setup() { PN532::setup(); } +bool PN532Spi::is_read_ready() { + this->enable(); + this->write_byte(0x02); + bool ready = this->read_byte() == 0x01; + this->disable(); + return ready; +} + bool PN532Spi::write_data(const std::vector &data) { this->enable(); delay(2); @@ -34,24 +42,8 @@ bool PN532Spi::write_data(const std::vector &data) { } bool PN532Spi::read_data(std::vector &data, uint8_t len) { - ESP_LOGV(TAG, "Waiting for ready byte..."); - - uint32_t start_time = millis(); - while (true) { - this->enable(); - // First byte, communication mode: Read state - this->write_byte(0x02); - bool ready = this->read_byte() == 0x01; - this->disable(); - if (ready) - break; - ESP_LOGV(TAG, "Not ready yet..."); - - if (millis() - start_time > 100) { - ESP_LOGV(TAG, "Timed out waiting for readiness from PN532!"); - return false; - } - yield(); + if (this->read_ready_(true) != pn532::PN532ReadReady::READY) { + return false; } // Read data (transmission from the PN532 to the host) @@ -72,22 +64,8 @@ bool PN532Spi::read_data(std::vector &data, uint8_t len) { bool PN532Spi::read_response(uint8_t command, std::vector &data) { ESP_LOGV(TAG, "Reading response"); - uint32_t start_time = millis(); - while (true) { - this->enable(); - // First byte, communication mode: Read state - this->write_byte(0x02); - bool ready = this->read_byte() == 0x01; - this->disable(); - if (ready) - break; - ESP_LOGV(TAG, "Not ready yet..."); - - if (millis() - start_time > 100) { - ESP_LOGV(TAG, "Timed out waiting for readiness from PN532!"); - return false; - } - yield(); + if (this->read_ready_(true) != pn532::PN532ReadReady::READY) { + return false; } this->enable(); diff --git a/esphome/components/pn532_spi/pn532_spi.h b/esphome/components/pn532_spi/pn532_spi.h index 2d8312813d..b7adca22e9 100644 --- a/esphome/components/pn532_spi/pn532_spi.h +++ b/esphome/components/pn532_spi/pn532_spi.h @@ -18,6 +18,7 @@ class PN532Spi : public pn532::PN532, void dump_config() override; protected: + bool is_read_ready() override; bool write_data(const std::vector &data) override; bool read_data(std::vector &data, uint8_t len) override; bool read_response(uint8_t command, std::vector &data) override; From 391eff8fd52b08854e5a6c55e2659c26fb96c847 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Wed, 29 Nov 2023 08:42:35 +1100 Subject: [PATCH 27/89] Add Chamberlain/HomEntry HE60R garage door opener (#5834) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/he60r/__init__.py | 1 + esphome/components/he60r/cover.py | 47 +++++ esphome/components/he60r/he60r.cpp | 265 +++++++++++++++++++++++++++ esphome/components/he60r/he60r.h | 47 +++++ tests/test4.yaml | 13 ++ 6 files changed, 374 insertions(+) create mode 100644 esphome/components/he60r/__init__.py create mode 100644 esphome/components/he60r/cover.py create mode 100644 esphome/components/he60r/he60r.cpp create mode 100644 esphome/components/he60r/he60r.h diff --git a/CODEOWNERS b/CODEOWNERS index e6bc53ee6f..de80806eac 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -124,6 +124,7 @@ esphome/components/haier/* @paveldn esphome/components/havells_solar/* @sourabhjaiswal esphome/components/hbridge/fan/* @WeekendWarrior esphome/components/hbridge/light/* @DotNetDann +esphome/components/he60r/* @clydebarrow esphome/components/heatpumpir/* @rob-deutsch esphome/components/hitachi_ac424/* @sourabhjaiswal esphome/components/hm3301/* @freekode diff --git a/esphome/components/he60r/__init__.py b/esphome/components/he60r/__init__.py new file mode 100644 index 0000000000..c58ce8a01e --- /dev/null +++ b/esphome/components/he60r/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@clydebarrow"] diff --git a/esphome/components/he60r/cover.py b/esphome/components/he60r/cover.py new file mode 100644 index 0000000000..fd4c746016 --- /dev/null +++ b/esphome/components/he60r/cover.py @@ -0,0 +1,47 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import cover, uart +from esphome.const import ( + CONF_CLOSE_DURATION, + CONF_ID, + CONF_OPEN_DURATION, +) + +he60r_ns = cg.esphome_ns.namespace("he60r") +HE60rCover = he60r_ns.class_("HE60rCover", cover.Cover, cg.Component) + +CONFIG_SCHEMA = ( + cover.COVER_SCHEMA.extend(uart.UART_DEVICE_SCHEMA) + .extend(cv.COMPONENT_SCHEMA) + .extend( + { + cv.GenerateID(): cv.declare_id(HE60rCover), + cv.Optional( + CONF_OPEN_DURATION, default="15s" + ): cv.positive_time_period_milliseconds, + cv.Optional( + CONF_CLOSE_DURATION, default="15s" + ): cv.positive_time_period_milliseconds, + } + ) +) + +FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( + "he60r", + baud_rate=1200, + require_tx=True, + require_rx=True, + data_bits=8, + parity="EVEN", + stop_bits=1, +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await cover.register_cover(var, config) + await uart.register_uart_device(var, config) + + cg.add(var.set_close_duration(config[CONF_CLOSE_DURATION])) + cg.add(var.set_open_duration(config[CONF_OPEN_DURATION])) diff --git a/esphome/components/he60r/he60r.cpp b/esphome/components/he60r/he60r.cpp new file mode 100644 index 0000000000..d6e6122b1b --- /dev/null +++ b/esphome/components/he60r/he60r.cpp @@ -0,0 +1,265 @@ +#include "he60r.h" +#include "esphome/core/hal.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace he60r { + +static const char *const TAG = "he60r.cover"; +static const uint8_t QUERY_BYTE = 0x38; +static const uint8_t TOGGLE_BYTE = 0x30; + +using namespace esphome::cover; + +void HE60rCover::setup() { + auto restore = this->restore_state_(); + + if (restore.has_value()) { + restore->apply(this); + this->publish_state(false); + } else { + // if no other information, assume half open + this->position = 0.5f; + } + this->current_operation = COVER_OPERATION_IDLE; + this->last_recompute_time_ = this->start_dir_time_ = millis(); + this->set_interval(300, [this]() { this->update_(); }); +} + +CoverTraits HE60rCover::get_traits() { + auto traits = CoverTraits(); + traits.set_supports_stop(true); + traits.set_supports_position(true); + traits.set_supports_toggle(true); + traits.set_is_assumed_state(false); + return traits; +} + +void HE60rCover::dump_config() { + LOG_COVER("", "HE60R Cover", this); + this->check_uart_settings(1200, 1, uart::UART_CONFIG_PARITY_EVEN, 8); + ESP_LOGCONFIG(TAG, " Open Duration: %.1fs", this->open_duration_ / 1e3f); + ESP_LOGCONFIG(TAG, " Close Duration: %.1fs", this->close_duration_ / 1e3f); + auto restore = this->restore_state_(); + if (restore.has_value()) + ESP_LOGCONFIG(TAG, " Saved position %d%%", (int) (restore->position * 100.f)); +} + +void HE60rCover::endstop_reached_(CoverOperation operation) { + const uint32_t now = millis(); + + this->set_current_operation_(COVER_OPERATION_IDLE); + auto new_position = operation == COVER_OPERATION_OPENING ? COVER_OPEN : COVER_CLOSED; + if (new_position != this->position || this->current_operation != COVER_OPERATION_IDLE) { + this->position = new_position; + this->current_operation = COVER_OPERATION_IDLE; + if (this->last_command_ == operation) { + float dur = (now - this->start_dir_time_) / 1e3f; + ESP_LOGD(TAG, "'%s' - %s endstop reached. Took %.1fs.", this->name_.c_str(), + operation == COVER_OPERATION_OPENING ? "Open" : "Close", dur); + } + this->publish_state(); + } +} + +void HE60rCover::set_current_operation_(cover::CoverOperation operation) { + if (this->current_operation != operation) { + this->current_operation = operation; + if (operation != COVER_OPERATION_IDLE) + this->last_recompute_time_ = millis(); + this->publish_state(); + } +} + +void HE60rCover::process_rx_(uint8_t data) { + ESP_LOGV(TAG, "Process RX data %X", data); + if (!this->query_seen_) { + this->query_seen_ = data == QUERY_BYTE; + if (!this->query_seen_) + ESP_LOGD(TAG, "RX Byte %02X", data); + return; + } + switch (data) { + case 0xB5: // at closed endstop, jammed? + case 0xF5: // at closed endstop, jammed? + case 0x55: // at closed endstop + this->next_direction_ = COVER_OPERATION_OPENING; + this->endstop_reached_(COVER_OPERATION_CLOSING); + break; + + case 0x52: // at opened endstop + this->next_direction_ = COVER_OPERATION_CLOSING; + this->endstop_reached_(COVER_OPERATION_OPENING); + break; + + case 0x51: // travelling up after encountering obstacle + case 0x01: // travelling up + case 0x11: // travelling up, triggered by remote + this->set_current_operation_(COVER_OPERATION_OPENING); + this->next_direction_ = COVER_OPERATION_IDLE; + break; + + case 0x44: // travelling down + case 0x14: // travelling down, triggered by remote + this->next_direction_ = COVER_OPERATION_IDLE; + this->set_current_operation_(COVER_OPERATION_CLOSING); + break; + + case 0x86: // Stopped, jammed? + case 0x16: // stopped midway while opening, by remote + case 0x06: // stopped midway while opening + this->next_direction_ = COVER_OPERATION_CLOSING; + this->set_current_operation_(COVER_OPERATION_IDLE); + break; + + case 0x10: // stopped midway while closing, by remote + case 0x00: // stopped midway while closing + this->next_direction_ = COVER_OPERATION_OPENING; + this->set_current_operation_(COVER_OPERATION_IDLE); + break; + + default: + break; + } +} + +void HE60rCover::update_() { + if (toggles_needed_ != 0) { + if ((this->counter_++ & 0x3) == 0) { + toggles_needed_--; + ESP_LOGD(TAG, "Writing byte 0x30, still needed=%d", toggles_needed_); + this->write_byte(TOGGLE_BYTE); + } else { + this->write_byte(QUERY_BYTE); + } + } else { + this->write_byte(QUERY_BYTE); + this->counter_ = 0; + } + if (this->current_operation != COVER_OPERATION_IDLE) { + this->recompute_position_(); + + // if we initiated the move, check if we reached the target position + if (this->last_command_ != COVER_OPERATION_IDLE) { + if (this->is_at_target_()) { + this->start_direction_(COVER_OPERATION_IDLE); + } + } + } +} + +void HE60rCover::loop() { + uint8_t data; + + while (this->available() > 0) { + if (this->read_byte(&data)) { + this->process_rx_(data); + } + } +} + +void HE60rCover::control(const CoverCall &call) { + if (call.get_stop()) { + this->start_direction_(COVER_OPERATION_IDLE); + } else if (call.get_toggle().has_value()) { + // toggle action logic: OPEN - STOP - CLOSE + if (this->last_command_ != COVER_OPERATION_IDLE) { + this->start_direction_(COVER_OPERATION_IDLE); + } else { + this->toggles_needed_++; + } + } else if (call.get_position().has_value()) { + // go to position action + auto pos = *call.get_position(); + // are we at the target? + if (pos == this->position) { + this->start_direction_(COVER_OPERATION_IDLE); + } else { + this->target_position_ = pos; + this->start_direction_(pos < this->position ? COVER_OPERATION_CLOSING : COVER_OPERATION_OPENING); + } + } +} + +/** + * Check if the cover has reached or passed the target position. This is used only + * for partial open/close requests - endstops are used for full open/close. + * @return True if the cover has reached or passed its target position. For full open/close target always return false. + */ +bool HE60rCover::is_at_target_() const { + // equality of floats is fraught with peril - this is reliable since the values are 0.0 or 1.0 which are + // exactly representable. + if (this->target_position_ == COVER_OPEN || this->target_position_ == COVER_CLOSED) + return false; + // aiming for an intermediate position - exact comparison here will not work and we need to allow for overshoot + switch (this->last_command_) { + case COVER_OPERATION_OPENING: + return this->position >= this->target_position_; + case COVER_OPERATION_CLOSING: + return this->position <= this->target_position_; + case COVER_OPERATION_IDLE: + return this->current_operation == COVER_OPERATION_IDLE; + default: + return true; + } +} +void HE60rCover::start_direction_(CoverOperation dir) { + this->last_command_ = dir; + if (this->current_operation == dir) + return; + ESP_LOGD(TAG, "'%s' - Direction '%s' requested.", this->name_.c_str(), + dir == COVER_OPERATION_OPENING ? "OPEN" + : dir == COVER_OPERATION_CLOSING ? "CLOSE" + : "STOP"); + + if (dir == this->next_direction_) { + // either moving and needs to stop, or stopped and will move correctly on one trigger + this->toggles_needed_ = 1; + } else { + if (this->current_operation == COVER_OPERATION_IDLE) { + // if stopped, but will go the wrong way, need 3 triggers. + this->toggles_needed_ = 3; + } else { + // just stop and reverse + this->toggles_needed_ = 2; + } + ESP_LOGD(TAG, "'%s' - Reversing direction.", this->name_.c_str()); + } + this->start_dir_time_ = millis(); +} + +void HE60rCover::recompute_position_() { + if (this->current_operation == COVER_OPERATION_IDLE) + return; + + const uint32_t now = millis(); + float dir; + float action_dur; + + switch (this->current_operation) { + case COVER_OPERATION_OPENING: + dir = 1.0f; + action_dur = this->open_duration_; + break; + case COVER_OPERATION_CLOSING: + dir = -1.0f; + action_dur = this->close_duration_; + break; + default: + return; + } + + if (now > this->last_recompute_time_) { + auto diff = now - last_recompute_time_; + auto delta = dir * diff / action_dur; + // make sure our guesstimate never reaches full open or close. + this->position = clamp(delta + this->position, COVER_CLOSED + 0.01f, COVER_OPEN - 0.01f); + ESP_LOGD(TAG, "Recompute %dms, dir=%f, action_dur=%f, delta=%f, pos=%f", (int) diff, dir, action_dur, delta, + this->position); + this->last_recompute_time_ = now; + this->publish_state(); + } +} + +} // namespace he60r +} // namespace esphome diff --git a/esphome/components/he60r/he60r.h b/esphome/components/he60r/he60r.h new file mode 100644 index 0000000000..624b61fc65 --- /dev/null +++ b/esphome/components/he60r/he60r.h @@ -0,0 +1,47 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "esphome/components/uart/uart.h" +#include "esphome/components/cover/cover.h" + +namespace esphome { +namespace he60r { + +class HE60rCover : public cover::Cover, public Component, public uart::UARTDevice { + public: + void setup() override; + void loop() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; }; + + void set_open_duration(uint32_t duration) { this->open_duration_ = duration; } + void set_close_duration(uint32_t duration) { this->close_duration_ = duration; } + + cover::CoverTraits get_traits() override; + + protected: + void update_(); + void control(const cover::CoverCall &call) override; + bool is_at_target_() const; + void start_direction_(cover::CoverOperation dir); + void update_operation_(cover::CoverOperation dir); + void endstop_reached_(cover::CoverOperation operation); + void recompute_position_(); + void set_current_operation_(cover::CoverOperation operation); + void process_rx_(uint8_t data); + + uint32_t open_duration_{0}; + uint32_t close_duration_{0}; + uint32_t toggles_needed_{0}; + cover::CoverOperation next_direction_{cover::COVER_OPERATION_IDLE}; + cover::CoverOperation last_command_{cover::COVER_OPERATION_IDLE}; + uint32_t last_recompute_time_{0}; + uint32_t start_dir_time_{0}; + float target_position_{0}; + bool query_seen_{}; + uint8_t counter_{}; +}; + +} // namespace he60r +} // namespace esphome diff --git a/tests/test4.yaml b/tests/test4.yaml index 69d9211969..65aab7cdde 100644 --- a/tests/test4.yaml +++ b/tests/test4.yaml @@ -58,6 +58,12 @@ uart: tx_pin: GPIO22 rx_pin: GPIO23 baud_rate: 9600 + - id: uart_he60r + tx_pin: 22 + rx_pin: 23 + baud_rate: 1200 + parity: EVEN + ota: safe_mode: true @@ -528,6 +534,13 @@ cover: - platform: copy source_id: tuya_cover name: Tuya Cover copy + - platform: he60r + uart_id: uart_he60r + id: garage_door + name: Garage Door + open_duration: 14s + close_duration: 14s + display: - platform: addressable_light From 6cf4412e7b5210c3f5fadd960f1f99179682b8b6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 30 Nov 2023 20:49:42 -1000 Subject: [PATCH 28/89] Bump aioesphomeapi from 19.1.7 to 19.2.1 (#5863) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 2af36b1339..574b1c0a69 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ platformio==6.1.11 # When updating platformio, also update Dockerfile esptool==4.6.2 click==8.1.7 esphome-dashboard==20231107.0 -aioesphomeapi==19.1.7 +aioesphomeapi==19.2.1 zeroconf==0.127.0 python-magic==0.4.27 From 782854ab365ba4c50dc29a228b73f0d63a421ef1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 30 Nov 2023 20:51:14 -1000 Subject: [PATCH 29/89] Bump tornado from 6.3.3 to 6.4 (#5862) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 574b1c0a69..841c5a97cd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ voluptuous==0.13.1 PyYAML==6.0.1 paho-mqtt==1.6.1 colorama==0.4.6 -tornado==6.3.3 +tornado==6.4 tzlocal==5.2 # from time tzdata>=2021.1 # from time pyserial==3.5 From 788f1b60e2449ebf1ebc84884ab8570bd6eec9c6 Mon Sep 17 00:00:00 2001 From: Samuel Sieb Date: Sat, 2 Dec 2023 22:58:25 -0800 Subject: [PATCH 30/89] add missing ifdef to pvvx_mithermometer (#5880) --- esphome/components/pvvx_mithermometer/display/pvvx_display.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/pvvx_mithermometer/display/pvvx_display.cpp b/esphome/components/pvvx_mithermometer/display/pvvx_display.cpp index fc200f7d71..d192e62430 100644 --- a/esphome/components/pvvx_mithermometer/display/pvvx_display.cpp +++ b/esphome/components/pvvx_mithermometer/display/pvvx_display.cpp @@ -13,7 +13,9 @@ void PVVXDisplay::dump_config() { ESP_LOGCONFIG(TAG, " Service UUID : %s", this->service_uuid_.to_string().c_str()); ESP_LOGCONFIG(TAG, " Characteristic UUID : %s", this->char_uuid_.to_string().c_str()); ESP_LOGCONFIG(TAG, " Auto clear : %s", YESNO(this->auto_clear_enabled_)); +#ifdef USE_TIME ESP_LOGCONFIG(TAG, " Set time on connection: %s", YESNO(this->time_ != nullptr)); +#endif ESP_LOGCONFIG(TAG, " Disconnect delay : %" PRIu32 "ms", this->disconnect_delay_ms_); LOG_UPDATE_INTERVAL(this); } From e271faa5441bcfef99c30d4ef1c864db8556027e Mon Sep 17 00:00:00 2001 From: Mike La Spina Date: Mon, 4 Dec 2023 17:15:01 -0600 Subject: [PATCH 31/89] Fix un-initialized version string (#5865) --- esphome/components/ld2420/ld2420.h | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/esphome/components/ld2420/ld2420.h b/esphome/components/ld2420/ld2420.h index 2780503776..2b50c7a1d4 100644 --- a/esphome/components/ld2420/ld2420.h +++ b/esphome/components/ld2420/ld2420.h @@ -255,12 +255,11 @@ class LD2420Component : public Component, public uart::UARTDevice { uint16_t gate_energy_[LD2420_TOTAL_GATES]; CmdReplyT cmd_reply_; - uint32_t timeout_; uint32_t max_distance_gate_; uint32_t min_distance_gate_; uint16_t system_mode_{CMD_SYSTEM_MODE_ENERGY}; bool cmd_active_{false}; - char ld2420_firmware_ver_[8]; + char ld2420_firmware_ver_[8]{"v0.0.0"}; bool presence_{false}; bool calibration_{false}; uint16_t distance_{0}; From 29dcc4031f9cc0d6b1044003da1ac72bc7e71c93 Mon Sep 17 00:00:00 2001 From: Samuel Sieb Date: Mon, 4 Dec 2023 15:41:33 -0800 Subject: [PATCH 32/89] fix a01nyub data reading (#5882) Co-authored-by: Samuel Sieb --- esphome/components/a01nyub/a01nyub.cpp | 49 ++++++++++---------------- 1 file changed, 18 insertions(+), 31 deletions(-) diff --git a/esphome/components/a01nyub/a01nyub.cpp b/esphome/components/a01nyub/a01nyub.cpp index 75cb276f84..d0bc89a0c9 100644 --- a/esphome/components/a01nyub/a01nyub.cpp +++ b/esphome/components/a01nyub/a01nyub.cpp @@ -8,50 +8,37 @@ namespace esphome { namespace a01nyub { static const char *const TAG = "a01nyub.sensor"; -static const uint8_t MAX_DATA_LENGTH_BYTES = 4; void A01nyubComponent::loop() { uint8_t data; while (this->available() > 0) { - if (this->read_byte(&data)) { - buffer_.push_back(data); + this->read_byte(&data); + if (this->buffer_.empty() && (data != 0xff)) + continue; + buffer_.push_back(data); + if (this->buffer_.size() == 4) this->check_buffer_(); - } } } void A01nyubComponent::check_buffer_() { - if (this->buffer_.size() >= MAX_DATA_LENGTH_BYTES) { - size_t i; - for (i = 0; i < this->buffer_.size(); i++) { - // Look for the first packet - if (this->buffer_[i] == 0xFF) { - if (i + 1 + 3 < this->buffer_.size()) { // Packet is not complete - return; // Wait for completion - } - - uint8_t checksum = (this->buffer_[i] + this->buffer_[i + 1] + this->buffer_[i + 2]) & 0xFF; - if (this->buffer_[i + 3] == checksum) { - float distance = (this->buffer_[i + 1] << 8) + this->buffer_[i + 2]; - if (distance > 280) { - float meters = distance / 1000.0; - ESP_LOGV(TAG, "Distance from sensor: %f mm, %f m", distance, meters); - this->publish_state(meters); - } else { - ESP_LOGW(TAG, "Invalid data read from sensor: %s", format_hex_pretty(this->buffer_).c_str()); - } - } - break; - } + uint8_t checksum = this->buffer_[0] + this->buffer_[1] + this->buffer_[2]; + if (this->buffer_[3] == checksum) { + float distance = (this->buffer_[1] << 8) + this->buffer_[2]; + if (distance > 280) { + float meters = distance / 1000.0; + ESP_LOGV(TAG, "Distance from sensor: %f mm, %f m", distance, meters); + this->publish_state(meters); + } else { + ESP_LOGW(TAG, "Invalid data read from sensor: %s", format_hex_pretty(this->buffer_).c_str()); } - this->buffer_.clear(); + } else { + ESP_LOGW(TAG, "checksum failed: %02x != %02x", checksum, this->buffer_[3]); } + this->buffer_.clear(); } -void A01nyubComponent::dump_config() { - ESP_LOGCONFIG(TAG, "A01nyub Sensor:"); - LOG_SENSOR(" ", "Distance", this); -} +void A01nyubComponent::dump_config() { LOG_SENSOR("", "A01nyub Sensor", this); } } // namespace a01nyub } // namespace esphome From 0c71685d55b8af17fd7a8fa95e7cd3ee8e99af27 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 5 Dec 2023 08:42:02 +0900 Subject: [PATCH 33/89] Bump pytest-asyncio from 0.21.1 to 0.23.2 (#5888) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index d2ce98cc8c..c4fa12d0e7 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -8,7 +8,7 @@ pre-commit pytest==7.4.3 pytest-cov==4.1.0 pytest-mock==3.12.0 -pytest-asyncio==0.21.1 +pytest-asyncio==0.23.2 asyncmock==0.4.2 hypothesis==5.49.0 From c2183eb7f07bb5a398359f4c2ebfd7c05a45f6b1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 5 Dec 2023 08:42:19 +0900 Subject: [PATCH 34/89] Bump zeroconf from 0.127.0 to 0.128.0 (#5889) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 841c5a97cd..7261972e01 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,7 +11,7 @@ esptool==4.6.2 click==8.1.7 esphome-dashboard==20231107.0 aioesphomeapi==19.2.1 -zeroconf==0.127.0 +zeroconf==0.128.0 python-magic==0.4.27 # esp-idf requires this, but doesn't bundle it by default From 2a740963bad3c27141bb429e361ef141a8dc68d3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 5 Dec 2023 08:42:37 +0900 Subject: [PATCH 35/89] Bump pylint from 2.17.6 to 3.0.2 (#5592) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index c4fa12d0e7..cb96f79587 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,4 +1,4 @@ -pylint==2.17.6 +pylint==3.0.2 flake8==6.1.0 # also change in .pre-commit-config.yaml when updating black==23.11.0 # also change in .pre-commit-config.yaml when updating pyupgrade==3.15.0 # also change in .pre-commit-config.yaml when updating From 657a7070cbbcbf6fa0c61cf271056e263229a578 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 5 Dec 2023 08:43:07 +0900 Subject: [PATCH 36/89] Bump voluptuous from 0.13.1 to 0.14.1 (#5784) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 7261972e01..7d37954656 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -voluptuous==0.13.1 +voluptuous==0.14.1 PyYAML==6.0.1 paho-mqtt==1.6.1 colorama==0.4.6 From b8fe4f8d56f1d3590d7d2e132d211837a52c27fc Mon Sep 17 00:00:00 2001 From: Fabio Pugliese Ornellas Date: Mon, 4 Dec 2023 23:50:01 +0000 Subject: [PATCH 37/89] Security improvement: Support wifi ap_timeout=0s (disable) (#5887) --- esphome/components/wifi/wifi_component.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index d023405728..519489097a 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -164,7 +164,7 @@ void WiFiComponent::loop() { #ifdef USE_WIFI_AP if (this->has_ap() && !this->ap_setup_) { - if (now - this->last_connected_ > this->ap_timeout_) { + if (this->ap_timeout_ != 0 && (now - this->last_connected_ > this->ap_timeout_)) { ESP_LOGI(TAG, "Starting fallback AP!"); this->setup_ap_config_(); #ifdef USE_CAPTIVE_PORTAL From df5394d51c73748b1d545a629c61e3787469ee97 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Tue, 5 Dec 2023 10:52:02 +1100 Subject: [PATCH 38/89] Suppress full config output of "esphome config" when -q option is used. (#5852) --- esphome/__main__.py | 3 ++- esphome/core/__init__.py | 2 ++ esphome/log.py | 1 + 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index e5456cf8e5..0796dead43 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -389,7 +389,8 @@ def command_config(args, config): output = re.sub( r"(password|key|psk|ssid)\: (.+)", r"\1: \\033[5m\2\\033[6m", output ) - safe_print(output) + if not CORE.quiet: + safe_print(output) _LOGGER.info("Configuration is valid!") return 0 diff --git a/esphome/core/__init__.py b/esphome/core/__init__.py index 60bd17b481..3c3b27a79d 100644 --- a/esphome/core/__init__.py +++ b/esphome/core/__init__.py @@ -522,6 +522,8 @@ class EsphomeCore: self.component_ids = set() # Whether ESPHome was started in verbose mode self.verbose = False + # Whether ESPHome was started in quiet mode + self.quiet = False def reset(self): self.dashboard = False diff --git a/esphome/log.py b/esphome/log.py index b5d72e774c..23dc453d32 100644 --- a/esphome/log.py +++ b/esphome/log.py @@ -78,6 +78,7 @@ def setup_log( CORE.verbose = True elif quiet: log_level = logging.CRITICAL + CORE.quiet = True else: log_level = logging.INFO logging.basicConfig(level=log_level) From d9792b0d923ef640019db418b21e78993b7be312 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Tue, 5 Dec 2023 10:56:53 +1100 Subject: [PATCH 39/89] Checks for pins used in multiple places (#5666) --- esphome/components/adc/__init__.py | 26 +- esphome/components/esp32/gpio.py | 37 +- esphome/components/esp8266/gpio.py | 27 +- esphome/components/host/gpio.py | 21 +- esphome/components/i2c/__init__.py | 5 +- esphome/components/libretiny/gpio.py | 24 +- esphome/components/max6956/__init__.py | 18 +- esphome/components/mcp23016/__init__.py | 18 +- esphome/components/mcp23xxx_base/__init__.py | 20 +- esphome/components/pca9554/__init__.py | 19 +- esphome/components/pcf8574/__init__.py | 18 +- esphome/components/rp2040/gpio.py | 22 +- esphome/components/sn74hc595/__init__.py | 22 +- esphome/config.py | 48 +- esphome/const.py | 1 + esphome/core/__init__.py | 3 + esphome/pins.py | 185 ++++++- esphome/util.py | 26 - tests/test1.1.yaml | 20 +- tests/test1.yaml | 539 ++++++++++++++----- tests/test11.5.yaml | 93 +++- tests/test2.yaml | 76 ++- tests/test3.1.yaml | 121 +++-- tests/test3.yaml | 169 ++++-- tests/test4.yaml | 289 +++++++--- tests/test5.yaml | 54 +- tests/test8.yaml | 9 +- 27 files changed, 1335 insertions(+), 575 deletions(-) diff --git a/esphome/components/adc/__init__.py b/esphome/components/adc/__init__.py index bad5cf74ef..952fbdd9b9 100644 --- a/esphome/components/adc/__init__.py +++ b/esphome/components/adc/__init__.py @@ -1,7 +1,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import pins -from esphome.const import CONF_ANALOG, CONF_INPUT +from esphome.const import CONF_ANALOG, CONF_INPUT, CONF_NUMBER from esphome.core import CORE from esphome.components.esp32 import get_esp32_variant @@ -152,7 +152,8 @@ def validate_adc_pin(value): return cv.only_on_rp2040("TEMPERATURE") if CORE.is_esp32: - value = pins.internal_gpio_input_pin_number(value) + conf = pins.internal_gpio_input_pin_schema(value) + value = conf[CONF_NUMBER] variant = get_esp32_variant() if ( variant not in ESP32_VARIANT_ADC1_PIN_TO_CHANNEL @@ -166,24 +167,23 @@ def validate_adc_pin(value): ): raise cv.Invalid(f"{variant} doesn't support ADC on this pin") - return pins.internal_gpio_input_pin_schema(value) + return conf if CORE.is_esp8266: - value = pins.internal_gpio_pin_number({CONF_ANALOG: True, CONF_INPUT: True})( - value - ) - - if value != 17: # A0 - raise cv.Invalid("ESP8266: Only pin A0 (GPIO17) supports ADC") - return pins.gpio_pin_schema( + conf = pins.gpio_pin_schema( {CONF_ANALOG: True, CONF_INPUT: True}, internal=True )(value) + if conf[CONF_NUMBER] != 17: # A0 + raise cv.Invalid("ESP8266: Only pin A0 (GPIO17) supports ADC") + return conf + if CORE.is_rp2040: - value = pins.internal_gpio_input_pin_number(value) - if value not in (26, 27, 28, 29): + conf = pins.internal_gpio_input_pin_schema(value) + number = conf[CONF_NUMBER] + if number not in (26, 27, 28, 29): raise cv.Invalid("RP2040: Only pins 26, 27, 28 and 29 support ADC") - return pins.internal_gpio_input_pin_schema(value) + return conf if CORE.is_libretiny: return pins.gpio_pin_schema( diff --git a/esphome/components/esp32/gpio.py b/esphome/components/esp32/gpio.py index a53649e3e4..16f99f2b15 100644 --- a/esphome/components/esp32/gpio.py +++ b/esphome/components/esp32/gpio.py @@ -3,15 +3,13 @@ from typing import Any from esphome.const import ( CONF_ID, - CONF_INPUT, CONF_INVERTED, CONF_MODE, CONF_NUMBER, CONF_OPEN_DRAIN, CONF_OUTPUT, - CONF_PULLDOWN, - CONF_PULLUP, CONF_IGNORE_STRAPPING_WARNING, + PLATFORM_ESP32, ) from esphome import pins from esphome.core import CORE @@ -33,7 +31,6 @@ from .const import ( esp32_ns, ) - from .gpio_esp32 import esp32_validate_gpio_pin, esp32_validate_supports from .gpio_esp32_s2 import esp32_s2_validate_gpio_pin, esp32_s2_validate_supports from .gpio_esp32_c3 import esp32_c3_validate_gpio_pin, esp32_c3_validate_supports @@ -42,7 +39,6 @@ from .gpio_esp32_c2 import esp32_c2_validate_gpio_pin, esp32_c2_validate_support from .gpio_esp32_c6 import esp32_c6_validate_gpio_pin, esp32_c6_validate_supports from .gpio_esp32_h2 import esp32_h2_validate_gpio_pin, esp32_h2_validate_supports - ESP32InternalGPIOPin = esp32_ns.class_("ESP32InternalGPIOPin", cg.InternalGPIOPin) @@ -161,33 +157,22 @@ DRIVE_STRENGTHS = { } gpio_num_t = cg.global_ns.enum("gpio_num_t") - CONF_DRIVE_STRENGTH = "drive_strength" ESP32_PIN_SCHEMA = cv.All( - { - cv.GenerateID(): cv.declare_id(ESP32InternalGPIOPin), - cv.Required(CONF_NUMBER): validate_gpio_pin, - cv.Optional(CONF_MODE, default={}): cv.Schema( - { - cv.Optional(CONF_INPUT, default=False): cv.boolean, - cv.Optional(CONF_OUTPUT, default=False): cv.boolean, - cv.Optional(CONF_OPEN_DRAIN, default=False): cv.boolean, - cv.Optional(CONF_PULLUP, default=False): cv.boolean, - cv.Optional(CONF_PULLDOWN, default=False): cv.boolean, - } - ), - cv.Optional(CONF_INVERTED, default=False): cv.boolean, - cv.Optional(CONF_IGNORE_STRAPPING_WARNING, default=False): cv.boolean, - cv.Optional(CONF_DRIVE_STRENGTH, default="20mA"): cv.All( - cv.float_with_unit("current", "mA", optional_unit=True), - cv.enum(DRIVE_STRENGTHS), - ), - }, + pins.gpio_base_schema(ESP32InternalGPIOPin, validate_gpio_pin).extend( + { + cv.Optional(CONF_IGNORE_STRAPPING_WARNING, default=False): cv.boolean, + cv.Optional(CONF_DRIVE_STRENGTH, default="20mA"): cv.All( + cv.float_with_unit("current", "mA", optional_unit=True), + cv.enum(DRIVE_STRENGTHS), + ), + } + ), validate_supports, ) -@pins.PIN_SCHEMA_REGISTRY.register("esp32", ESP32_PIN_SCHEMA) +@pins.PIN_SCHEMA_REGISTRY.register(PLATFORM_ESP32, ESP32_PIN_SCHEMA) async def esp32_pin_to_code(config): var = cg.new_Pvariable(config[CONF_ID]) num = config[CONF_NUMBER] diff --git a/esphome/components/esp8266/gpio.py b/esphome/components/esp8266/gpio.py index e75578cc16..c42bc9204f 100644 --- a/esphome/components/esp8266/gpio.py +++ b/esphome/components/esp8266/gpio.py @@ -12,6 +12,7 @@ from esphome.const import ( CONF_OUTPUT, CONF_PULLDOWN, CONF_PULLUP, + PLATFORM_ESP8266, ) from esphome import pins from esphome.core import CORE, coroutine_with_priority @@ -21,10 +22,8 @@ import esphome.codegen as cg from . import boards from .const import KEY_BOARD, KEY_ESP8266, KEY_PIN_INITIAL_STATES, esp8266_ns - _LOGGER = logging.getLogger(__name__) - ESP8266GPIOPin = esp8266_ns.class_("ESP8266GPIOPin", cg.InternalGPIOPin) @@ -124,6 +123,8 @@ def validate_supports(value): (True, False, False, False, False), # OUTPUT (False, True, False, False, False), + # INPUT and OUTPUT, e.g. for i2c + (True, True, False, False, False), # INPUT_PULLUP (True, False, False, True, False), # INPUT_PULLDOWN_16 @@ -142,21 +143,11 @@ def validate_supports(value): ESP8266_PIN_SCHEMA = cv.All( - { - cv.GenerateID(): cv.declare_id(ESP8266GPIOPin), - cv.Required(CONF_NUMBER): validate_gpio_pin, - cv.Optional(CONF_MODE, default={}): cv.Schema( - { - cv.Optional(CONF_ANALOG, default=False): cv.boolean, - cv.Optional(CONF_INPUT, default=False): cv.boolean, - cv.Optional(CONF_OUTPUT, default=False): cv.boolean, - cv.Optional(CONF_OPEN_DRAIN, default=False): cv.boolean, - cv.Optional(CONF_PULLUP, default=False): cv.boolean, - cv.Optional(CONF_PULLDOWN, default=False): cv.boolean, - } - ), - cv.Optional(CONF_INVERTED, default=False): cv.boolean, - }, + pins.gpio_base_schema( + ESP8266GPIOPin, + validate_gpio_pin, + modes=pins.GPIO_STANDARD_MODES + (CONF_ANALOG,), + ), validate_supports, ) @@ -167,7 +158,7 @@ class PinInitialState: level: int = 255 -@pins.PIN_SCHEMA_REGISTRY.register("esp8266", ESP8266_PIN_SCHEMA) +@pins.PIN_SCHEMA_REGISTRY.register(PLATFORM_ESP8266, ESP8266_PIN_SCHEMA) async def esp8266_pin_to_code(config): var = cg.new_Pvariable(config[CONF_ID]) num = config[CONF_NUMBER] diff --git a/esphome/components/host/gpio.py b/esphome/components/host/gpio.py index d523d28ee5..180919de4f 100644 --- a/esphome/components/host/gpio.py +++ b/esphome/components/host/gpio.py @@ -17,10 +17,8 @@ import esphome.codegen as cg from .const import host_ns - _LOGGER = logging.getLogger(__name__) - HostGPIOPin = host_ns.class_("HostGPIOPin", cg.InternalGPIOPin) @@ -45,21 +43,10 @@ def validate_gpio_pin(value): return _translate_pin(value) -HOST_PIN_SCHEMA = cv.All( - { - cv.GenerateID(): cv.declare_id(HostGPIOPin), - cv.Required(CONF_NUMBER): validate_gpio_pin, - cv.Optional(CONF_MODE, default={}): cv.Schema( - { - cv.Optional(CONF_INPUT, default=False): cv.boolean, - cv.Optional(CONF_OUTPUT, default=False): cv.boolean, - cv.Optional(CONF_OPEN_DRAIN, default=False): cv.boolean, - cv.Optional(CONF_PULLUP, default=False): cv.boolean, - cv.Optional(CONF_PULLDOWN, default=False): cv.boolean, - } - ), - cv.Optional(CONF_INVERTED, default=False): cv.boolean, - }, +HOST_PIN_SCHEMA = pins.gpio_base_schema( + HostGPIOPin, + validate_gpio_pin, + modes=[CONF_INPUT, CONF_OUTPUT, CONF_OPEN_DRAIN, CONF_PULLUP, CONF_PULLDOWN], ) diff --git a/esphome/components/i2c/__init__.py b/esphome/components/i2c/__init__.py index 676190b0e5..0a1f049b93 100644 --- a/esphome/components/i2c/__init__.py +++ b/esphome/components/i2c/__init__.py @@ -39,9 +39,8 @@ def _bus_declare_type(value): raise NotImplementedError -pin_with_input_and_output_support = cv.All( - pins.internal_gpio_pin_number({CONF_INPUT: True}), - pins.internal_gpio_pin_number({CONF_OUTPUT: True}), +pin_with_input_and_output_support = pins.internal_gpio_pin_number( + {CONF_OUTPUT: True, CONF_INPUT: True} ) diff --git a/esphome/components/libretiny/gpio.py b/esphome/components/libretiny/gpio.py index ba9bfffcc9..1d7b37cc9b 100644 --- a/esphome/components/libretiny/gpio.py +++ b/esphome/components/libretiny/gpio.py @@ -186,25 +186,11 @@ def validate_gpio_usage(value): return value -BASE_PIN_SCHEMA = cv.Schema( - { - cv.GenerateID(): cv.declare_id(ArduinoInternalGPIOPin), - cv.Required(CONF_NUMBER): validate_gpio_pin, - cv.Optional(CONF_MODE, default={}): cv.Schema( - { - cv.Optional(CONF_ANALOG, default=False): cv.boolean, - cv.Optional(CONF_INPUT, default=False): cv.boolean, - cv.Optional(CONF_OUTPUT, default=False): cv.boolean, - cv.Optional(CONF_OPEN_DRAIN, default=False): cv.boolean, - cv.Optional(CONF_PULLUP, default=False): cv.boolean, - cv.Optional(CONF_PULLDOWN, default=False): cv.boolean, - } - ), - cv.Optional(CONF_INVERTED, default=False): cv.boolean, - }, -) - -BASE_PIN_SCHEMA.add_extra(validate_gpio_usage) +BASE_PIN_SCHEMA = pins.gpio_base_schema( + ArduinoInternalGPIOPin, + validate_gpio_pin, + modes=pins.GPIO_STANDARD_MODES + (CONF_ANALOG,), +).add_extra(validate_gpio_usage) async def component_pin_to_code(config): diff --git a/esphome/components/max6956/__init__.py b/esphome/components/max6956/__init__.py index 77e0d37e76..bb71dba8bf 100644 --- a/esphome/components/max6956/__init__.py +++ b/esphome/components/max6956/__init__.py @@ -74,20 +74,14 @@ def validate_mode(value): CONF_MAX6956 = "max6956" -MAX6956_PIN_SCHEMA = cv.All( +MAX6956_PIN_SCHEMA = pins.gpio_base_schema( + MAX6956GPIOPin, + cv.int_range(min=4, max=31), + modes=[CONF_INPUT, CONF_PULLUP, CONF_OUTPUT], + mode_validator=validate_mode, +).extend( { - cv.GenerateID(): cv.declare_id(MAX6956GPIOPin), cv.Required(CONF_MAX6956): cv.use_id(MAX6956), - cv.Required(CONF_NUMBER): cv.int_range(min=4, max=31), - cv.Optional(CONF_MODE, default={}): cv.All( - { - cv.Optional(CONF_INPUT, default=False): cv.boolean, - cv.Optional(CONF_PULLUP, default=False): cv.boolean, - cv.Optional(CONF_OUTPUT, default=False): cv.boolean, - }, - validate_mode, - ), - cv.Optional(CONF_INVERTED, default=False): cv.boolean, } ) diff --git a/esphome/components/mcp23016/__init__.py b/esphome/components/mcp23016/__init__.py index c1209a9627..55722e3ae0 100644 --- a/esphome/components/mcp23016/__init__.py +++ b/esphome/components/mcp23016/__init__.py @@ -45,19 +45,15 @@ def validate_mode(value): CONF_MCP23016 = "mcp23016" -MCP23016_PIN_SCHEMA = cv.All( +MCP23016_PIN_SCHEMA = pins.gpio_base_schema( + MCP23016GPIOPin, + cv.int_range(min=0, max=15), + modes=[CONF_INPUT, CONF_OUTPUT], + mode_validator=validate_mode, + invertable=True, +).extend( { - cv.GenerateID(): cv.declare_id(MCP23016GPIOPin), cv.Required(CONF_MCP23016): cv.use_id(MCP23016), - cv.Required(CONF_NUMBER): cv.int_range(min=0, max=15), - cv.Optional(CONF_MODE, default={}): cv.All( - { - cv.Optional(CONF_INPUT, default=False): cv.boolean, - cv.Optional(CONF_OUTPUT, default=False): cv.boolean, - }, - validate_mode, - ), - cv.Optional(CONF_INVERTED, default=False): cv.boolean, } ) diff --git a/esphome/components/mcp23xxx_base/__init__.py b/esphome/components/mcp23xxx_base/__init__.py index 7bcd5c84fc..1e41a8ddff 100644 --- a/esphome/components/mcp23xxx_base/__init__.py +++ b/esphome/components/mcp23xxx_base/__init__.py @@ -54,20 +54,16 @@ def validate_mode(value): CONF_MCP23XXX = "mcp23xxx" -MCP23XXX_PIN_SCHEMA = cv.All( + +MCP23XXX_PIN_SCHEMA = pins.gpio_base_schema( + MCP23XXXGPIOPin, + cv.int_range(min=0, max=15), + modes=[CONF_INPUT, CONF_OUTPUT, CONF_PULLUP], + mode_validator=validate_mode, + invertable=True, +).extend( { - cv.GenerateID(): cv.declare_id(MCP23XXXGPIOPin), cv.Required(CONF_MCP23XXX): cv.use_id(MCP23XXXBase), - cv.Required(CONF_NUMBER): cv.int_range(min=0, max=15), - cv.Optional(CONF_MODE, default={}): cv.All( - { - cv.Optional(CONF_INPUT, default=False): cv.boolean, - cv.Optional(CONF_PULLUP, default=False): cv.boolean, - cv.Optional(CONF_OUTPUT, default=False): cv.boolean, - }, - validate_mode, - ), - cv.Optional(CONF_INVERTED, default=False): cv.boolean, cv.Optional(CONF_INTERRUPT, default="NO_INTERRUPT"): cv.enum( MCP23XXX_INTERRUPT_MODES, upper=True ), diff --git a/esphome/components/pca9554/__init__.py b/esphome/components/pca9554/__init__.py index fd52fafc5d..da31dbd9d9 100644 --- a/esphome/components/pca9554/__init__.py +++ b/esphome/components/pca9554/__init__.py @@ -52,20 +52,15 @@ def validate_mode(value): return value -PCA9554_PIN_SCHEMA = cv.All( +PCA9554_PIN_SCHEMA = pins.gpio_base_schema( + PCA9554GPIOPin, + cv.int_range(min=0, max=15), + modes=[CONF_INPUT, CONF_OUTPUT], + mode_validator=validate_mode, +).extend( { - cv.GenerateID(): cv.declare_id(PCA9554GPIOPin), cv.Required(CONF_PCA9554): cv.use_id(PCA9554Component), - cv.Required(CONF_NUMBER): cv.int_range(min=0, max=15), - cv.Optional(CONF_MODE, default={}): cv.All( - { - cv.Optional(CONF_INPUT, default=False): cv.boolean, - cv.Optional(CONF_OUTPUT, default=False): cv.boolean, - }, - validate_mode, - ), - cv.Optional(CONF_INVERTED, default=False): cv.boolean, - }, + } ) diff --git a/esphome/components/pcf8574/__init__.py b/esphome/components/pcf8574/__init__.py index d44ac28364..ebf112b85b 100644 --- a/esphome/components/pcf8574/__init__.py +++ b/esphome/components/pcf8574/__init__.py @@ -48,19 +48,15 @@ def validate_mode(value): return value -PCF8574_PIN_SCHEMA = cv.All( +PCF8574_PIN_SCHEMA = pins.gpio_base_schema( + PCF8574GPIOPin, + cv.int_range(min=0, max=17), + modes=[CONF_INPUT, CONF_OUTPUT], + mode_validator=validate_mode, + invertable=True, +).extend( { - cv.GenerateID(): cv.declare_id(PCF8574GPIOPin), cv.Required(CONF_PCF8574): cv.use_id(PCF8574Component), - cv.Required(CONF_NUMBER): cv.int_range(min=0, max=17), - cv.Optional(CONF_MODE, default={}): cv.All( - { - cv.Optional(CONF_INPUT, default=False): cv.boolean, - cv.Optional(CONF_OUTPUT, default=False): cv.boolean, - }, - validate_mode, - ), - cv.Optional(CONF_INVERTED, default=False): cv.boolean, } ) diff --git a/esphome/components/rp2040/gpio.py b/esphome/components/rp2040/gpio.py index 4823a6d22a..6ba0975a2c 100644 --- a/esphome/components/rp2040/gpio.py +++ b/esphome/components/rp2040/gpio.py @@ -1,7 +1,6 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import ( - CONF_ANALOG, CONF_ID, CONF_INPUT, CONF_INVERTED, @@ -11,6 +10,7 @@ from esphome.const import ( CONF_OUTPUT, CONF_PULLDOWN, CONF_PULLUP, + CONF_ANALOG, ) from esphome.core import CORE from esphome import pins @@ -78,22 +78,10 @@ def validate_supports(value): RP2040_PIN_SCHEMA = cv.All( - cv.Schema( - { - cv.GenerateID(): cv.declare_id(RP2040GPIOPin), - cv.Required(CONF_NUMBER): validate_gpio_pin, - cv.Optional(CONF_MODE, default={}): cv.Schema( - { - cv.Optional(CONF_ANALOG, default=False): cv.boolean, - cv.Optional(CONF_INPUT, default=False): cv.boolean, - cv.Optional(CONF_OUTPUT, default=False): cv.boolean, - cv.Optional(CONF_OPEN_DRAIN, default=False): cv.boolean, - cv.Optional(CONF_PULLUP, default=False): cv.boolean, - cv.Optional(CONF_PULLDOWN, default=False): cv.boolean, - } - ), - cv.Optional(CONF_INVERTED, default=False): cv.boolean, - } + pins.gpio_base_schema( + RP2040GPIOPin, + validate_gpio_pin, + modes=pins.GPIO_STANDARD_MODES + (CONF_ANALOG,), ), validate_supports, ) diff --git a/esphome/components/sn74hc595/__init__.py b/esphome/components/sn74hc595/__init__.py index e7ba45175c..11a6747656 100644 --- a/esphome/components/sn74hc595/__init__.py +++ b/esphome/components/sn74hc595/__init__.py @@ -5,7 +5,6 @@ from esphome.components import spi from esphome.const import ( CONF_ID, CONF_SPI_ID, - CONF_MODE, CONF_NUMBER, CONF_INVERTED, CONF_DATA_PIN, @@ -35,7 +34,6 @@ CONF_LATCH_PIN = "latch_pin" CONF_OE_PIN = "oe_pin" CONF_SR_COUNT = "sr_count" - CONFIG_SCHEMA = cv.Any( cv.Schema( { @@ -88,24 +86,20 @@ async def to_code(config): def _validate_output_mode(value): - if value is not True: + if value.get(CONF_OUTPUT) is not True: raise cv.Invalid("Only output mode is supported") return value -SN74HC595_PIN_SCHEMA = cv.All( +SN74HC595_PIN_SCHEMA = pins.gpio_base_schema( + SN74HC595GPIOPin, + cv.int_range(min=0, max=2047), + modes=[CONF_OUTPUT], + mode_validator=_validate_output_mode, + invertable=True, +).extend( { - cv.GenerateID(): cv.declare_id(SN74HC595GPIOPin), cv.Required(CONF_SN74HC595): cv.use_id(SN74HC595Component), - cv.Required(CONF_NUMBER): cv.int_range(min=0, max=2048, max_included=False), - cv.Optional(CONF_MODE, default={}): cv.All( - { - cv.Optional(CONF_OUTPUT, default=True): cv.All( - cv.boolean, _validate_output_mode - ), - }, - ), - cv.Optional(CONF_INVERTED, default=False): cv.boolean, } ) diff --git a/esphome/config.py b/esphome/config.py index a980358186..6f644cee14 100644 --- a/esphome/config.py +++ b/esphome/config.py @@ -7,6 +7,7 @@ import re from typing import Optional, Union from contextlib import contextmanager +import contextvars import voluptuous as vol @@ -53,6 +54,7 @@ def iter_components(config): ConfigPath = list[Union[str, int]] +path_context = contextvars.ContextVar("Config path") def _path_begins_with(path: ConfigPath, other: ConfigPath) -> bool: @@ -489,6 +491,7 @@ class SchemaValidationStep(ConfigValidationStep): def run(self, result: Config) -> None: if self.comp.config_schema is None: return + token = path_context.set(self.path) with result.catch_error(self.path): if self.comp.is_platform: # Remove 'platform' key for validation @@ -507,6 +510,7 @@ class SchemaValidationStep(ConfigValidationStep): validated = schema(self.conf) result.set_by_path(self.path, validated) + path_context.reset(token) result.add_validation_step(FinalValidateValidationStep(self.path, self.comp)) @@ -652,37 +656,24 @@ class FinalValidateValidationStep(ConfigValidationStep): if self.comp.final_validate_schema is not None: self.comp.final_validate_schema(conf) - fconf = fv.full_config.get() - - def _check_pins(c): - for value in c.values(): - if not isinstance(value, dict): - continue - for key, ( - _, - _, - pin_final_validate, - ) in pins.PIN_SCHEMA_REGISTRY.items(): - if ( - key != CORE.target_platform - and key in value - and pin_final_validate is not None - ): - pin_final_validate(fconf, value) - - # Check for pin configs and a final_validate schema in the pin registry - confs = conf - if not isinstance( - confs, list - ): # Handle components like SPI that have a list instead of MULTI_CONF - confs = [conf] - for c in confs: - if c: # Some component have None or empty schemas - _check_pins(c) - fv.full_config.reset(token) +class PinUseValidationCheck(ConfigValidationStep): + """Check for pin reuse""" + + priority = -30 # Should happen after component final validations + + def __init__(self) -> None: + pass + + def run(self, result: Config) -> None: + if result.errors: + # If result already has errors, skip this step + return + pins.PIN_SCHEMA_REGISTRY.final_validate(result) + + def validate_config(config, command_line_substitutions) -> Config: result = Config() @@ -778,6 +769,7 @@ def validate_config(config, command_line_substitutions) -> Config: for domain, conf in config.items(): result.add_validation_step(LoadValidationStep(domain, conf)) result.add_validation_step(IDPassValidationStep()) + result.add_validation_step(PinUseValidationCheck()) result.run_validation_steps() diff --git a/esphome/const.py b/esphome/const.py index 2487d7b64c..2bc088c063 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -46,6 +46,7 @@ CONF_ADDRESS = "address" CONF_ADDRESSABLE_LIGHT_ID = "addressable_light_id" CONF_ADVANCED = "advanced" CONF_AFTER = "after" +CONF_ALLOW_OTHER_USES = "allow_other_uses" CONF_ALPHA = "alpha" CONF_ALTITUDE = "altitude" CONF_ANALOG = "analog" diff --git a/esphome/core/__init__.py b/esphome/core/__init__.py index 3c3b27a79d..58ae23e139 100644 --- a/esphome/core/__init__.py +++ b/esphome/core/__init__.py @@ -526,6 +526,8 @@ class EsphomeCore: self.quiet = False def reset(self): + from esphome.pins import PIN_SCHEMA_REGISTRY + self.dashboard = False self.name = None self.friendly_name = None @@ -545,6 +547,7 @@ class EsphomeCore: self.platformio_options = {} self.loaded_integrations = set() self.component_ids = set() + PIN_SCHEMA_REGISTRY.reset() @property def address(self) -> Optional[str]: diff --git a/esphome/pins.py b/esphome/pins.py index 0035bea4f0..e2fd8e98e2 100644 --- a/esphome/pins.py +++ b/esphome/pins.py @@ -1,5 +1,7 @@ import operator from functools import reduce +import esphome.config_validation as cv +from esphome.core import CORE, ID from esphome.const import ( CONF_INPUT, @@ -10,16 +12,120 @@ from esphome.const import ( CONF_PULLDOWN, CONF_PULLUP, CONF_IGNORE_STRAPPING_WARNING, + CONF_ALLOW_OTHER_USES, + CONF_INVERTED, ) -from esphome.util import PinRegistry -from esphome.core import CORE + + +class PinRegistry(dict): + def __init__(self): + super().__init__() + self.pins_used = {} + + def reset(self): + self.pins_used = {} + + def get_count(self, key, number): + """ + Get the number of places a given pin is used. + :param key: The ID of the defining component + :param number: The pin number + :return: The number of places the pin is used. + """ + pin_key = (key, number) + return self.pins_used[pin_key] if pin_key in self.pins_used else 0 + + def register(self, name, schema, final_validate=None): + """ + Register a pin schema + :param name: + :param schema: + :param final_validate: + :return: + """ + + def decorator(fun): + self[name] = (fun, schema, final_validate) + return fun + + return decorator + + def validate(self, conf, key=None): + """ + Validate a pin against a registered schema + :param conf The pin config + :param key: an optional scalar key (e.g. platform) + :return: The transformed result + """ + from esphome.config import path_context + + key = self.get_key(conf) if key is None else key + # Element 1 is the pin validation function + # evaluate here so a validation failure skips the rest + result = self[key][1](conf) + if CONF_NUMBER in result: + # key maps to the pin schema + if isinstance(key, ID): + key = key.id + pin_key = (key, result[CONF_NUMBER]) + if pin_key not in self.pins_used: + self.pins_used[pin_key] = [] + # client_id identifies the instance of the providing component + client_id = result.get(key) + self.pins_used[pin_key].append((path_context.get(), client_id, result)) + # return the validated pin config + return result + + def get_key(self, conf): + """ + Is there a key in conf corresponding to a registered pin schema? + If not, fall back to the default platform schema. + :param conf The config for the component + :return: the schema key + """ + keys = list(filter(lambda k: k in conf, self)) + return keys[0] if keys else CORE.target_platform + + def get_to_code(self, key): + """ + Return the code generator function for a pin schema, stored as tuple element 0 + :param conf: The pin config + :param key An optional specific key + :return: The awaitable coroutine + """ + key = self.get_key(key) if isinstance(key, dict) else key + return self[key][0] + + def final_validate(self, fconf): + """ + Run the final validation for all pins, and check for reuse + :param fconf: The full config + """ + for (key, _), pin_list in self.pins_used.items(): + count = len(pin_list) # number of places same pin used. + final_val_fun = self[key][2] # final validation function + for pin_path, client_id, pin_config in pin_list: + with fconf.catch_error([cv.ROOT_CONFIG_PATH] + pin_path): + if final_val_fun is not None: + # Get the containing path of the config providing this pin. + parent_path = fconf.get_path_for_id(client_id)[:-1] + parent_config = fconf.get_config_for_path(parent_path) + final_val_fun(pin_config, parent_config) + allow_others = pin_config.get(CONF_ALLOW_OTHER_USES, False) + if count != 1 and not allow_others: + raise cv.Invalid( + f"Pin {pin_config[CONF_NUMBER]} is used in multiple places" + ) + if count == 1 and allow_others: + raise cv.Invalid( + f"Pin {pin_config[CONF_NUMBER]} incorrectly sets {CONF_ALLOW_OTHER_USES}: true" + ) + PIN_SCHEMA_REGISTRY = PinRegistry() def _set_mode(value, default_mode): - import esphome.config_validation as cv - if CONF_MODE not in value: return {**value, CONF_MODE: default_mode} mode = value[CONF_MODE] @@ -65,20 +171,26 @@ def _schema_creator(default_mode, internal: bool = False): if not isinstance(value, dict): return validator({CONF_NUMBER: value}) value = _set_mode(value, default_mode) - if not internal: - for key, entry in PIN_SCHEMA_REGISTRY.items(): - if key != CORE.target_platform and key in value: - return entry[1](value) - return PIN_SCHEMA_REGISTRY[CORE.target_platform][1](value) + if internal: + return PIN_SCHEMA_REGISTRY.validate(value, CORE.target_platform) + return PIN_SCHEMA_REGISTRY.validate(value) return validator def _internal_number_creator(mode): def validator(value): - value_d = {CONF_NUMBER: value} + if isinstance(value, dict): + if CONF_MODE in value or CONF_INVERTED in value: + raise cv.Invalid( + "This variable only supports pin numbers, not full pin schemas " + "(with inverted and mode)." + ) + value_d = value + else: + value_d = {CONF_NUMBER: value} value_d = _set_mode(value_d, mode) - return PIN_SCHEMA_REGISTRY[CORE.target_platform][1](value_d)[CONF_NUMBER] + return PIN_SCHEMA_REGISTRY.validate(value_d, CORE.target_platform)[CONF_NUMBER] return validator @@ -149,8 +261,6 @@ internal_gpio_input_pullup_pin_number = _internal_number_creator( def check_strapping_pin(conf, strapping_pin_list, logger): - import esphome.config_validation as cv - num = conf[CONF_NUMBER] if num in strapping_pin_list and not conf.get(CONF_IGNORE_STRAPPING_WARNING): logger.warning( @@ -161,3 +271,52 @@ def check_strapping_pin(conf, strapping_pin_list, logger): # mitigate undisciplined use of strapping: if num not in strapping_pin_list and conf.get(CONF_IGNORE_STRAPPING_WARNING): raise cv.Invalid(f"GPIO{num} is not a strapping pin") + + +GPIO_STANDARD_MODES = ( + CONF_INPUT, + CONF_OUTPUT, + CONF_OPEN_DRAIN, + CONF_PULLUP, + CONF_PULLDOWN, +) + + +def gpio_validate_modes(value): + if not value[CONF_INPUT] and not value[CONF_OUTPUT]: + raise cv.Invalid("Mode must be input or output") + return value + + +def gpio_base_schema( + pin_type, + number_validator, + modes=GPIO_STANDARD_MODES, + mode_validator=gpio_validate_modes, + invertable=True, +): + """ + Generate a base gpio pin schema + :param pin_type: The type for the pin variable + :param number_validator: A validator for the pin number + :param modes: The available modes, default is all standard modes + :param mode_validator: A validator function for the pin mode + :param invertable: If the pin supports hardware inversion + :return: A schema for the pin + """ + mode_default = len(modes) == 1 + mode_dict = dict( + map(lambda m: (cv.Optional(m, default=mode_default), cv.boolean), modes) + ) + + schema = cv.Schema( + { + cv.GenerateID(): cv.declare_id(pin_type), + cv.Required(CONF_NUMBER): number_validator, + cv.Optional(CONF_ALLOW_OTHER_USES): cv.boolean, + cv.Optional(CONF_MODE, default={}): cv.All(mode_dict, mode_validator), + } + ) + if invertable: + return schema.extend({cv.Optional(CONF_INVERTED, default=False): cv.boolean}) + return schema diff --git a/esphome/util.py b/esphome/util.py index d9c8502e0e..d5a4c60570 100644 --- a/esphome/util.py +++ b/esphome/util.py @@ -57,32 +57,6 @@ class SimpleRegistry(dict): return decorator -def _final_validate(parent_id_key, fun): - def validator(fconf, pin_config): - import esphome.config_validation as cv - - parent_path = fconf.get_path_for_id(pin_config[parent_id_key])[:-1] - parent_config = fconf.get_config_for_path(parent_path) - - pin_path = fconf.get_path_for_id(pin_config[const.CONF_ID])[:-1] - with cv.prepend_path([cv.ROOT_CONFIG_PATH] + pin_path): - fun(pin_config, parent_config) - - return validator - - -class PinRegistry(dict): - def register(self, name, schema, final_validate=None): - if final_validate is not None: - final_validate = _final_validate(name, final_validate) - - def decorator(fun): - self[name] = (fun, schema, final_validate) - return fun - - return decorator - - def safe_print(message="", end="\n"): from esphome.core import CORE diff --git a/tests/test1.1.yaml b/tests/test1.1.yaml index e2e7bd5d63..c71aa6e0ef 100644 --- a/tests/test1.1.yaml +++ b/tests/test1.1.yaml @@ -54,7 +54,9 @@ power_supply: i2c: sda: 21 - scl: 22 + scl: + number: 22 + allow_other_uses: true scan: true frequency: 100kHz setup_priority: -100 @@ -86,7 +88,9 @@ light: - platform: fastled_clockless id: addr1 chipset: WS2811 - pin: GPIO23 + pin: + allow_other_uses: true + number: GPIO23 num_leds: 60 rgb_order: BRG max_refresh_rate: 20ms @@ -168,8 +172,12 @@ light: - platform: fastled_spi id: addr2 chipset: WS2801 - data_pin: GPIO23 - clock_pin: GPIO22 + data_pin: + allow_other_uses: true + number: GPIO23 + clock_pin: + number: GPIO22 + allow_other_uses: true data_rate: 2MHz num_leds: 60 rgb_order: BRG @@ -190,7 +198,9 @@ light: variant: SK6812 method: ESP32_I2S_0 num_leds: 60 - pin: GPIO23 + pin: + allow_other_uses: true + number: GPIO23 - platform: partition name: Partition Light segments: diff --git a/tests/test1.yaml b/tests/test1.yaml index 0849d8aeb6..c90cdf6d90 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -181,8 +181,12 @@ mqtt: - light.turn_off: ${roomname}_lights i2c: - sda: 21 - scl: 22 + sda: + allow_other_uses: true + number: 21 + scl: + allow_other_uses: true + number: 22 scan: true frequency: 100kHz setup_priority: -100 @@ -190,15 +194,23 @@ i2c: spi: id: spi_bus - clk_pin: GPIO21 - mosi_pin: GPIO22 - miso_pin: GPIO23 + clk_pin: + allow_other_uses: true + number: GPIO21 + mosi_pin: + allow_other_uses: true + number: GPIO22 + miso_pin: + allow_other_uses: true + number: GPIO23 uart: - tx_pin: + allow_other_uses: true number: GPIO22 inverted: true rx_pin: + allow_other_uses: true number: GPIO23 inverted: true baud_rate: 115200 @@ -220,18 +232,30 @@ uart: - lambda: UARTDebug::log_int(direction, bytes, ','); - lambda: UARTDebug::log_binary(direction, bytes, ';'); - id: ld2410_uart - tx_pin: 18 - rx_pin: 23 + tx_pin: + allow_other_uses: true + number: 18 + rx_pin: + allow_other_uses: true + number: 23 baud_rate: 256000 parity: NONE stop_bits: 1 - id: dfrobot_mmwave_uart - tx_pin: 14 - rx_pin: 27 + tx_pin: + allow_other_uses: true + number: 14 + rx_pin: + allow_other_uses: true + number: 27 baud_rate: 115200 - id: ld2420_uart - tx_pin: 17 - rx_pin: 16 + tx_pin: + allow_other_uses: true + number: 17 + rx_pin: + allow_other_uses: true + number: 16 baud_rate: 115200 parity: NONE stop_bits: 1 @@ -282,12 +306,16 @@ power_supply: keep_on_time: 10s pin: number: 13 + allow_other_uses: true inverted: true deep_sleep: run_duration: 20s sleep_duration: 50s - wakeup_pin: GPIO2 + wakeup_pin: + allow_other_uses: true + number: GPIO2 + ignore_strapping_warning: true wakeup_pin_mode: INVERT_WAKEUP ads1115: @@ -295,11 +323,18 @@ ads1115: i2c_id: i2c_bus dallas: - pin: GPIO23 + pin: + allow_other_uses: true + number: GPIO23 as3935_spi: - cs_pin: GPIO12 - irq_pin: GPIO13 + cs_pin: + ignore_strapping_warning: true + allow_other_uses: true + number: GPIO12 + irq_pin: + allow_other_uses: true + number: GPIO13 esp32_ble: io_capability: keyboard_only @@ -339,16 +374,24 @@ bedjet: time_id: sntp_time mcp23s08: - id: mcp23s08_hub - cs_pin: GPIO12 + cs_pin: + ignore_strapping_warning: true + number: GPIO12 + allow_other_uses: true deviceaddress: 0 mcp23s17: - id: mcp23s17_hub - cs_pin: GPIO12 + cs_pin: + ignore_strapping_warning: true + number: GPIO12 + allow_other_uses: true deviceaddress: 1 micronova: - enable_rx_pin: 4 + enable_rx_pin: + allow_other_uses: true + number: 4 uart_id: uart_0 dfrobot_sen0395: @@ -539,7 +582,9 @@ sensor: name: NIR i2c_id: i2c_bus - platform: atm90e26 - cs_pin: 5 + cs_pin: + allow_other_uses: true + number: 5 voltage: name: Line Voltage current: @@ -558,7 +603,9 @@ sensor: gain_voltage: 26400 gain_ct: 31251 - platform: atm90e32 - cs_pin: 5 + cs_pin: + allow_other_uses: true + number: 5 phase_a: voltage: name: EMON Line Voltage A @@ -675,7 +722,9 @@ sensor: index: 1 name: Living Room Temperature 2 - platform: dht - pin: GPIO26 + pin: + allow_other_uses: true + number: GPIO26 temperature: id: dht_temperature name: Living Room Temperature 3 @@ -692,7 +741,9 @@ sensor: update_interval: 15s i2c_id: i2c_bus - platform: duty_cycle - pin: GPIO25 + pin: + allow_other_uses: true + number: GPIO25 name: Duty Cycle Sensor - platform: ee895 co2: @@ -721,9 +772,15 @@ sensor: update_interval: 15s i2c_id: i2c_bus - platform: hlw8012 - sel_pin: 5 - cf_pin: 14 - cf1_pin: 13 + sel_pin: + allow_other_uses: true + number: 5 + cf_pin: + allow_other_uses: true + number: 14 + cf1_pin: + allow_other_uses: true + number: 13 current: name: HLW8012 Current voltage: @@ -772,7 +829,9 @@ sensor: max_pressure: 15 temperature: name: Honeywell temperature - cs_pin: GPIO5 + cs_pin: + allow_other_uses: true + number: GPIO5 - platform: honeywellabp2_i2c pressure: name: Honeywell2 pressure @@ -806,8 +865,12 @@ sensor: i2c_id: i2c_bus - platform: hx711 name: HX711 Value - dout_pin: GPIO23 - clk_pin: GPIO25 + dout_pin: + allow_other_uses: true + number: GPIO23 + clk_pin: + allow_other_uses: true + number: GPIO25 gain: 128 update_interval: 15s - platform: ina219 @@ -880,22 +943,30 @@ sensor: i2c_id: i2c_bus - platform: max6675 name: Living Room Temperature - cs_pin: GPIO23 + cs_pin: + allow_other_uses: true + number: GPIO23 update_interval: 15s - platform: max31855 name: Den Temperature - cs_pin: GPIO23 + cs_pin: + allow_other_uses: true + number: GPIO23 update_interval: 15s reference_temperature: name: MAX31855 Internal Temperature - platform: max31856 name: BBQ Temperature - cs_pin: GPIO17 + cs_pin: + allow_other_uses: true + number: GPIO17 update_interval: 15s mains_filter: 50Hz - platform: max31865 name: Water Tank Temperature - cs_pin: GPIO23 + cs_pin: + allow_other_uses: true + number: GPIO23 update_interval: 15s reference_resistance: 430 Ω rtd_nominal_resistance: 100 Ω @@ -1007,7 +1078,10 @@ sensor: i2c_id: i2c_bus - platform: pulse_counter name: Pulse Counter - pin: GPIO12 + pin: + ignore_strapping_warning: true + number: GPIO12 + allow_other_uses: true count_mode: rising_edge: INCREMENT falling_edge: DECREMENT @@ -1016,7 +1090,10 @@ sensor: - platform: pulse_meter name: Pulse Meter id: pulse_meter_sensor - pin: GPIO12 + pin: + ignore_strapping_warning: true + number: GPIO12 + allow_other_uses: true internal_filter: 100ms timeout: 2 min on_value: @@ -1039,9 +1116,15 @@ sensor: - platform: rotary_encoder name: Rotary Encoder id: rotary_encoder1 - pin_a: GPIO23 - pin_b: GPIO25 - pin_reset: GPIO25 + pin_a: + allow_other_uses: true + number: GPIO23 + pin_b: + allow_other_uses: true + number: GPIO25 + pin_reset: + allow_other_uses: true + number: GPIO25 filters: - or: - debounce: 0.1s @@ -1064,7 +1147,9 @@ sensor: - display_menu.up: - platform: pulse_width name: Pulse Width - pin: GPIO12 + pin: + allow_other_uses: true + number: GPIO12 - platform: sm300d2 uart_id: uart_0 co2: @@ -1247,9 +1332,12 @@ sensor: address: 0x48 i2c_id: i2c_bus - platform: ultrasonic - trigger_pin: GPIO25 + trigger_pin: + allow_other_uses: true + number: GPIO25 echo_pin: number: GPIO23 + allow_other_uses: true inverted: true name: Ultrasonic Sensor timeout: 5.5m @@ -1296,9 +1384,14 @@ sensor: pin: number: GPIO04 mode: INPUT + allow_other_uses: true - platform: zyaura - clock_pin: GPIO5 - data_pin: GPIO4 + clock_pin: + allow_other_uses: true + number: GPIO5 + data_pin: + allow_other_uses: true + number: GPIO4 co2: name: ZyAura CO2 temperature: @@ -1572,6 +1665,7 @@ binary_sensor: mcp23xxx: mcp23s08_hub # Use pin number 1 number: 1 + allow_other_uses: true # One of INPUT or INPUT_PULLUP mode: INPUT_PULLUP inverted: false @@ -1581,6 +1675,7 @@ binary_sensor: mcp23xxx: mcp23s17_hub # Use pin number 1 number: 1 + allow_other_uses: true # One of INPUT or INPUT_PULLUP mode: INPUT_PULLUP inverted: false @@ -1589,13 +1684,16 @@ binary_sensor: pin: mcp23xxx: mcp23s17_hub # Use pin number 1 + allow_other_uses: true number: 1 # One of INPUT or INPUT_PULLUP mode: INPUT_PULLUP inverted: false interrupt: FALLING - platform: gpio - pin: GPIO9 + pin: + allow_other_uses: true + number: GPIO9 name: Living Room Window device_class: window filters: @@ -1664,11 +1762,13 @@ binary_sensor: - platform: gpio pin: number: GPIO9 + allow_other_uses: true mode: INPUT_PULLUP name: Living Room Window 2 - platform: gpio pin: number: GPIO9 + allow_other_uses: true mode: INPUT_OUTPUT_OPEN_DRAIN name: Living Room Button - platform: status @@ -1747,6 +1847,7 @@ binary_sensor: pin: mcp23xxx: mcp23017_hub number: 1 + allow_other_uses: true mode: INPUT inverted: true - platform: gpio @@ -1767,6 +1868,7 @@ binary_sensor: name: Speed Fan Cycle binary sensor" pin: number: 18 + allow_other_uses: true mode: input: true pulldown: true @@ -1891,42 +1993,66 @@ tlc59208f: i2c_id: i2c_bus my9231: - data_pin: GPIO12 - clock_pin: GPIO14 + data_pin: + allow_other_uses: true + number: GPIO12 + clock_pin: + allow_other_uses: true + number: GPIO14 num_channels: 6 num_chips: 2 bit_depth: 16 sm2235: - data_pin: GPIO4 - clock_pin: GPIO5 + data_pin: + allow_other_uses: true + number: GPIO4 + clock_pin: + allow_other_uses: true + number: GPIO5 max_power_color_channels: 9 max_power_white_channels: 9 sm2335: - data_pin: GPIO4 - clock_pin: GPIO5 + data_pin: + allow_other_uses: true + number: GPIO4 + clock_pin: + allow_other_uses: true + number: GPIO5 max_power_color_channels: 9 max_power_white_channels: 9 bp1658cj: - data_pin: GPIO3 - clock_pin: GPIO5 + data_pin: + allow_other_uses: true + number: GPIO3 + clock_pin: + allow_other_uses: true + number: GPIO5 max_power_color_channels: 4 max_power_white_channels: 6 bp5758d: - data_pin: GPIO3 - clock_pin: GPIO5 + data_pin: + allow_other_uses: true + number: GPIO3 + clock_pin: + allow_other_uses: true + number: GPIO5 output: - platform: gpio - pin: GPIO26 + pin: + allow_other_uses: true + number: GPIO26 id: gpio_26 power_supply: atx_power_supply inverted: false - platform: ledc - pin: 19 + pin: + allow_other_uses: true + number: 19 id: gpio_19 frequency: 1500Hz channel: 14 @@ -1996,6 +2122,7 @@ output: pin: pcf8574: pcf8574_hub number: 0 + #allow_other_uses: true mode: OUTPUT inverted: false - platform: gpio @@ -2003,6 +2130,7 @@ output: pin: pca9554: pca9554_hub number: 0 + #allow_other_uses: true mode: OUTPUT inverted: false - platform: gpio @@ -2010,6 +2138,7 @@ output: pin: mcp23xxx: mcp23017_hub number: 0 + allow_other_uses: true mode: OUTPUT inverted: false - platform: gpio @@ -2017,6 +2146,7 @@ output: pin: mcp23xxx: mcp23008_hub number: 0 + allow_other_uses: true mode: OUTPUT inverted: false - platform: gpio @@ -2076,14 +2206,22 @@ output: channel: 3 - platform: slow_pwm id: id24 - pin: GPIO26 + pin: + allow_other_uses: true + number: GPIO26 period: 15s - platform: ac_dimmer id: dimmer1 - gate_pin: GPIO5 - zero_cross_pin: GPIO26 + gate_pin: + allow_other_uses: true + number: GPIO5 + zero_cross_pin: + allow_other_uses: true + number: GPIO26 - platform: esp32_dac - pin: GPIO25 + pin: + allow_other_uses: true + number: GPIO25 id: dac_output - platform: mcp4725 id: mcp4725_dac_output @@ -2147,9 +2285,15 @@ output: current: 10 - platform: x9c id: test_x9c - cs_pin: GPIO25 - inc_pin: GPIO26 - ud_pin: GPIO27 + cs_pin: + allow_other_uses: true + number: GPIO25 + inc_pin: + allow_other_uses: true + number: GPIO26 + ud_pin: + allow_other_uses: true + number: GPIO27 initial_value: 0.5 light: @@ -2256,7 +2400,9 @@ light: warm_white_color_temperature: 500 mireds remote_transmitter: - - pin: 32 + - pin: + allow_other_uses: true + number: 32 carrier_duty_percent: 100% climate: @@ -2440,6 +2586,7 @@ switch: mcp23xxx: mcp23s08_hub # Use pin number 0 number: 0 + allow_other_uses: true mode: OUTPUT inverted: false - platform: gpio @@ -2448,10 +2595,13 @@ switch: mcp23xxx: mcp23s17_hub # Use pin number 0 number: 1 + allow_other_uses: true mode: OUTPUT inverted: false - platform: gpio - pin: GPIO25 + pin: + allow_other_uses: true + number: GPIO25 name: Living Room Dehumidifier icon: "mdi:restart" inverted: true @@ -2834,12 +2984,24 @@ display: id: my_lcd_gpio dimensions: 18x4 data_pins: - - GPIO19 - - GPIO21 - - GPIO22 - - GPIO23 - enable_pin: GPIO23 - rs_pin: GPIO25 + - + allow_other_uses: true + number: GPIO19 + - + allow_other_uses: true + number: GPIO21 + - + allow_other_uses: true + number: GPIO22 + - + allow_other_uses: true + number: GPIO23 + enable_pin: + allow_other_uses: true + number: GPIO23 + rs_pin: + allow_other_uses: true + number: GPIO25 lambda: |- it.print("Hello World!"); - platform: lcd_pcf8574 @@ -2860,13 +3022,19 @@ display: it.print("Hello World!"); i2c_id: i2c_bus - platform: max7219 - cs_pin: GPIO23 + cs_pin: + allow_other_uses: true + number: GPIO23 num_chips: 1 lambda: |- it.print("01234567"); - platform: tm1637 - clk_pin: GPIO23 - dio_pin: GPIO25 + clk_pin: + allow_other_uses: true + number: GPIO23 + dio_pin: + allow_other_uses: true + number: GPIO25 intensity: 3 lambda: |- it.print("1234"); @@ -2874,6 +3042,7 @@ display: clk_pin: mcp23xxx: mcp23017_hub number: 1 + allow_other_uses: true dio_pin: mcp23xxx: mcp23017_hub number: 2 @@ -2883,15 +3052,23 @@ display: lambda: |- it.print("1234"); - platform: pcd8544 - cs_pin: GPIO23 - dc_pin: GPIO23 - reset_pin: GPIO23 + cs_pin: + allow_other_uses: true + number: GPIO23 + dc_pin: + allow_other_uses: true + number: GPIO23 + reset_pin: + allow_other_uses: true + number: GPIO23 contrast: 60 lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); - platform: ssd1306_i2c model: SSD1306_128X64 - reset_pin: GPIO23 + reset_pin: + allow_other_uses: true + number: GPIO23 address: 0x3C id: display1 contrast: 60% @@ -2912,28 +3089,48 @@ display: i2c_id: i2c_bus - platform: ssd1306_spi model: SSD1306 128x64 - cs_pin: GPIO23 - dc_pin: GPIO23 - reset_pin: GPIO23 + cs_pin: + allow_other_uses: true + number: GPIO23 + dc_pin: + allow_other_uses: true + number: GPIO23 + reset_pin: + allow_other_uses: true + number: GPIO23 lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); - platform: ssd1322_spi model: SSD1322 256x64 - cs_pin: GPIO23 - dc_pin: GPIO23 - reset_pin: GPIO23 + cs_pin: + allow_other_uses: true + number: GPIO23 + dc_pin: + allow_other_uses: true + number: GPIO23 + reset_pin: + allow_other_uses: true + number: GPIO23 lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); - platform: ssd1325_spi model: SSD1325 128x64 - cs_pin: GPIO23 - dc_pin: GPIO23 - reset_pin: GPIO23 + cs_pin: + allow_other_uses: true + number: GPIO23 + dc_pin: + allow_other_uses: true + number: GPIO23 + reset_pin: + allow_other_uses: true + number: GPIO23 lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); - platform: ssd1327_i2c model: SSD1327 128X128 - reset_pin: GPIO23 + reset_pin: + allow_other_uses: true + number: GPIO23 address: 0x3D id: display1327 brightness: 60% @@ -2947,29 +3144,53 @@ display: i2c_id: i2c_bus - platform: ssd1327_spi model: SSD1327 128x128 - cs_pin: GPIO23 - dc_pin: GPIO23 - reset_pin: GPIO23 + cs_pin: + allow_other_uses: true + number: GPIO23 + dc_pin: + allow_other_uses: true + number: GPIO23 + reset_pin: + allow_other_uses: true + number: GPIO23 lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); - platform: ssd1331_spi - cs_pin: GPIO23 - dc_pin: GPIO23 - reset_pin: GPIO23 + cs_pin: + allow_other_uses: true + number: GPIO23 + dc_pin: + allow_other_uses: true + number: GPIO23 + reset_pin: + allow_other_uses: true + number: GPIO23 lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); - platform: ssd1351_spi model: SSD1351 128x128 - cs_pin: GPIO23 - dc_pin: GPIO23 - reset_pin: GPIO23 + cs_pin: + allow_other_uses: true + number: GPIO23 + dc_pin: + allow_other_uses: true + number: GPIO23 + reset_pin: + allow_other_uses: true + number: GPIO23 lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); - platform: st7789v model: TTGO TDisplay 135x240 - cs_pin: GPIO5 - dc_pin: GPIO16 - reset_pin: GPIO23 + cs_pin: + allow_other_uses: true + number: GPIO5 + dc_pin: + allow_other_uses: true + number: GPIO16 + reset_pin: + allow_other_uses: true + number: GPIO23 backlight_pin: no lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); @@ -2977,15 +3198,22 @@ display: width: 128 height: 64 cs_pin: + allow_other_uses: true number: GPIO23 inverted: true lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); - platform: st7735 model: INITR_BLACKTAB - cs_pin: GPIO5 - dc_pin: GPIO16 - reset_pin: GPIO23 + cs_pin: + allow_other_uses: true + number: GPIO5 + dc_pin: + allow_other_uses: true + number: GPIO16 + reset_pin: + allow_other_uses: true + number: GPIO23 rotation: 0 device_width: 128 device_height: 160 @@ -3001,10 +3229,16 @@ display: mirror_x: true mirror_y: false model: TFT 2.4 - cs_pin: GPIO5 - dc_pin: GPIO4 + cs_pin: + allow_other_uses: true + number: GPIO5 + dc_pin: + allow_other_uses: true + number: GPIO4 color_palette: GRAYSCALE - reset_pin: GPIO22 + reset_pin: + allow_other_uses: true + number: GPIO22 lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); - platform: ili9xxx @@ -3014,9 +3248,15 @@ display: offset_width: 20 offset_height: 10 model: TFT 2.4 - cs_pin: GPIO5 - dc_pin: GPIO4 - reset_pin: GPIO22 + cs_pin: + allow_other_uses: true + number: GPIO5 + dc_pin: + allow_other_uses: true + number: GPIO4 + reset_pin: + allow_other_uses: true + number: GPIO22 auto_clear_enabled: false rotation: 90 lambda: |- @@ -3041,10 +3281,18 @@ display: it.print_battery(true); - platform: tm1621 id: tm1621_display - cs_pin: GPIO17 - data_pin: GPIO5 - read_pin: GPIO23 - write_pin: GPIO18 + cs_pin: + allow_other_uses: true + number: GPIO17 + data_pin: + allow_other_uses: true + number: GPIO5 + read_pin: + allow_other_uses: true + number: GPIO23 + write_pin: + allow_other_uses: true + number: GPIO18 lambda: |- it.printf(0, "%.1f", id(dht_temperature).state); it.display_celsius(true); @@ -3053,12 +3301,18 @@ display: tm1651: id: tm1651_battery - clk_pin: GPIO23 - dio_pin: GPIO23 + clk_pin: + allow_other_uses: true + number: GPIO23 + dio_pin: + allow_other_uses: true + number: GPIO23 remote_receiver: id: rcvr - pin: GPIO32 + pin: + allow_other_uses: true + number: GPIO32 dump: all on_coolix: then: @@ -3068,11 +3322,16 @@ remote_receiver: delay: !lambda "return uint32_t(x.code) + x.protocol;" status_led: - pin: GPIO2 + pin: + allow_other_uses: true + number: GPIO2 + ignore_strapping_warning: true pn532_spi: id: pn532_bs - cs_pin: GPIO23 + cs_pin: + allow_other_uses: true + number: GPIO23 update_interval: 1s on_tag: - lambda: |- @@ -3094,7 +3353,9 @@ rdm6300: uart_id: uart_0 rc522_spi: - cs_pin: GPIO23 + cs_pin: + allow_other_uses: true + number: GPIO23 update_interval: 1s on_tag: - lambda: |- @@ -3287,9 +3548,15 @@ mcp23016: stepper: - platform: a4988 id: my_stepper - step_pin: GPIO23 - dir_pin: GPIO25 - sleep_pin: GPIO25 + step_pin: + allow_other_uses: true + number: GPIO23 + dir_pin: + allow_other_uses: true + number: GPIO25 + sleep_pin: + allow_other_uses: true + number: GPIO25 max_speed: 250 steps/s acceleration: 100 steps/s^2 deceleration: 200 steps/s^2 @@ -3397,14 +3664,26 @@ text_sensor: sn74hc595: - id: sn74hc595_hub - data_pin: GPIO21 - clock_pin: GPIO23 - latch_pin: GPIO22 - oe_pin: GPIO32 + data_pin: + allow_other_uses: true + number: GPIO21 + clock_pin: + allow_other_uses: true + number: GPIO23 + latch_pin: + allow_other_uses: true + number: GPIO22 + oe_pin: + allow_other_uses: true + number: GPIO32 sr_count: 2 - id: sn74hc595_hub_2 - latch_pin: GPIO22 - oe_pin: GPIO32 + latch_pin: + allow_other_uses: true + number: GPIO22 + oe_pin: + allow_other_uses: true + number: GPIO32 sr_count: 2 spi_id: spi_bus @@ -3456,8 +3735,12 @@ canbus: } - platform: esp32_can id: esp32_internal_can - rx_pin: GPIO04 - tx_pin: GPIO05 + rx_pin: + allow_other_uses: true + number: GPIO04 + tx_pin: + allow_other_uses: true + number: GPIO05 can_id: 4 bit_rate: 50kbps on_frame: diff --git a/tests/test11.5.yaml b/tests/test11.5.yaml index 685487e871..2a9b40c5c3 100644 --- a/tests/test11.5.yaml +++ b/tests/test11.5.yaml @@ -44,25 +44,42 @@ uart: rx_pin: 3 baud_rate: 9600 - id: uart_2 - tx_pin: 17 - rx_pin: 16 + tx_pin: + allow_other_uses: true + number: 17 + rx_pin: + allow_other_uses: true + number: 16 baud_rate: 19200 i2c: + sda: + number: 21 + allow_other_uses: true frequency: 100khz spi: - id: spi_1 - clk_pin: 12 - mosi_pin: 13 - miso_pin: 14 + clk_pin: + allow_other_uses: true + number: 12 + mosi_pin: + allow_other_uses: true + number: 13 + miso_pin: + allow_other_uses: true + number: 14 - id: spi_2 - clk_pin: 32 + clk_pin: + allow_other_uses: true + number: 32 mosi_pin: 33 modbus: uart_id: uart_1 - flow_control_pin: 5 + flow_control_pin: + allow_other_uses: true + number: 5 id: mod_bus1 modbus_controller: @@ -229,9 +246,15 @@ binary_sensor: lambda: return x[0] & 1; tlc5947: - data_pin: GPIO12 - clock_pin: GPIO14 - lat_pin: GPIO15 + data_pin: + allow_other_uses: true + number: GPIO12 + clock_pin: + allow_other_uses: true + number: GPIO14 + lat_pin: + allow_other_uses: true + number: GPIO15 gp8403: - id: gp8403_5v @@ -417,7 +440,9 @@ sensor: - platform: adc id: adc_sensor_p32 name: ADC pin 32 - pin: 32 + pin: + allow_other_uses: true + number: 32 attenuation: 11db update_interval: 1s - platform: internal_temperature @@ -584,7 +609,9 @@ sensor: name: Kuntze temperature - platform: ade7953_i2c - irq_pin: 16 + irq_pin: + allow_other_uses: true + number: 16 voltage: name: ADE7953 Voltage current_a: @@ -612,7 +639,9 @@ sensor: - platform: ade7953_spi spi_id: spi_1 cs_pin: 04 - irq_pin: 16 + irq_pin: + allow_other_uses: true + number: 16 voltage: name: ADE7953 Voltage current_a: @@ -683,7 +712,9 @@ switch: display: - platform: tm1638 id: primarydisplay - stb_pin: 5 #TM1638 STB + stb_pin: + allow_other_uses: true + number: 5 #TM1638 STB clk_pin: 18 #TM1638 CLK dio_pin: 23 #TM1638 DIO update_interval: 5s @@ -728,20 +759,32 @@ text_sensor: sn74hc165: id: sn74hc165_hub - data_pin: GPIO12 - clock_pin: GPIO14 - load_pin: GPIO27 - clock_inhibit_pin: GPIO26 + data_pin: + allow_other_uses: true + number: GPIO12 + clock_pin: + allow_other_uses: true + number: GPIO14 + load_pin: + number: GPIO27 + clock_inhibit_pin: + number: GPIO26 sr_count: 4 matrix_keypad: id: keypad rows: - - pin: 21 + - pin: + allow_other_uses: true + number: 21 - pin: 19 columns: - - pin: 17 - - pin: 16 + - pin: + allow_other_uses: true + number: 17 + - pin: + allow_other_uses: true + number: 16 keys: "1234" key_collector: @@ -753,14 +796,18 @@ key_collector: light: - platform: esp32_rmt_led_strip id: led_strip - pin: 13 + pin: + allow_other_uses: true + number: 13 num_leds: 60 rmt_channel: 6 rgb_order: GRB chipset: ws2812 - platform: esp32_rmt_led_strip id: led_strip2 - pin: 15 + pin: + allow_other_uses: true + number: 15 num_leds: 60 rmt_channel: 2 rgb_order: RGB diff --git a/tests/test2.yaml b/tests/test2.yaml index b258b103d3..e5358781df 100644 --- a/tests/test2.yaml +++ b/tests/test2.yaml @@ -17,11 +17,17 @@ substitutions: ethernet: type: LAN8720 - mdc_pin: GPIO23 - mdio_pin: GPIO25 + mdc_pin: + allow_other_uses: true + number: GPIO23 + mdio_pin: + allow_other_uses: true + number: GPIO25 clk_mode: GPIO0_IN phy_addr: 0 - power_pin: GPIO25 + power_pin: + allow_other_uses: true + number: GPIO25 manual_ip: static_ip: 192.168.178.56 gateway: 192.168.178.1 @@ -37,18 +43,32 @@ mdns: api: i2c: - sda: 21 - scl: 22 + sda: + allow_other_uses: true + number: 21 + scl: + allow_other_uses: true + number: 22 scan: false spi: - clk_pin: GPIO21 - mosi_pin: GPIO22 - miso_pin: GPIO23 + clk_pin: + allow_other_uses: true + number: GPIO21 + mosi_pin: + allow_other_uses: true + number: GPIO22 + miso_pin: + allow_other_uses: true + number: GPIO23 uart: - tx_pin: GPIO22 - rx_pin: GPIO23 + tx_pin: + allow_other_uses: true + number: GPIO22 + rx_pin: + allow_other_uses: true + number: GPIO23 baud_rate: 115200 # Specifically added for testing debug with no after: definition. debug: @@ -73,21 +93,29 @@ deep_sleep: gpio_wakeup_reason: 10s touch_wakeup_reason: 15s sleep_duration: 50s - wakeup_pin: GPIO2 + wakeup_pin: + allow_other_uses: true + number: GPIO2 wakeup_pin_mode: INVERT_WAKEUP as3935_i2c: - irq_pin: GPIO12 + irq_pin: + allow_other_uses: true + number: GPIO12 mcp3008: - id: mcp3008_hub - cs_pin: GPIO12 + cs_pin: + allow_other_uses: true + number: GPIO12 output: - platform: ac_dimmer id: dimmer1 gate_pin: GPIO5 - zero_cross_pin: GPIO12 + zero_cross_pin: + allow_other_uses: true + number: GPIO12 sensor: - platform: homeassistant @@ -534,7 +562,9 @@ binary_sensor: name: Mi Motion Sensor 2 Button - platform: gpio id: gpio_set_retry_test - pin: GPIO9 + pin: + allow_other_uses: true + number: GPIO9 on_press: then: - lambda: |- @@ -601,7 +631,9 @@ xiaomi_rtcgq02lm: bindkey: "48403ebe2d385db8d0c187f81e62cb64" status_led: - pin: GPIO2 + pin: + allow_other_uses: true + number: GPIO2 text_sensor: - platform: version @@ -704,9 +736,13 @@ script: stepper: - platform: uln2003 id: my_stepper - pin_a: GPIO23 + pin_a: + allow_other_uses: true + number: GPIO23 pin_b: GPIO27 - pin_c: GPIO25 + pin_c: + allow_other_uses: true + number: GPIO25 pin_d: GPIO26 sleep_when_done: false step_mode: HALF_STEP @@ -731,7 +767,9 @@ display: offset_height: 35 offset_width: 0 dc_pin: GPIO13 - reset_pin: GPIO9 + reset_pin: + allow_other_uses: true + number: GPIO9 image: - id: binary_image diff --git a/tests/test3.1.yaml b/tests/test3.1.yaml index 9000636f63..63ef4e8ce0 100644 --- a/tests/test3.1.yaml +++ b/tests/test3.1.yaml @@ -29,14 +29,24 @@ web_server: version: 2 i2c: - sda: 4 - scl: 5 + sda: + allow_other_uses: true + number: 4 + scl: + allow_other_uses: true + number: 5 scan: false spi: - clk_pin: GPIO12 - mosi_pin: GPIO13 - miso_pin: GPIO14 + clk_pin: + allow_other_uses: true + number: GPIO12 + mosi_pin: + allow_other_uses: true + number: GPIO13 + miso_pin: + allow_other_uses: true + number: GPIO14 ota: @@ -52,7 +62,9 @@ sensor: name: VL53L0x Distance address: 0x29 update_interval: 60s - enable_pin: GPIO13 + enable_pin: + allow_other_uses: true + number: GPIO13 timeout: 200us - platform: apds9960 type: clear @@ -170,7 +182,9 @@ sensor: name: Custom Sensor - platform: ade7953_i2c - irq_pin: GPIO16 + irq_pin: + allow_other_uses: true + number: GPIO16 voltage: name: ADE7953 Voltage id: ade7953_voltage @@ -199,8 +213,12 @@ sensor: update_interval: 1s - platform: ade7953_spi - cs_pin: GPIO04 - irq_pin: GPIO16 + cs_pin: + allow_other_uses: true + number: GPIO04 + irq_pin: + allow_other_uses: true + number: GPIO16 voltage: name: ADE7953 Voltage current_a: @@ -360,8 +378,12 @@ text_sensor: name: Custom Text Sensor sm2135: - data_pin: GPIO12 - clock_pin: GPIO14 + data_pin: + allow_other_uses: true + number: GPIO12 + clock_pin: + allow_other_uses: true + number: GPIO14 rgb_current: 20mA cw_current: 60mA @@ -379,6 +401,7 @@ switch: pin: mcp23xxx: mcp23017_hub number: 0 + allow_other_uses: true mode: OUTPUT interlock: &interlock [gpio_switch1, gpio_switch2, gpio_switch3] - platform: gpio @@ -386,11 +409,14 @@ switch: pin: mcp23xxx: mcp23008_hub number: 0 + allow_other_uses: true mode: OUTPUT interlock: *interlock - platform: gpio id: gpio_switch3 - pin: GPIO1 + pin: + allow_other_uses: true + number: GPIO1 interlock: *interlock - platform: custom lambda: |- @@ -440,10 +466,18 @@ custom_component: stepper: - platform: uln2003 id: my_stepper - pin_a: GPIO12 - pin_b: GPIO13 - pin_c: GPIO14 - pin_d: GPIO15 + pin_a: + allow_other_uses: true + number: GPIO12 + pin_b: + allow_other_uses: true + number: GPIO13 + pin_c: + allow_other_uses: true + number: GPIO14 + pin_d: + allow_other_uses: true + number: GPIO15 sleep_when_done: false step_mode: HALF_STEP max_speed: 250 steps/s @@ -451,8 +485,12 @@ stepper: deceleration: inf - platform: a4988 id: my_stepper2 - step_pin: GPIO1 - dir_pin: GPIO2 + step_pin: + allow_other_uses: true + number: GPIO1 + dir_pin: + allow_other_uses: true + number: GPIO2 max_speed: 0.1 steps/s acceleration: 10 steps/s^2 deceleration: 10 steps/s^2 @@ -556,11 +594,14 @@ cover: output: - platform: esp8266_pwm id: out - pin: D3 + pin: + number: D3 frequency: 50Hz - platform: esp8266_pwm id: out2 - pin: D4 + pin: + allow_other_uses: true + number: D4 - platform: custom type: binary lambda: |- @@ -572,7 +613,9 @@ output: - platform: sigma_delta_output id: sddac update_interval: 60s - pin: D4 + pin: + allow_other_uses: true + number: D4 turn_on_action: then: - logger.log: "Turned on" @@ -593,7 +636,9 @@ output: outputs: - id: custom_float - platform: slow_pwm - pin: GPIO5 + pin: + allow_other_uses: true + number: GPIO5 id: my_slow_pwm period: 15s restart_cycle_on_state_change: false @@ -635,12 +680,18 @@ servo: ttp229_lsf: ttp229_bsf: - sdo_pin: D2 - scl_pin: D1 + sdo_pin: + allow_other_uses: true + number: D2 + scl_pin: + allow_other_uses: true + number: D1 display: - platform: max7219digit - cs_pin: GPIO15 + cs_pin: + allow_other_uses: true + number: GPIO15 num_chips: 4 rotate_chip: 0 intensity: 10 @@ -666,10 +717,20 @@ button: name: Restart Button (Factory Default Settings) cd74hc4067: - pin_s0: GPIO12 - pin_s1: GPIO13 - pin_s2: GPIO14 - pin_s3: GPIO15 + pin_s0: + allow_other_uses: true + number: GPIO12 + pin_s1: + allow_other_uses: true + number: GPIO13 + pin_s2: + allow_other_uses: true + number: GPIO14 + pin_s3: + allow_other_uses: true + number: GPIO15 adc128s102: - cs_pin: GPIO12 + cs_pin: + allow_other_uses: true + number: GPIO12 diff --git a/tests/test3.yaml b/tests/test3.yaml index 0a405a2841..6b1757c2ad 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -223,55 +223,102 @@ uart: tx_pin: number: GPIO1 inverted: true - rx_pin: GPIO3 + allow_other_uses: true + rx_pin: + allow_other_uses: true + number: GPIO3 baud_rate: 115200 - id: uart_2 - tx_pin: GPIO4 - rx_pin: GPIO5 + tx_pin: + allow_other_uses: true + number: GPIO4 + rx_pin: + allow_other_uses: true + number: GPIO5 baud_rate: 9600 - id: uart_3 - tx_pin: GPIO4 - rx_pin: GPIO5 + tx_pin: + allow_other_uses: true + number: GPIO4 + rx_pin: + allow_other_uses: true + number: GPIO5 baud_rate: 4800 - id: uart_4 - tx_pin: GPIO4 - rx_pin: GPIO5 + tx_pin: + allow_other_uses: true + number: GPIO4 + rx_pin: + allow_other_uses: true + number: GPIO5 baud_rate: 9600 - id: uart_5 - tx_pin: GPIO4 - rx_pin: GPIO5 + tx_pin: + allow_other_uses: true + number: GPIO4 + rx_pin: + allow_other_uses: true + number: GPIO5 baud_rate: 9600 - id: uart_6 - tx_pin: GPIO4 - rx_pin: GPIO5 + tx_pin: + allow_other_uses: true + number: GPIO4 + rx_pin: + allow_other_uses: true + number: GPIO5 baud_rate: 9600 - id: uart_7 - tx_pin: GPIO4 - rx_pin: GPIO5 + tx_pin: + allow_other_uses: true + number: GPIO4 + rx_pin: + allow_other_uses: true + number: GPIO5 baud_rate: 38400 - id: uart_8 - tx_pin: GPIO4 - rx_pin: GPIO5 + tx_pin: + allow_other_uses: true + number: GPIO4 + rx_pin: + allow_other_uses: true + number: GPIO5 baud_rate: 4800 parity: NONE stop_bits: 2 # Specifically added for testing debug with no options at all. debug: - id: uart_9 - tx_pin: GPIO4 - rx_pin: GPIO5 + tx_pin: + allow_other_uses: true + number: GPIO4 + rx_pin: + allow_other_uses: true + number: GPIO5 baud_rate: 9600 - id: uart_10 - tx_pin: GPIO4 - rx_pin: GPIO5 + tx_pin: + allow_other_uses: true + number: GPIO4 + rx_pin: + allow_other_uses: true + number: GPIO5 baud_rate: 9600 - id: uart_11 - tx_pin: GPIO4 - rx_pin: GPIO5 + tx_pin: + allow_other_uses: true + number: GPIO4 + rx_pin: + allow_other_uses: true + number: GPIO5 baud_rate: 9600 - id: uart_12 - tx_pin: GPIO4 - rx_pin: GPIO5 + tx_pin: + allow_other_uses: true + number: GPIO4 + rx_pin: + allow_other_uses: true + number: GPIO5 baud_rate: 9600 modbus: @@ -748,13 +795,19 @@ binary_sensor: - platform: gpio id: bin1 - pin: 1 + pin: + allow_other_uses: true + number: 1 - platform: gpio id: bin2 - pin: 2 + pin: + allow_other_uses: true + number: 2 - platform: gpio id: bin3 - pin: 3 + pin: + allow_other_uses: true + number: 3 globals: - id: my_global_string @@ -762,11 +815,15 @@ globals: initial_value: '""' remote_receiver: - pin: GPIO12 + pin: + allow_other_uses: true + number: GPIO12 dump: [] status_led: - pin: GPIO2 + pin: + allow_other_uses: true + number: GPIO2 text_sensor: - platform: daly_bms @@ -819,13 +876,19 @@ script: switch: - platform: gpio id: gpio_switch1 - pin: 1 + pin: + allow_other_uses: true + number: 1 - platform: gpio id: gpio_switch2 - pin: 2 + pin: + allow_other_uses: true + number: 2 - platform: gpio id: gpio_switch3 - pin: 3 + pin: + allow_other_uses: true + number: 3 - platform: nextion id: r0 @@ -1023,13 +1086,18 @@ sprinkler: output: - platform: esp8266_pwm id: out - pin: D3 + pin: + number: D3 frequency: 50Hz - platform: esp8266_pwm id: out2 - pin: D4 + pin: + allow_other_uses: true + number: D4 - platform: slow_pwm - pin: GPIO5 + pin: + allow_other_uses: true + number: GPIO5 id: my_slow_pwm period: 15s restart_cycle_on_state_change: false @@ -1039,7 +1107,9 @@ e131: light: - platform: neopixelbus name: Neopixelbus Light - pin: GPIO1 + pin: + allow_other_uses: true + number: GPIO1 type: GRBW variant: SK6812 method: ESP8266_UART0 @@ -1071,6 +1141,12 @@ light: max_brightness: 500 firmware: "51.6" uart_id: uart_11 + nrst_pin: + number: 5 + allow_other_uses: true + boot0_pin: + number: 4 + allow_other_uses: true sim800l: uart_id: uart_4 @@ -1096,8 +1172,12 @@ dfplayer: logger.log: Playback finished event tm1651: id: tm1651_battery - clk_pin: D6 - dio_pin: D5 + clk_pin: + allow_other_uses: true + number: D6 + dio_pin: + allow_other_uses: true + number: D5 rf_bridge: uart_id: uart_5 @@ -1150,7 +1230,9 @@ display: lambda: 'ESP_LOGD("display","Display shows new page %u", x);' fingerprint_grow: - sensing_pin: 4 + sensing_pin: + allow_other_uses: true + number: 4 password: 0x12FE37DC new_password: 0xA65B9840 on_finger_scan_matched: @@ -1184,7 +1266,9 @@ dsmr: decryption_key: 00112233445566778899aabbccddeeff uart_id: uart_6 max_telegram_length: 1000 - request_pin: D5 + request_pin: + allow_other_uses: true + number: D5 request_interval: 20s receive_timeout: 100ms @@ -1197,8 +1281,11 @@ qr_code: value: https://esphome.io/index.html lightwaverf: - read_pin: 13 - write_pin: 14 + read_pin: + number: 13 + write_pin: + allow_other_uses: true + number: 14 alarm_control_panel: - platform: template diff --git a/tests/test4.yaml b/tests/test4.yaml index 65aab7cdde..a3dfc6aceb 100644 --- a/tests/test4.yaml +++ b/tests/test4.yaml @@ -10,11 +10,17 @@ substitutions: ethernet: type: LAN8720 - mdc_pin: GPIO23 - mdio_pin: GPIO25 + mdc_pin: + allow_other_uses: true + number: GPIO23 + mdio_pin: + allow_other_uses: true + number: GPIO25 clk_mode: GPIO0_IN phy_addr: 0 - power_pin: GPIO25 + power_pin: + allow_other_uses: true + number: GPIO25 manual_ip: static_ip: 192.168.178.56 gateway: 192.168.178.1 @@ -34,29 +40,49 @@ mqtt: api: i2c: - sda: 21 - scl: 22 + sda: + allow_other_uses: true + number: 21 + scl: + allow_other_uses: true + number: 22 scan: false spi: - id: spi_id_1 - clk_pin: GPIO21 - mosi_pin: GPIO22 - miso_pin: GPIO23 + clk_pin: + allow_other_uses: true + number: GPIO21 + mosi_pin: + allow_other_uses: true + number: GPIO22 + miso_pin: + allow_other_uses: true + number: GPIO23 interface: hardware - id: spi_id_2 - clk_pin: GPIO32 - mosi_pin: GPIO33 + clk_pin: + number: GPIO32 + mosi_pin: + number: GPIO33 interface: hardware uart: - id: uart115200 - tx_pin: GPIO22 - rx_pin: GPIO23 + tx_pin: + allow_other_uses: true + number: GPIO22 + rx_pin: + allow_other_uses: true + number: GPIO23 baud_rate: 115200 - id: uart9600 - tx_pin: GPIO22 - rx_pin: GPIO23 + tx_pin: + allow_other_uses: true + number: GPIO22 + rx_pin: + allow_other_uses: true + number: GPIO23 baud_rate: 9600 - id: uart_he60r tx_pin: 22 @@ -91,6 +117,7 @@ tuya: status_pin: number: 14 inverted: true + allow_other_uses: true select: - platform: tuya @@ -117,7 +144,9 @@ sx1509: mcp3204: spi_id: spi_id_1 - cs_pin: GPIO23 + cs_pin: + allow_other_uses: true + number: GPIO23 dac7678: address: 0x4A @@ -510,7 +539,9 @@ light: id: led_matrix_32x8 name: led_matrix_32x8 chipset: WS2812B - pin: GPIO15 + pin: + allow_other_uses: true + number: GPIO15 num_leds: 256 rgb_order: GRB default_transition_length: 0s @@ -566,20 +597,36 @@ display: - platform: waveshare_epaper spi_id: spi_id_1 - cs_pin: GPIO23 - dc_pin: GPIO23 - busy_pin: GPIO23 - reset_pin: GPIO23 + cs_pin: + allow_other_uses: true + number: GPIO23 + dc_pin: + allow_other_uses: true + number: GPIO23 + busy_pin: + allow_other_uses: true + number: GPIO23 + reset_pin: + allow_other_uses: true + number: GPIO23 model: 2.13in-ttgo-b1 full_update_every: 30 lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); - platform: waveshare_epaper spi_id: spi_id_1 - cs_pin: GPIO23 - dc_pin: GPIO23 - busy_pin: GPIO23 - reset_pin: GPIO23 + cs_pin: + allow_other_uses: true + number: GPIO23 + dc_pin: + allow_other_uses: true + number: GPIO23 + busy_pin: + allow_other_uses: true + number: GPIO23 + reset_pin: + allow_other_uses: true + number: GPIO23 model: 2.90in full_update_every: 30 reset_duration: 200ms @@ -587,20 +634,36 @@ display: it.rectangle(0, 0, it.get_width(), it.get_height()); - platform: waveshare_epaper spi_id: spi_id_1 - cs_pin: GPIO23 - dc_pin: GPIO23 - busy_pin: GPIO23 - reset_pin: GPIO23 + cs_pin: + allow_other_uses: true + number: GPIO23 + dc_pin: + allow_other_uses: true + number: GPIO23 + busy_pin: + allow_other_uses: true + number: GPIO23 + reset_pin: + allow_other_uses: true + number: GPIO23 model: 2.90inv2 full_update_every: 30 lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); - platform: waveshare_epaper spi_id: spi_id_1 - cs_pin: GPIO23 - dc_pin: GPIO23 - busy_pin: GPIO23 - reset_pin: GPIO23 + cs_pin: + allow_other_uses: true + number: GPIO23 + dc_pin: + allow_other_uses: true + number: GPIO23 + busy_pin: + allow_other_uses: true + number: GPIO23 + reset_pin: + allow_other_uses: true + number: GPIO23 model: 1.54in-m5coreink-m09 lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); @@ -610,15 +673,54 @@ display: partial_updating: false update_interval: 60s - ckv_pin: GPIO1 - sph_pin: GPIO1 - gmod_pin: GPIO1 - gpio0_enable_pin: GPIO1 - oe_pin: GPIO1 - spv_pin: GPIO1 - powerup_pin: GPIO1 - wakeup_pin: GPIO1 - vcom_pin: GPIO1 + display_data_1_pin: + number: 5 + allow_other_uses: true + display_data_2_pin: + number: 18 + allow_other_uses: true + display_data_3_pin: + number: 19 + allow_other_uses: true + display_data_5_pin: + number: 25 + allow_other_uses: true + display_data_4_pin: + number: 23 + allow_other_uses: true + display_data_6_pin: + number: 26 + allow_other_uses: true + display_data_7_pin: + number: 27 + allow_other_uses: true + ckv_pin: + allow_other_uses: true + number: GPIO1 + sph_pin: + allow_other_uses: true + number: GPIO1 + gmod_pin: + allow_other_uses: true + number: GPIO1 + gpio0_enable_pin: + allow_other_uses: true + number: GPIO1 + oe_pin: + allow_other_uses: true + number: GPIO1 + spv_pin: + allow_other_uses: true + number: GPIO1 + powerup_pin: + allow_other_uses: true + number: GPIO1 + wakeup_pin: + allow_other_uses: true + number: GPIO1 + vcom_pin: + allow_other_uses: true + number: GPIO1 number: - platform: tuya @@ -706,18 +808,54 @@ output: id: dac7678_1_ch7 esp32_camera: name: ESP-32 Camera - data_pins: [GPIO17, GPIO35, GPIO34, GPIO5, GPIO39, GPIO18, GPIO36, GPIO19] - vsync_pin: GPIO22 - href_pin: GPIO26 - pixel_clock_pin: GPIO21 + data_pins: + - number: GPIO17 + allow_other_uses: true + - number: GPIO35 + allow_other_uses: true + - + number: GPIO34 + - + number: GPIO5 + allow_other_uses: true + - + number: GPIO39 + - + number: GPIO18 + allow_other_uses: true + - + number: GPIO36 + allow_other_uses: true + - + number: GPIO19 + allow_other_uses: true + vsync_pin: + allow_other_uses: true + number: GPIO22 + href_pin: + allow_other_uses: true + number: GPIO26 + pixel_clock_pin: + allow_other_uses: true + number: GPIO21 external_clock: - pin: GPIO27 + pin: + allow_other_uses: true + number: GPIO27 frequency: 20MHz i2c_pins: - sda: GPIO25 - scl: GPIO23 - reset_pin: GPIO15 - power_down_pin: GPIO1 + sda: + allow_other_uses: true + number: GPIO25 + scl: + allow_other_uses: true + number: GPIO23 + reset_pin: + allow_other_uses: true + number: GPIO15 + power_down_pin: + allow_other_uses: true + number: GPIO1 resolution: 640x480 jpeg_quality: 10 @@ -748,8 +886,12 @@ button: touchscreen: - platform: ektf2232 - interrupt_pin: GPIO36 - rts_pin: GPIO5 + interrupt_pin: + allow_other_uses: true + number: GPIO36 + rts_pin: + allow_other_uses: true + number: GPIO5 display: inkplate_display on_touch: - logger.log: @@ -759,8 +901,11 @@ touchscreen: - platform: xpt2046 id: xpt_touchscreen spi_id: spi_id_2 - cs_pin: 17 - interrupt_pin: 16 + cs_pin: + allow_other_uses: true + number: 17 + interrupt_pin: + number: 16 display: inkplate_display update_interval: 50ms report_interval: 1s @@ -777,7 +922,9 @@ touchscreen: - platform: lilygo_t5_47 id: lilygo_touchscreen - interrupt_pin: GPIO36 + interrupt_pin: + allow_other_uses: true + number: GPIO36 display: inkplate_display on_touch: - logger.log: @@ -789,16 +936,26 @@ touchscreen: i2s_audio: - i2s_lrclk_pin: GPIO26 - i2s_bclk_pin: GPIO27 - i2s_mclk_pin: GPIO25 + i2s_lrclk_pin: + allow_other_uses: true + number: GPIO26 + i2s_bclk_pin: + allow_other_uses: true + number: GPIO27 + i2s_mclk_pin: + allow_other_uses: true + number: GPIO25 media_player: - platform: i2s_audio name: None dac_type: external - i2s_dout_pin: GPIO25 - mute_pin: GPIO14 + i2s_dout_pin: + allow_other_uses: true + number: GPIO25 + mute_pin: + allow_other_uses: true + number: GPIO14 on_state: - media_player.play: - media_player.play_media: http://localhost/media.mp3 @@ -827,12 +984,16 @@ prometheus: microphone: - platform: i2s_audio id: mic_id_adc - adc_pin: GPIO35 + adc_pin: + allow_other_uses: true + number: GPIO35 adc_type: internal - platform: i2s_audio id: mic_id_external - i2s_din_pin: GPIO23 + i2s_din_pin: + allow_other_uses: true + number: GPIO23 adc_type: external pdm: false @@ -840,7 +1001,9 @@ speaker: - platform: i2s_audio id: speaker_id dac_type: external - i2s_dout_pin: GPIO25 + i2s_dout_pin: + allow_other_uses: true + number: GPIO25 mode: mono voice_assistant: diff --git a/tests/test5.yaml b/tests/test5.yaml index 46cedcabd2..bf4247fb92 100644 --- a/tests/test5.yaml +++ b/tests/test5.yaml @@ -42,17 +42,27 @@ uart: baud_rate: 9600 - id: uart_2 tx_pin: + allow_other_uses: true number: 17 inverted: true - rx_pin: 16 + rx_pin: + allow_other_uses: true + number: 16 baud_rate: 19200 i2c: + sda: + allow_other_uses: true + number: 21 + scl: + number: 22 frequency: 100khz modbus: uart_id: uart_1 - flow_control_pin: 5 + flow_control_pin: + allow_other_uses: true + number: 5 id: mod_bus1 modbus_controller: @@ -214,9 +224,15 @@ binary_sensor: lambda: return x[0] & 1; tlc5947: - data_pin: GPIO12 - clock_pin: GPIO14 - lat_pin: GPIO15 + data_pin: + number: GPIO12 + allow_other_uses: true + clock_pin: + allow_other_uses: true + number: GPIO14 + lat_pin: + allow_other_uses: true + number: GPIO15 gp8403: - id: gp8403_5v @@ -614,7 +630,9 @@ switch: display: - platform: tm1638 id: primarydisplay - stb_pin: 5 #TM1638 STB + stb_pin: + allow_other_uses: true + number: 5 #TM1638 STB clk_pin: 18 #TM1638 CLK dio_pin: 23 #TM1638 DIO update_interval: 5s @@ -659,8 +677,12 @@ text_sensor: sn74hc165: id: sn74hc165_hub - data_pin: GPIO12 - clock_pin: GPIO14 + data_pin: + allow_other_uses: true + number: GPIO12 + clock_pin: + allow_other_uses: true + number: GPIO14 load_pin: GPIO27 clock_inhibit_pin: GPIO26 sr_count: 4 @@ -668,11 +690,17 @@ sn74hc165: matrix_keypad: id: keypad rows: - - pin: 21 + - pin: + allow_other_uses: true + number: 21 - pin: 19 columns: - - pin: 17 - - pin: 16 + - pin: + allow_other_uses: true + number: 17 + - pin: + allow_other_uses: true + number: 16 keys: "1234" has_pulldowns: true @@ -692,7 +720,9 @@ light: chipset: ws2812 - platform: esp32_rmt_led_strip id: led_strip2 - pin: 15 + pin: + allow_other_uses: true + number: 15 num_leds: 60 rmt_channel: 2 rgb_order: RGB diff --git a/tests/test8.yaml b/tests/test8.yaml index cbac2cb833..5e4a41080a 100644 --- a/tests/test8.yaml +++ b/tests/test8.yaml @@ -57,7 +57,9 @@ display: model: ili9342 cs_pin: GPIO5 dc_pin: GPIO4 - reset_pin: GPIO48 + reset_pin: + number: GPIO48 + allow_other_uses: true i2c: scl: GPIO18 @@ -68,7 +70,10 @@ touchscreen: interrupt_pin: number: GPIO3 ignore_strapping_warning: true - reset_pin: GPIO48 + allow_other_uses: false + reset_pin: + number: GPIO48 + allow_other_uses: true binary_sensor: - platform: tt21100 From 4d3730b50eb9d8a0997d2b3289d5873c8fa1911c Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Tue, 5 Dec 2023 02:09:44 +0100 Subject: [PATCH 40/89] Nextion support to idf with `cinttypes` (#5876) --- esphome/components/nextion/nextion.cpp | 1 + esphome/components/nextion/nextion_commands.cpp | 1 + esphome/components/nextion/nextion_upload_idf.cpp | 1 + 3 files changed, 3 insertions(+) diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index fcc0d97655..15f1d02841 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -2,6 +2,7 @@ #include "esphome/core/util.h" #include "esphome/core/log.h" #include "esphome/core/application.h" +#include namespace esphome { namespace nextion { diff --git a/esphome/components/nextion/nextion_commands.cpp b/esphome/components/nextion/nextion_commands.cpp index 0722ed6f4e..3cefc6618a 100644 --- a/esphome/components/nextion/nextion_commands.cpp +++ b/esphome/components/nextion/nextion_commands.cpp @@ -1,6 +1,7 @@ #include "nextion.h" #include "esphome/core/util.h" #include "esphome/core/log.h" +#include namespace esphome { namespace nextion { diff --git a/esphome/components/nextion/nextion_upload_idf.cpp b/esphome/components/nextion/nextion_upload_idf.cpp index 58f5659ade..84da7bb1be 100644 --- a/esphome/components/nextion/nextion_upload_idf.cpp +++ b/esphome/components/nextion/nextion_upload_idf.cpp @@ -11,6 +11,7 @@ #include #include +#include namespace esphome { namespace nextion { From 1d0fb59208cb8e2ede49ae004a9d119e44dcb9a8 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Mon, 4 Dec 2023 20:50:40 -0600 Subject: [PATCH 41/89] Fix test4.yaml after #5666 (#5890) --- tests/test4.yaml | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/tests/test4.yaml b/tests/test4.yaml index a3dfc6aceb..05870d3b8f 100644 --- a/tests/test4.yaml +++ b/tests/test4.yaml @@ -79,18 +79,21 @@ uart: - id: uart9600 tx_pin: allow_other_uses: true - number: GPIO22 + number: GPIO25 rx_pin: allow_other_uses: true - number: GPIO23 + number: GPIO26 baud_rate: 9600 - id: uart_he60r - tx_pin: 22 - rx_pin: 23 + tx_pin: + number: GPIO18 + allow_other_uses: true + rx_pin: + number: GPIO36 + allow_other_uses: true baud_rate: 1200 parity: EVEN - ota: safe_mode: true port: 3286 @@ -115,7 +118,7 @@ tuya: time_id: sntp_time uart_id: uart115200 status_pin: - number: 14 + number: GPIO5 inverted: true allow_other_uses: true @@ -954,7 +957,6 @@ media_player: allow_other_uses: true number: GPIO25 mute_pin: - allow_other_uses: true number: GPIO14 on_state: - media_player.play: From 7fd08fb8163206534ca94251669b1cdc7f2cc655 Mon Sep 17 00:00:00 2001 From: Fabio Pugliese Ornellas Date: Tue, 5 Dec 2023 04:17:43 +0000 Subject: [PATCH 42/89] Fix template text component length check (#5881) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Co-authored-by: Keith Burzinski --- esphome/components/template/text/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/template/text/__init__.py b/esphome/components/template/text/__init__.py index a82664ee15..0f228a3c6b 100644 --- a/esphome/components/template/text/__init__.py +++ b/esphome/components/template/text/__init__.py @@ -39,8 +39,8 @@ def validate(config): ) with cv.prepend_path(CONF_MIN_LENGTH): - if config[CONF_MIN_LENGTH] >= config[CONF_MAX_LENGTH]: - raise cv.Invalid("min_length must be less than max_length") + if config[CONF_MIN_LENGTH] > config[CONF_MAX_LENGTH]: + raise cv.Invalid("min_length must be less than or equal to max_length") return config From 89d7cdf86b0c71531e3867b33a79e607fed95bc0 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Tue, 5 Dec 2023 02:21:12 -0600 Subject: [PATCH 43/89] RC522 - Fix error counter error (#5873) --- esphome/components/rc522/rc522.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/esphome/components/rc522/rc522.cpp b/esphome/components/rc522/rc522.cpp index 4e74020e4c..e2146dd14e 100644 --- a/esphome/components/rc522/rc522.cpp +++ b/esphome/components/rc522/rc522.cpp @@ -397,8 +397,10 @@ RC522::StatusCode RC522::await_transceive_() { back_length_ = 0; ESP_LOGW(TAG, "Communication with the MFRC522 might be down, reset in %d", 10 - error_counter_); // todo: trigger reset? - if (error_counter_++ > 10) + if (error_counter_++ >= 10) { setup(); + error_counter_ = 0; // reset the error counter + } return STATUS_TIMEOUT; } From 8f70ef24a22f0c03b4e1e3c4aef84cf748b56688 Mon Sep 17 00:00:00 2001 From: Subhash Chandra Date: Wed, 6 Dec 2023 06:34:17 +0530 Subject: [PATCH 44/89] feat(packages): support removing components (#5821) --- .devcontainer/devcontainer.json | 1 + esphome/config.py | 39 ++++- esphome/config_helpers.py | 28 ++- esphome/config_validation.py | 6 +- esphome/yaml_util.py | 7 +- .../component_tests/packages/test_packages.py | 164 +++++++++++++++++- 6 files changed, 240 insertions(+), 5 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index c8f94cb6bb..7abcb43417 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -37,6 +37,7 @@ "!secret scalar", "!lambda scalar", "!extend scalar", + "!remove scalar", "!include_dir_named scalar", "!include_dir_list scalar", "!include_dir_merge_list scalar", diff --git a/esphome/config.py b/esphome/config.py index 6f644cee14..3461223490 100644 --- a/esphome/config.py +++ b/esphome/config.py @@ -26,7 +26,7 @@ from esphome.core import CORE, EsphomeError from esphome.helpers import indent from esphome.util import safe_print, OrderedDict -from esphome.config_helpers import Extend +from esphome.config_helpers import Extend, Remove from esphome.loader import get_component, get_platform, ComponentManifest from esphome.yaml_util import is_secret, ESPHomeDataBase, ESPForceValue from esphome.voluptuous_schema import ExtraKeysInvalid @@ -345,6 +345,12 @@ class LoadValidationStep(ConfigValidationStep): path + [CONF_ID], ) continue + if isinstance(p_id, Remove): + result.add_str_error( + f"Source for removal of ID '{p_id.value}' was not found.", + path + [CONF_ID], + ) + continue result.add_str_error("No platform specified! See 'platform' key.", path) continue # Remove temp output path and construct new one @@ -634,6 +640,35 @@ class IDPassValidationStep(ConfigValidationStep): ) +class RemoveReferenceValidationStep(ConfigValidationStep): + """ + Make sure all !remove references have been removed from the config. + Any left overs mean the merge step couldn't find corresponding previously existing id/key + """ + + def run(self, result: Config) -> None: + if result.errors: + # If result already has errors, skip this step + return + + def recursive_check_remove_tag(config: Config, path: ConfigPath = None): + path = path or [] + + if isinstance(config, Remove): + result.add_str_error( + f"Source for removal at '{'->'.join([str(p) for p in path])}' was not found.", + path, + ) + elif isinstance(config, list): + for i, item in enumerate(config): + recursive_check_remove_tag(item, path + [i]) + elif isinstance(config, dict): + for key, value in config.items(): + recursive_check_remove_tag(value, path + [key]) + + recursive_check_remove_tag(result) + + class FinalValidateValidationStep(ConfigValidationStep): """Run final_validate_schema for all components.""" @@ -771,6 +806,8 @@ def validate_config(config, command_line_substitutions) -> Config: result.add_validation_step(IDPassValidationStep()) result.add_validation_step(PinUseValidationCheck()) + result.add_validation_step(RemoveReferenceValidationStep()) + result.run_validation_steps() return result diff --git a/esphome/config_helpers.py b/esphome/config_helpers.py index e1d63775bb..ac52c6ede2 100644 --- a/esphome/config_helpers.py +++ b/esphome/config_helpers.py @@ -22,6 +22,22 @@ class Extend: return isinstance(b, Extend) and self.value == b.value +class Remove: + def __init__(self, value=None): + self.value = value + + def __str__(self): + return f"!remove {self.value}" + + def __eq__(self, b): + """ + Check if two Remove objects contain the same ID. + + Only used in unit tests. + """ + return isinstance(b, Remove) and self.value == b.value + + def read_config_file(path: str) -> str: if CORE.vscode and ( not CORE.ace or os.path.abspath(path) == os.path.abspath(CORE.config_path) @@ -48,7 +64,10 @@ def merge_config(full_old, full_new): return new res = old.copy() for k, v in new.items(): - res[k] = merge(old[k], v) if k in old else v + if isinstance(v, Remove) and k in old: + del res[k] + else: + res[k] = merge(old[k], v) if k in old else v return res if isinstance(new, list): if not isinstance(old, list): @@ -59,6 +78,7 @@ def merge_config(full_old, full_new): for i, v in enumerate(res) if CONF_ID in v and isinstance(v[CONF_ID], str) } + ids_to_delete = [] for v in new: if CONF_ID in v: new_id = v[CONF_ID] @@ -68,9 +88,15 @@ def merge_config(full_old, full_new): v[CONF_ID] = new_id res[ids[new_id]] = merge(res[ids[new_id]], v) continue + elif isinstance(new_id, Remove): + new_id = new_id.value + if new_id in ids: + ids_to_delete.append(ids[new_id]) + continue else: ids[new_id] = len(res) res.append(v) + res = [v for i, v in enumerate(res) if i not in ids_to_delete] return res if new is None: return old diff --git a/esphome/config_validation.py b/esphome/config_validation.py index eb347d0a4d..ad2ee11512 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -13,7 +13,7 @@ import voluptuous as vol from esphome import core import esphome.codegen as cg -from esphome.config_helpers import Extend +from esphome.config_helpers import Extend, Remove from esphome.const import ( ALLOWED_NAME_CHARS, CONF_AVAILABILITY, @@ -532,6 +532,10 @@ def declare_id(type): if isinstance(value, Extend): raise Invalid(f"Source for extension of ID '{value.value}' was not found.") + + if isinstance(value, Remove): + raise Invalid(f"Source for Removal of ID '{value.value}' was not found.") + return core.ID(validate_id_name(value), is_declaration=True, type=type) return validator diff --git a/esphome/yaml_util.py b/esphome/yaml_util.py index a954415d12..f0f755dd61 100644 --- a/esphome/yaml_util.py +++ b/esphome/yaml_util.py @@ -10,7 +10,7 @@ import yaml import yaml.constructor from esphome import core -from esphome.config_helpers import read_config_file, Extend +from esphome.config_helpers import read_config_file, Extend, Remove from esphome.core import ( EsphomeError, IPAddress, @@ -362,6 +362,10 @@ class ESPHomeLoader(FastestAvailableSafeLoader): def construct_extend(self, node): return Extend(str(node.value)) + @_add_data_ref + def construct_remove(self, node): + return Remove(str(node.value)) + ESPHomeLoader.add_constructor("tag:yaml.org,2002:int", ESPHomeLoader.construct_yaml_int) ESPHomeLoader.add_constructor( @@ -394,6 +398,7 @@ ESPHomeLoader.add_constructor( ESPHomeLoader.add_constructor("!lambda", ESPHomeLoader.construct_lambda) ESPHomeLoader.add_constructor("!force", ESPHomeLoader.construct_force) ESPHomeLoader.add_constructor("!extend", ESPHomeLoader.construct_extend) +ESPHomeLoader.add_constructor("!remove", ESPHomeLoader.construct_remove) def load_yaml(fname, clear_secrets=True): diff --git a/tests/component_tests/packages/test_packages.py b/tests/component_tests/packages/test_packages.py index 0e24d78f5c..01cf55872c 100644 --- a/tests/component_tests/packages/test_packages.py +++ b/tests/component_tests/packages/test_packages.py @@ -20,7 +20,7 @@ from esphome.const import ( CONF_WIFI, ) from esphome.components.packages import do_packages_pass -from esphome.config_helpers import Extend +from esphome.config_helpers import Extend, Remove import esphome.config_validation as cv # Test strings @@ -349,3 +349,165 @@ def test_package_merge_by_missing_id(): actual = do_packages_pass(config) assert actual == expected + + +def test_package_list_remove_by_id(): + """ + Ensures that components with matching IDs are removed correctly. + + In this test, two sensors are defined in a package, and one of them is removed at the top level. + """ + config = { + CONF_PACKAGES: { + "package_sensors": { + CONF_SENSOR: [ + { + CONF_ID: TEST_SENSOR_ID_1, + CONF_PLATFORM: TEST_SENSOR_PLATFORM_1, + CONF_NAME: TEST_SENSOR_NAME_1, + }, + { + CONF_ID: TEST_SENSOR_ID_2, + CONF_PLATFORM: TEST_SENSOR_PLATFORM_1, + CONF_NAME: TEST_SENSOR_NAME_2, + }, + ] + }, + # "package2": { + # CONF_SENSOR: [ + # { + # CONF_ID: Remove(TEST_SENSOR_ID_1), + # } + # ], + # }, + }, + CONF_SENSOR: [ + { + CONF_ID: Remove(TEST_SENSOR_ID_1), + }, + ], + } + + expected = { + CONF_SENSOR: [ + { + CONF_ID: TEST_SENSOR_ID_2, + CONF_PLATFORM: TEST_SENSOR_PLATFORM_1, + CONF_NAME: TEST_SENSOR_NAME_2, + }, + ] + } + + actual = do_packages_pass(config) + assert actual == expected + + +def test_multiple_package_list_remove_by_id(): + """ + Ensures that components with matching IDs are removed correctly. + + In this test, two sensors are defined in a package, and one of them is removed in another package. + """ + config = { + CONF_PACKAGES: { + "package_sensors": { + CONF_SENSOR: [ + { + CONF_ID: TEST_SENSOR_ID_1, + CONF_PLATFORM: TEST_SENSOR_PLATFORM_1, + CONF_NAME: TEST_SENSOR_NAME_1, + }, + { + CONF_ID: TEST_SENSOR_ID_2, + CONF_PLATFORM: TEST_SENSOR_PLATFORM_1, + CONF_NAME: TEST_SENSOR_NAME_2, + }, + ] + }, + "package2": { + CONF_SENSOR: [ + { + CONF_ID: Remove(TEST_SENSOR_ID_1), + } + ], + }, + }, + } + + expected = { + CONF_SENSOR: [ + { + CONF_ID: TEST_SENSOR_ID_2, + CONF_PLATFORM: TEST_SENSOR_PLATFORM_1, + CONF_NAME: TEST_SENSOR_NAME_2, + }, + ] + } + + actual = do_packages_pass(config) + assert actual == expected + + +def test_package_dict_remove_by_id(basic_wifi, basic_esphome): + """ + Ensures that components with missing IDs are removed from dict. + """ + """ + Ensures that the top-level configuration takes precedence over duplicate keys defined in a package. + + In this test, CONF_SSID should be overwritten by that defined in the top-level config. + """ + config = { + CONF_ESPHOME: basic_esphome, + CONF_PACKAGES: {"network": {CONF_WIFI: basic_wifi}}, + CONF_WIFI: Remove(), + } + + expected = { + CONF_ESPHOME: basic_esphome, + } + + actual = do_packages_pass(config) + assert actual == expected + + +def test_package_remove_by_missing_id(): + """ + Ensures that components with missing IDs are not merged. + """ + + config = { + CONF_PACKAGES: { + "sensors": { + CONF_SENSOR: [ + {CONF_ID: TEST_SENSOR_ID_1, CONF_FILTERS: [{CONF_MULTIPLY: 42.0}]}, + ] + } + }, + "missing_key": Remove(), + CONF_SENSOR: [ + {CONF_ID: TEST_SENSOR_ID_1, CONF_FILTERS: [{CONF_MULTIPLY: 10.0}]}, + {CONF_ID: Remove(TEST_SENSOR_ID_2), CONF_FILTERS: [{CONF_OFFSET: 146.0}]}, + ], + } + + expected = { + "missing_key": Remove(), + CONF_SENSOR: [ + { + CONF_ID: TEST_SENSOR_ID_1, + CONF_FILTERS: [{CONF_MULTIPLY: 42.0}], + }, + { + CONF_ID: TEST_SENSOR_ID_1, + CONF_FILTERS: [{CONF_MULTIPLY: 10.0}], + }, + { + CONF_ID: Remove(TEST_SENSOR_ID_2), + CONF_FILTERS: [{CONF_OFFSET: 146.0}], + }, + ], + } + + actual = do_packages_pass(config) + assert actual == expected From be07463fbd8bdb8464cd0d6f48bf46678148061b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 5 Dec 2023 15:06:05 -1000 Subject: [PATCH 45/89] dashboard: Add some basic tests for the dashboard (#5870) --- tests/dashboard/common.py | 6 ++ tests/dashboard/fixtures/conf/pico.yaml | 47 +++++++++++++++ tests/dashboard/test_web_server.py | 80 +++++++++++++++++++++++++ 3 files changed, 133 insertions(+) create mode 100644 tests/dashboard/common.py create mode 100644 tests/dashboard/fixtures/conf/pico.yaml create mode 100644 tests/dashboard/test_web_server.py diff --git a/tests/dashboard/common.py b/tests/dashboard/common.py new file mode 100644 index 0000000000..f84c03aad8 --- /dev/null +++ b/tests/dashboard/common.py @@ -0,0 +1,6 @@ +import pathlib + + +def get_fixture_path(filename: str) -> pathlib.Path: + """Get path of fixture.""" + return pathlib.Path(__file__).parent.joinpath("fixtures", filename) diff --git a/tests/dashboard/fixtures/conf/pico.yaml b/tests/dashboard/fixtures/conf/pico.yaml new file mode 100644 index 0000000000..cf5b5b75bf --- /dev/null +++ b/tests/dashboard/fixtures/conf/pico.yaml @@ -0,0 +1,47 @@ +substitutions: + name: picoproxy + friendly_name: Pico Proxy + +esphome: + name: ${name} + friendly_name: ${friendly_name} + project: + name: esphome.bluetooth-proxy + version: "1.0" + +esp32: + board: esp32dev + framework: + type: esp-idf + +wifi: + ap: + +api: +logger: +ota: +improv_serial: + +dashboard_import: + package_import_url: github://esphome/firmware/bluetooth-proxy/esp32-generic.yaml@main + +button: + - platform: factory_reset + id: resetf + - platform: safe_mode + name: Safe Mode Boot + entity_category: diagnostic + +sensor: + - platform: template + id: pm11 + name: "pm 1.0µm" + lambda: return 1.0; + - platform: template + id: pm251 + name: "pm 2.5µm" + lambda: return 2.5; + - platform: template + id: pm101 + name: "pm 10µm" + lambda: return 10; diff --git a/tests/dashboard/test_web_server.py b/tests/dashboard/test_web_server.py new file mode 100644 index 0000000000..a61850abf3 --- /dev/null +++ b/tests/dashboard/test_web_server.py @@ -0,0 +1,80 @@ +from __future__ import annotations + +import asyncio +import json +import os +from unittest.mock import Mock + +import pytest +import pytest_asyncio +from tornado.httpclient import AsyncHTTPClient, HTTPResponse +from tornado.httpserver import HTTPServer +from tornado.ioloop import IOLoop +from tornado.testing import bind_unused_port + +from esphome.dashboard import web_server +from esphome.dashboard.core import DASHBOARD + +from .common import get_fixture_path + + +class DashboardTestHelper: + def __init__(self, io_loop: IOLoop, client: AsyncHTTPClient, port: int) -> None: + self.io_loop = io_loop + self.client = client + self.port = port + + async def fetch(self, path: str, **kwargs) -> HTTPResponse: + """Get a response for the given path.""" + if path.lower().startswith(("http://", "https://")): + url = path + else: + url = f"http://127.0.0.1:{self.port}{path}" + future = self.client.fetch(url, raise_error=True, **kwargs) + result = await future + return result + + +@pytest_asyncio.fixture() +async def dashboard() -> DashboardTestHelper: + sock, port = bind_unused_port() + args = Mock( + ha_addon=True, + configuration=get_fixture_path("conf"), + port=port, + ) + DASHBOARD.settings.parse_args(args) + app = web_server.make_app() + http_server = HTTPServer(app) + http_server.add_sockets([sock]) + await DASHBOARD.async_setup() + os.environ["DISABLE_HA_AUTHENTICATION"] = "1" + assert DASHBOARD.settings.using_password is False + assert DASHBOARD.settings.on_ha_addon is True + assert DASHBOARD.settings.using_auth is False + task = asyncio.create_task(DASHBOARD.async_run()) + client = AsyncHTTPClient() + io_loop = IOLoop(make_current=False) + yield DashboardTestHelper(io_loop, client, port) + task.cancel() + sock.close() + client.close() + io_loop.close() + + +@pytest.mark.asyncio +async def test_main_page(dashboard: DashboardTestHelper) -> None: + response = await dashboard.fetch("/") + assert response.code == 200 + + +@pytest.mark.asyncio +async def test_devices_page(dashboard: DashboardTestHelper) -> None: + response = await dashboard.fetch("/devices") + assert response.code == 200 + assert response.headers["content-type"] == "application/json" + json_data = json.loads(response.body.decode()) + configured_devices = json_data["configured"] + first_device = configured_devices[0] + assert first_device["name"] == "pico" + assert first_device["configuration"] == "pico.yaml" From f026f49415e0b3e6c7ab01d708cf91e5868f0275 Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Wed, 6 Dec 2023 07:55:07 +0100 Subject: [PATCH 46/89] Nextion exit reparse mode on startup (#5868) Co-authored-by: Keith Burzinski --- esphome/components/nextion/base_component.py | 1 + esphome/components/nextion/display.py | 7 +++++-- esphome/components/nextion/nextion.cpp | 7 ++++++- esphome/components/nextion/nextion.h | 17 +++++++++++++++++ esphome/components/nextion/nextion_commands.cpp | 1 + 5 files changed, 30 insertions(+), 3 deletions(-) diff --git a/esphome/components/nextion/base_component.py b/esphome/components/nextion/base_component.py index f1c3a1d227..5bd6643cb8 100644 --- a/esphome/components/nextion/base_component.py +++ b/esphome/components/nextion/base_component.py @@ -29,6 +29,7 @@ CONF_BACKGROUND_PRESSED_COLOR = "background_pressed_color" CONF_FOREGROUND_COLOR = "foreground_color" CONF_FOREGROUND_PRESSED_COLOR = "foreground_pressed_color" CONF_FONT_ID = "font_id" +CONF_EXIT_REPARSE_ON_START = "exit_reparse_on_start" def NextionName(value): diff --git a/esphome/components/nextion/display.py b/esphome/components/nextion/display.py index 92e85ad28a..fd61dfa2be 100644 --- a/esphome/components/nextion/display.py +++ b/esphome/components/nextion/display.py @@ -21,6 +21,7 @@ from .base_component import ( CONF_WAKE_UP_PAGE, CONF_START_UP_PAGE, CONF_AUTO_WAKE_ON_TOUCH, + CONF_EXIT_REPARSE_ON_START, ) CODEOWNERS = ["@senexcrenshaw"] @@ -69,6 +70,7 @@ CONFIG_SCHEMA = ( cv.Optional(CONF_WAKE_UP_PAGE): cv.positive_int, cv.Optional(CONF_START_UP_PAGE): cv.positive_int, cv.Optional(CONF_AUTO_WAKE_ON_TOUCH, default=True): cv.boolean, + cv.Optional(CONF_EXIT_REPARSE_ON_START, default=False): cv.boolean, } ) .extend(cv.polling_component_schema("5s")) @@ -106,8 +108,9 @@ async def to_code(config): if CONF_START_UP_PAGE in config: cg.add(var.set_start_up_page_internal(config[CONF_START_UP_PAGE])) - if CONF_AUTO_WAKE_ON_TOUCH in config: - cg.add(var.set_auto_wake_on_touch_internal(config[CONF_AUTO_WAKE_ON_TOUCH])) + cg.add(var.set_auto_wake_on_touch_internal(config[CONF_AUTO_WAKE_ON_TOUCH])) + + cg.add(var.set_exit_reparse_on_start_internal(config[CONF_EXIT_REPARSE_ON_START])) await display.register_display(var, config) diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index 15f1d02841..29dcfa6cef 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -48,6 +48,9 @@ bool Nextion::check_connect_() { this->ignore_is_setup_ = true; this->send_command_("boguscommand=0"); // bogus command. needed sometimes after updating + if (this->exit_reparse_on_start_) { + this->send_command_("DRAKJHSUYDGBNCJHGJKSHBDN"); + } this->send_command_("connect"); this->comok_sent_ = millis(); @@ -127,7 +130,8 @@ void Nextion::dump_config() { ESP_LOGCONFIG(TAG, " Firmware Version: %s", this->firmware_version_.c_str()); ESP_LOGCONFIG(TAG, " Serial Number: %s", this->serial_number_.c_str()); ESP_LOGCONFIG(TAG, " Flash Size: %s", this->flash_size_.c_str()); - ESP_LOGCONFIG(TAG, " Wake On Touch: %s", this->auto_wake_on_touch_ ? "True" : "False"); + ESP_LOGCONFIG(TAG, " Wake On Touch: %s", YESNO(this->auto_wake_on_touch_)); + ESP_LOGCONFIG(TAG, " Exit reparse: %s", YESNO(this->exit_reparse_on_start_)); if (this->touch_sleep_timeout_ != 0) { ESP_LOGCONFIG(TAG, " Touch Timeout: %" PRIu32, this->touch_sleep_timeout_); @@ -248,6 +252,7 @@ void Nextion::loop() { } this->set_auto_wake_on_touch(this->auto_wake_on_touch_); + this->set_exit_reparse_on_start(this->exit_reparse_on_start_); if (this->touch_sleep_timeout_ != 0) { this->set_touch_sleep_timeout(this->touch_sleep_timeout_); diff --git a/esphome/components/nextion/nextion.h b/esphome/components/nextion/nextion.h index f188708f35..acbf394fc6 100644 --- a/esphome/components/nextion/nextion.h +++ b/esphome/components/nextion/nextion.h @@ -815,6 +815,19 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * The display will wake up by touch. */ void set_auto_wake_on_touch(bool auto_wake); + /** + * Sets if Nextion should exit the active reparse mode before the "connect" command is sent + * @param exit_reparse True or false. When exit_reparse is true, the exit reparse command + * will be sent before requesting the connection from Nextion. + * + * Example: + * ```cpp + * it.set_exit_reparse_on_start(true); + * ``` + * + * The display will be requested to leave active reparse mode before setup. + */ + void set_exit_reparse_on_start(bool exit_reparse); /** * Sets Nextion mode between sleep and awake * @param True or false. Sleep=true to enter sleep mode or sleep=false to exit sleep mode. @@ -943,6 +956,9 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe void set_wake_up_page_internal(uint8_t wake_up_page) { this->wake_up_page_ = wake_up_page; } void set_start_up_page_internal(uint8_t start_up_page) { this->start_up_page_ = start_up_page; } void set_auto_wake_on_touch_internal(bool auto_wake_on_touch) { this->auto_wake_on_touch_ = auto_wake_on_touch; } + void set_exit_reparse_on_start_internal(bool exit_reparse_on_start) { + this->exit_reparse_on_start_ = exit_reparse_on_start; + } protected: std::deque nextion_queue_; @@ -966,6 +982,7 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe int wake_up_page_ = -1; int start_up_page_ = -1; bool auto_wake_on_touch_ = true; + bool exit_reparse_on_start_ = false; /** * Manually send a raw command to the display and don't wait for an acknowledgement packet. diff --git a/esphome/components/nextion/nextion_commands.cpp b/esphome/components/nextion/nextion_commands.cpp index 3cefc6618a..8512ea5573 100644 --- a/esphome/components/nextion/nextion_commands.cpp +++ b/esphome/components/nextion/nextion_commands.cpp @@ -53,6 +53,7 @@ void Nextion::set_protocol_reparse_mode(bool active_mode) { this->write_str("connect"); this->write_array(to_send, sizeof(to_send)); } +void Nextion::set_exit_reparse_on_start(bool exit_reparse) { this->exit_reparse_on_start_ = exit_reparse; } // Set Colors - Background void Nextion::set_component_background_color(const char *component, uint16_t color) { From c53874788a8db88e03cf45764d7e1b1b1a822ac3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 6 Dec 2023 11:04:20 -1000 Subject: [PATCH 47/89] Bump aioesphomeapi from 19.2.1 to 19.3.0 (#5895) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 7d37954656..cde39175da 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ platformio==6.1.11 # When updating platformio, also update Dockerfile esptool==4.6.2 click==8.1.7 esphome-dashboard==20231107.0 -aioesphomeapi==19.2.1 +aioesphomeapi==19.3.0 zeroconf==0.128.0 python-magic==0.4.27 From 049a7a0113e64ae122df9aa6172a54672ebd6656 Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Thu, 7 Dec 2023 00:57:06 +0100 Subject: [PATCH 48/89] Add framework info to Nextion log tags (#5864) --- esphome/components/nextion/nextion_upload_arduino.cpp | 2 +- esphome/components/nextion/nextion_upload_idf.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/nextion/nextion_upload_arduino.cpp b/esphome/components/nextion/nextion_upload_arduino.cpp index 3337ff9b50..e3d0903d09 100644 --- a/esphome/components/nextion/nextion_upload_arduino.cpp +++ b/esphome/components/nextion/nextion_upload_arduino.cpp @@ -15,7 +15,7 @@ namespace esphome { namespace nextion { -static const char *const TAG = "nextion_upload"; +static const char *const TAG = "nextion.upload.arduino"; // Followed guide // https://unofficialnextion.com/t/nextion-upload-protocol-v1-2-the-fast-one/1044/2 diff --git a/esphome/components/nextion/nextion_upload_idf.cpp b/esphome/components/nextion/nextion_upload_idf.cpp index 84da7bb1be..57bb9c45e8 100644 --- a/esphome/components/nextion/nextion_upload_idf.cpp +++ b/esphome/components/nextion/nextion_upload_idf.cpp @@ -15,7 +15,7 @@ namespace esphome { namespace nextion { -static const char *const TAG = "nextion_upload"; +static const char *const TAG = "nextion.upload.idf"; // Followed guide // https://unofficialnextion.com/t/nextion-upload-protocol-v1-2-the-fast-one/1044/2 From 51428dcbc27b86819aa4a1decad45a13a78af159 Mon Sep 17 00:00:00 2001 From: mtl010957 Date: Wed, 6 Dec 2023 19:31:27 -0500 Subject: [PATCH 49/89] Handle case where using enetity level name: None with MQTT (#5897) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/mqtt/mqtt_component.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/esphome/components/mqtt/mqtt_component.cpp b/esphome/components/mqtt/mqtt_component.cpp index f9f8c850e9..af4d6f13a5 100644 --- a/esphome/components/mqtt/mqtt_component.cpp +++ b/esphome/components/mqtt/mqtt_component.cpp @@ -76,7 +76,11 @@ bool MQTTComponent::send_discovery_() { this->send_discovery(root, config); // Fields from EntityBase - root[MQTT_NAME] = this->friendly_name(); + if (this->get_entity()->has_own_name()) { + root[MQTT_NAME] = this->friendly_name(); + } else { + root[MQTT_NAME] = ""; + } if (this->is_disabled_by_default()) root[MQTT_ENABLED_BY_DEFAULT] = false; if (!this->get_icon().empty()) From a6f1701902355489bdfcb7dee53671abf063d1aa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 7 Dec 2023 01:08:50 +0000 Subject: [PATCH 50/89] Bump actions/setup-python from 4.7.1 to 5.0.0 (#5896) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- .github/actions/restore-python/action.yml | 2 +- .github/workflows/ci-docker.yml | 2 +- .github/workflows/ci.yml | 2 +- .github/workflows/release.yml | 4 ++-- .github/workflows/sync-device-classes.yml | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/actions/restore-python/action.yml b/.github/actions/restore-python/action.yml index aa8dd6d894..18a2485dbb 100644 --- a/.github/actions/restore-python/action.yml +++ b/.github/actions/restore-python/action.yml @@ -17,7 +17,7 @@ runs: steps: - name: Set up Python ${{ inputs.python-version }} id: python - uses: actions/setup-python@v4.7.0 + uses: actions/setup-python@v5.0.0 with: python-version: ${{ inputs.python-version }} - name: Restore Python virtual environment diff --git a/.github/workflows/ci-docker.yml b/.github/workflows/ci-docker.yml index 51f47d39aa..8fe8bbdc52 100644 --- a/.github/workflows/ci-docker.yml +++ b/.github/workflows/ci-docker.yml @@ -42,7 +42,7 @@ jobs: steps: - uses: actions/checkout@v4.1.1 - name: Set up Python - uses: actions/setup-python@v4.7.1 + uses: actions/setup-python@v5.0.0 with: python-version: "3.9" - name: Set up Docker Buildx diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8d1daf922f..8182f92f94 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,7 +40,7 @@ jobs: run: echo key="${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }}" >> $GITHUB_OUTPUT - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v4.7.1 + uses: actions/setup-python@v5.0.0 with: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore Python virtual environment diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 69456619e9..625a8c8ecb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -45,7 +45,7 @@ jobs: steps: - uses: actions/checkout@v4.1.1 - name: Set up Python - uses: actions/setup-python@v4.7.1 + uses: actions/setup-python@v5.0.0 with: python-version: "3.x" - name: Set up python environment @@ -80,7 +80,7 @@ jobs: steps: - uses: actions/checkout@v4.1.1 - name: Set up Python - uses: actions/setup-python@v4.7.1 + uses: actions/setup-python@v5.0.0 with: python-version: "3.9" diff --git a/.github/workflows/sync-device-classes.yml b/.github/workflows/sync-device-classes.yml index 88edb63546..d45784bf7f 100644 --- a/.github/workflows/sync-device-classes.yml +++ b/.github/workflows/sync-device-classes.yml @@ -22,7 +22,7 @@ jobs: path: lib/home-assistant - name: Setup Python - uses: actions/setup-python@v4.7.1 + uses: actions/setup-python@v5.0.0 with: python-version: 3.11 From 0906559afe55d1e233d4ead938c6f439482ff29e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Dec 2023 19:38:55 +0000 Subject: [PATCH 51/89] Bump zeroconf from 0.128.0 to 0.128.4 (#5906) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index cde39175da..39715232d6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,7 +11,7 @@ esptool==4.6.2 click==8.1.7 esphome-dashboard==20231107.0 aioesphomeapi==19.3.0 -zeroconf==0.128.0 +zeroconf==0.128.4 python-magic==0.4.27 # esp-idf requires this, but doesn't bundle it by default From b62c099d54eb24c3dff0e53aa3561b524a9c512b Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Tue, 12 Dec 2023 10:22:41 +1100 Subject: [PATCH 52/89] Fix 18 bit displays. (#5908) --- esphome/components/ili9xxx/ili9xxx_display.h | 25 ++++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/esphome/components/ili9xxx/ili9xxx_display.h b/esphome/components/ili9xxx/ili9xxx_display.h index db1274450a..bf4889afe1 100644 --- a/esphome/components/ili9xxx/ili9xxx_display.h +++ b/esphome/components/ili9xxx/ili9xxx_display.h @@ -30,13 +30,24 @@ class ILI9XXXDisplay : public display::DisplayBuffer, const uint8_t *addr = init_sequence; while ((cmd = *addr++) != 0) { num_args = *addr++ & 0x7F; - if (cmd == ILI9XXX_MADCTL) { - bits = *addr; - this->swap_xy_ = (bits & MADCTL_MV) != 0; - this->mirror_x_ = (bits & MADCTL_MX) != 0; - this->mirror_y_ = (bits & MADCTL_MY) != 0; - this->color_order_ = (bits & MADCTL_BGR) ? display::COLOR_ORDER_BGR : display::COLOR_ORDER_RGB; - break; + bits = *addr; + switch (cmd) { + case ILI9XXX_MADCTL: { + this->swap_xy_ = (bits & MADCTL_MV) != 0; + this->mirror_x_ = (bits & MADCTL_MX) != 0; + this->mirror_y_ = (bits & MADCTL_MY) != 0; + this->color_order_ = (bits & MADCTL_BGR) ? display::COLOR_ORDER_BGR : display::COLOR_ORDER_RGB; + break; + } + + case ILI9XXX_PIXFMT: { + if ((bits & 0xF) == 6) + this->is_18bitdisplay_ = true; + break; + } + + default: + break; } addr += num_args; } From 86e6a8a5033acaf9d1e83a7ec65624fab1f276ce Mon Sep 17 00:00:00 2001 From: Clemens Date: Tue, 12 Dec 2023 00:28:16 +0100 Subject: [PATCH 53/89] fix RGBW Mode on RP2040 (#5907) --- esphome/components/rp2040_pio_led_strip/led_strip.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/esphome/components/rp2040_pio_led_strip/led_strip.cpp b/esphome/components/rp2040_pio_led_strip/led_strip.cpp index ce1836306f..c04419a9bf 100644 --- a/esphome/components/rp2040_pio_led_strip/led_strip.cpp +++ b/esphome/components/rp2040_pio_led_strip/led_strip.cpp @@ -70,9 +70,10 @@ void RP2040PIOLEDStripLightOutput::write_state(light::LightState *state) { // assemble bits in buffer to 32 bit words with ex for GBR: 0bGGGGGGGGRRRRRRRRBBBBBBBB00000000 for (int i = 0; i < this->num_leds_; i++) { - uint8_t c1 = this->buf_[(i * 3) + 0]; - uint8_t c2 = this->buf_[(i * 3) + 1]; - uint8_t c3 = this->buf_[(i * 3) + 2]; + uint8_t multiplier = this->is_rgbw_ ? 4 : 3; + uint8_t c1 = this->buf_[(i * multiplier) + 0]; + uint8_t c2 = this->buf_[(i * multiplier) + 1]; + uint8_t c3 = this->buf_[(i * multiplier) + 2]; uint8_t w = this->is_rgbw_ ? this->buf_[(i * 4) + 3] : 0; uint32_t color = encode_uint32(c1, c2, c3, w); pio_sm_put_blocking(this->pio_, this->sm_, color); From b30430b0bda4f12e72281c987183d7c7a45cc659 Mon Sep 17 00:00:00 2001 From: Michael Davidson Date: Tue, 12 Dec 2023 14:15:59 +1100 Subject: [PATCH 54/89] Add graphical display menu (#4105) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Co-authored-by: Alex Hermann --- CODEOWNERS | 1 + esphome/components/display/display.cpp | 7 + esphome/components/display/display.h | 15 ++ .../display_menu_base/display_menu_base.cpp | 12 + .../display_menu_base/display_menu_base.h | 5 + .../display_menu_base/menu_item.cpp | 23 ++ .../components/display_menu_base/menu_item.h | 4 + .../graphical_display_menu/__init__.py | 96 +++++++ .../graphical_display_menu.cpp | 243 ++++++++++++++++++ .../graphical_display_menu.h | 84 ++++++ esphome/core/defines.h | 1 + tests/test1.yaml | 116 ++++++++- 12 files changed, 598 insertions(+), 9 deletions(-) create mode 100644 esphome/components/graphical_display_menu/__init__.py create mode 100644 esphome/components/graphical_display_menu/graphical_display_menu.cpp create mode 100644 esphome/components/graphical_display_menu/graphical_display_menu.h diff --git a/CODEOWNERS b/CODEOWNERS index de80806eac..43b7647fe6 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -116,6 +116,7 @@ esphome/components/gp8403/* @jesserockz esphome/components/gpio/* @esphome/core esphome/components/gps/* @coogle esphome/components/graph/* @synco +esphome/components/graphical_display_menu/* @MrMDavidson esphome/components/gree/* @orestismers esphome/components/grove_tb6612fng/* @max246 esphome/components/growatt_solar/* @leeuwte diff --git a/esphome/components/display/display.cpp b/esphome/components/display/display.cpp index 22454aeddb..88ee64ea55 100644 --- a/esphome/components/display/display.cpp +++ b/esphome/components/display/display.cpp @@ -166,6 +166,13 @@ void Display::qr_code(int x, int y, qr_code::QrCode *qr_code, Color color_on, in } #endif // USE_QR_CODE +#ifdef USE_GRAPHICAL_DISPLAY_MENU +void Display::menu(int x, int y, graphical_display_menu::GraphicalDisplayMenu *menu, int width, int height) { + Rect rect(x, y, width, height); + menu->draw(this, &rect); +} +#endif // USE_GRAPHICAL_DISPLAY_MENU + void Display::get_text_bounds(int x, int y, const char *text, BaseFont *font, TextAlign align, int *x1, int *y1, int *width, int *height) { int x_offset, baseline; diff --git a/esphome/components/display/display.h b/esphome/components/display/display.h index 7ce6d179ef..3afcfb9528 100644 --- a/esphome/components/display/display.h +++ b/esphome/components/display/display.h @@ -17,6 +17,10 @@ #include "esphome/components/qr_code/qr_code.h" #endif +#ifdef USE_GRAPHICAL_DISPLAY_MENU +#include "esphome/components/graphical_display_menu/graphical_display_menu.h" +#endif + namespace esphome { namespace display { @@ -392,6 +396,17 @@ class Display : public PollingComponent { void qr_code(int x, int y, qr_code::QrCode *qr_code, Color color_on = COLOR_ON, int scale = 1); #endif +#ifdef USE_GRAPHICAL_DISPLAY_MENU + /** + * @param x The x coordinate of the upper left corner + * @param y The y coordinate of the upper left corner + * @param menu The GraphicalDisplayMenu to draw + * @param width Width of the menu + * @param height Height of the menu + */ + void menu(int x, int y, graphical_display_menu::GraphicalDisplayMenu *menu, int width, int height); +#endif // USE_GRAPHICAL_DISPLAY_MENU + /** Get the text bounds of the given string. * * @param x The x coordinate to place the string at, can be 0 if only interested in dimensions. diff --git a/esphome/components/display_menu_base/display_menu_base.cpp b/esphome/components/display_menu_base/display_menu_base.cpp index 57da3cec35..0bfee338ca 100644 --- a/esphome/components/display_menu_base/display_menu_base.cpp +++ b/esphome/components/display_menu_base/display_menu_base.cpp @@ -172,6 +172,8 @@ void DisplayMenuComponent::show_main() { this->process_initial_(); + this->on_before_show(); + if (this->active_ && this->editing_) this->finish_editing_(); @@ -188,6 +190,8 @@ void DisplayMenuComponent::show_main() { } this->draw_and_update(); + + this->on_after_show(); } void DisplayMenuComponent::show() { @@ -196,18 +200,26 @@ void DisplayMenuComponent::show() { this->process_initial_(); + this->on_before_show(); + if (!this->active_) { this->active_ = true; this->draw_and_update(); } + + this->on_after_show(); } void DisplayMenuComponent::hide() { if (this->check_healthy_and_active_()) { + this->on_before_hide(); + if (this->editing_) this->finish_editing_(); this->active_ = false; this->update(); + + this->on_after_hide(); } } diff --git a/esphome/components/display_menu_base/display_menu_base.h b/esphome/components/display_menu_base/display_menu_base.h index 46bb0a8192..6208fcd3b4 100644 --- a/esphome/components/display_menu_base/display_menu_base.h +++ b/esphome/components/display_menu_base/display_menu_base.h @@ -60,6 +60,11 @@ class DisplayMenuComponent : public Component { update(); } + virtual void on_before_show(){}; + virtual void on_after_show(){}; + virtual void on_before_hide(){}; + virtual void on_after_hide(){}; + uint8_t rows_; bool active_; MenuMode mode_; diff --git a/esphome/components/display_menu_base/menu_item.cpp b/esphome/components/display_menu_base/menu_item.cpp index bbe6ec0e89..2c7f34c493 100644 --- a/esphome/components/display_menu_base/menu_item.cpp +++ b/esphome/components/display_menu_base/menu_item.cpp @@ -5,6 +5,29 @@ namespace esphome { namespace display_menu_base { +const LogString *menu_item_type_to_string(MenuItemType type) { + switch (type) { + case MenuItemType::MENU_ITEM_LABEL: + return LOG_STR("MENU_ITEM_LABEL"); + case MenuItemType::MENU_ITEM_MENU: + return LOG_STR("MENU_ITEM_MENU"); + case MenuItemType::MENU_ITEM_BACK: + return LOG_STR("MENU_ITEM_BACK"); + case MenuItemType::MENU_ITEM_SELECT: + return LOG_STR("MENU_ITEM_SELECT"); + case MenuItemType::MENU_ITEM_NUMBER: + return LOG_STR("MENU_ITEM_NUMBER"); + case MenuItemType::MENU_ITEM_SWITCH: + return LOG_STR("MENU_ITEM_SWITCH"); + case MenuItemType::MENU_ITEM_COMMAND: + return LOG_STR("MENU_ITEM_COMMAND"); + case MenuItemType::MENU_ITEM_CUSTOM: + return LOG_STR("MENU_ITEM_CUSTOM"); + default: + return LOG_STR("UNKNOWN"); + } +} + void MenuItem::on_enter() { this->on_enter_callbacks_.call(); } void MenuItem::on_leave() { this->on_leave_callbacks_.call(); } diff --git a/esphome/components/display_menu_base/menu_item.h b/esphome/components/display_menu_base/menu_item.h index a30f31e88f..36de146031 100644 --- a/esphome/components/display_menu_base/menu_item.h +++ b/esphome/components/display_menu_base/menu_item.h @@ -14,6 +14,7 @@ #endif #include +#include "esphome/core/log.h" namespace esphome { namespace display_menu_base { @@ -29,6 +30,9 @@ enum MenuItemType { MENU_ITEM_CUSTOM, }; +/// @brief Returns a string representation of a menu item type suitable for logging +const LogString *menu_item_type_to_string(MenuItemType type); + class MenuItem; class MenuItemMenu; using value_getter_t = std::function; diff --git a/esphome/components/graphical_display_menu/__init__.py b/esphome/components/graphical_display_menu/__init__.py new file mode 100644 index 0000000000..dc49358efd --- /dev/null +++ b/esphome/components/graphical_display_menu/__init__.py @@ -0,0 +1,96 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import display, font, color +from esphome.const import CONF_ID, CONF_TRIGGER_ID +from esphome import automation, core + +from esphome.components.display_menu_base import ( + DISPLAY_MENU_BASE_SCHEMA, + DisplayMenuComponent, + display_menu_to_code, +) + +CONF_DISPLAY = "display" +CONF_FONT = "font" +CONF_MENU_ITEM_VALUE = "menu_item_value" +CONF_FOREGROUND_COLOR = "foreground_color" +CONF_BACKGROUND_COLOR = "background_color" +CONF_ON_REDRAW = "on_redraw" + +graphical_display_menu_ns = cg.esphome_ns.namespace("graphical_display_menu") +GraphicalDisplayMenu = graphical_display_menu_ns.class_( + "GraphicalDisplayMenu", DisplayMenuComponent +) +GraphicalDisplayMenuConstPtr = GraphicalDisplayMenu.operator("ptr").operator("const") +MenuItemValueArguments = graphical_display_menu_ns.struct("MenuItemValueArguments") +MenuItemValueArgumentsConstPtr = MenuItemValueArguments.operator("ptr").operator( + "const" +) +GraphicalDisplayMenuOnRedrawTrigger = graphical_display_menu_ns.class_( + "GraphicalDisplayMenuOnRedrawTrigger", automation.Trigger +) + +CODEOWNERS = ["@MrMDavidson"] + +AUTO_LOAD = ["display_menu_base"] + +CONFIG_SCHEMA = DISPLAY_MENU_BASE_SCHEMA.extend( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(GraphicalDisplayMenu), + cv.Optional(CONF_DISPLAY): cv.use_id(display.DisplayBuffer), + cv.Required(CONF_FONT): cv.use_id(font.Font), + cv.Optional(CONF_MENU_ITEM_VALUE): cv.templatable(cv.string), + cv.Optional(CONF_FOREGROUND_COLOR): cv.use_id(color.ColorStruct), + cv.Optional(CONF_BACKGROUND_COLOR): cv.use_id(color.ColorStruct), + cv.Optional(CONF_ON_REDRAW): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + GraphicalDisplayMenuOnRedrawTrigger + ) + } + ), + } + ) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + + if display_config := config.get(CONF_DISPLAY): + drawing_display = await cg.get_variable(display_config) + cg.add(var.set_display(drawing_display)) + + menu_font = await cg.get_variable(config[CONF_FONT]) + cg.add(var.set_font(menu_font)) + + if (menu_item_value_config := config.get(CONF_MENU_ITEM_VALUE, None)) is not None: + if isinstance(menu_item_value_config, core.Lambda): + template_ = await cg.templatable( + menu_item_value_config, + [(MenuItemValueArgumentsConstPtr, "it")], + cg.std_string, + ) + cg.add(var.set_menu_item_value(template_)) + else: + cg.add(var.set_menu_item_value(menu_item_value_config)) + + if foreground_color_config := config.get(CONF_FOREGROUND_COLOR): + foreground_color = await cg.get_variable(foreground_color_config) + cg.add(var.set_foreground_color(foreground_color)) + + if background_color_config := config.get(CONF_BACKGROUND_COLOR): + background_color = await cg.get_variable(background_color_config) + cg.add(var.set_background_color(background_color)) + + for conf in config.get(CONF_ON_REDRAW, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation( + trigger, [(GraphicalDisplayMenuConstPtr, "it")], conf + ) + + await display_menu_to_code(var, config) + + cg.add_define("USE_GRAPHICAL_DISPLAY_MENU") diff --git a/esphome/components/graphical_display_menu/graphical_display_menu.cpp b/esphome/components/graphical_display_menu/graphical_display_menu.cpp new file mode 100644 index 0000000000..2e4c14fb7b --- /dev/null +++ b/esphome/components/graphical_display_menu/graphical_display_menu.cpp @@ -0,0 +1,243 @@ +#include "graphical_display_menu.h" +#include "esphome/core/hal.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" +#include +#include "esphome/components/display/display.h" + +namespace esphome { +namespace graphical_display_menu { + +static const char *const TAG = "graphical_display_menu"; + +void GraphicalDisplayMenu::setup() { + if (this->display_ != nullptr) { + display::display_writer_t writer = [this](display::Display &it) { this->draw_menu(); }; + this->display_page_ = make_unique(writer); + } + + if (!this->menu_item_value_.has_value()) { + this->menu_item_value_ = [](const MenuItemValueArguments *it) { + std::string label = " "; + if (it->is_item_selected && it->is_menu_editing) { + label.append(">"); + label.append(it->item->get_value_text()); + label.append("<"); + } else { + label.append("("); + label.append(it->item->get_value_text()); + label.append(")"); + } + return label; + }; + } + + display_menu_base::DisplayMenuComponent::setup(); +} + +void GraphicalDisplayMenu::dump_config() { + ESP_LOGCONFIG(TAG, "Graphical Display Menu"); + ESP_LOGCONFIG(TAG, "Has Display: %s", YESNO(this->display_ != nullptr)); + ESP_LOGCONFIG(TAG, "Popup Mode: %s", YESNO(this->display_ != nullptr)); + ESP_LOGCONFIG(TAG, "Advanced Drawing Mode: %s", YESNO(this->display_ == nullptr)); + ESP_LOGCONFIG(TAG, "Has Font: %s", YESNO(this->font_ != nullptr)); + ESP_LOGCONFIG(TAG, "Mode: %s", this->mode_ == display_menu_base::MENU_MODE_ROTARY ? "Rotary" : "Joystick"); + ESP_LOGCONFIG(TAG, "Active: %s", YESNO(this->active_)); + ESP_LOGCONFIG(TAG, "Menu items:"); + for (size_t i = 0; i < this->displayed_item_->items_size(); i++) { + auto *item = this->displayed_item_->get_item(i); + ESP_LOGCONFIG(TAG, " %i: %s (Type: %s, Immediate Edit: %s)", i, item->get_text().c_str(), + LOG_STR_ARG(display_menu_base::menu_item_type_to_string(item->get_type())), + YESNO(item->get_immediate_edit())); + } +} + +void GraphicalDisplayMenu::set_display(display::Display *display) { this->display_ = display; } + +void GraphicalDisplayMenu::set_font(display::BaseFont *font) { this->font_ = font; } + +void GraphicalDisplayMenu::set_foreground_color(Color foreground_color) { this->foreground_color_ = foreground_color; } +void GraphicalDisplayMenu::set_background_color(Color background_color) { this->background_color_ = background_color; } + +void GraphicalDisplayMenu::on_before_show() { + if (this->display_ != nullptr) { + this->previous_display_page_ = this->display_->get_active_page(); + this->display_->show_page(this->display_page_.get()); + this->display_->clear(); + } else { + this->update(); + } +} + +void GraphicalDisplayMenu::on_before_hide() { + if (this->previous_display_page_ != nullptr) { + this->display_->show_page((display::DisplayPage *) this->previous_display_page_); + this->display_->clear(); + this->update(); + this->previous_display_page_ = nullptr; + } else { + this->update(); + } +} + +void GraphicalDisplayMenu::draw_and_update() { + this->update(); + + // If we're in advanced drawing mode we won't have a display and will instead require the update callback to do + // our drawing + if (this->display_ != nullptr) { + draw_menu(); + } +} + +void GraphicalDisplayMenu::draw_menu() { + if (this->display_ == nullptr) { + ESP_LOGE(TAG, "draw_menu() called without a display_. This is only available when using the menu in pop up mode"); + return; + } + display::Rect bounds(0, 0, this->display_->get_width(), this->display_->get_height()); + this->draw_menu_internal_(this->display_, &bounds); +} + +void GraphicalDisplayMenu::draw(display::Display *display, const display::Rect *bounds) { + this->draw_menu_internal_(display, bounds); +} + +void GraphicalDisplayMenu::draw_menu_internal_(display::Display *display, const display::Rect *bounds) { + int total_height = 0; + int y_padding = 2; + bool scroll_menu_items = false; + std::vector menu_dimensions; + int number_items_fit_to_screen = 0; + const int max_item_index = this->displayed_item_->items_size() - 1; + + for (size_t i = 0; i <= max_item_index; i++) { + const auto *item = this->displayed_item_->get_item(i); + const bool selected = i == this->cursor_index_; + const display::Rect item_dimensions = this->measure_item(display, item, bounds, selected); + + menu_dimensions.push_back(item_dimensions); + total_height += item_dimensions.h + (i == 0 ? 0 : y_padding); + + if (total_height <= bounds->h) { + number_items_fit_to_screen++; + } else { + // Scroll the display if the selected item or the item immediately after it overflows + if ((selected) || (i == this->cursor_index_ + 1)) { + scroll_menu_items = true; + } + } + } + + // Determine what items to draw + int first_item_index = 0; + int last_item_index = max_item_index; + + if (number_items_fit_to_screen <= 1) { + // If only one item can fit to the bounds draw the current cursor item + last_item_index = std::min(last_item_index, this->cursor_index_ + 1); + first_item_index = this->cursor_index_; + } else { + if (scroll_menu_items) { + // Attempt to draw the item after the current item (+1 for equality check in the draw loop) + last_item_index = std::min(last_item_index, this->cursor_index_ + 1); + + // Go back through the measurements to determine how many prior items we can fit + int height_left_to_use = bounds->h; + for (int i = last_item_index; i >= 0; i--) { + const display::Rect item_dimensions = menu_dimensions[i]; + height_left_to_use -= (item_dimensions.h + y_padding); + + if (height_left_to_use <= 0) { + // Ran out of space - this is our first item to draw + first_item_index = i; + break; + } + } + const int items_to_draw = last_item_index - first_item_index; + // Dont't draw last item partially if it is the selected item + if ((this->cursor_index_ == last_item_index) && (number_items_fit_to_screen <= items_to_draw) && + (first_item_index < max_item_index)) { + first_item_index++; + } + } + } + + // Render the items into the view port + display->start_clipping(*bounds); + + int y_offset = bounds->y; + for (size_t i = first_item_index; i <= last_item_index; i++) { + const auto *item = this->displayed_item_->get_item(i); + const bool selected = i == this->cursor_index_; + display::Rect dimensions = menu_dimensions[i]; + + dimensions.y = y_offset; + dimensions.x = bounds->x; + this->draw_item(display, item, &dimensions, selected); + + y_offset = dimensions.y + dimensions.h + y_padding; + } + + display->end_clipping(); +} + +display::Rect GraphicalDisplayMenu::measure_item(display::Display *display, const display_menu_base::MenuItem *item, + const display::Rect *bounds, const bool selected) { + display::Rect dimensions(0, 0, 0, 0); + + if (selected) { + // TODO: Support selection glyph + dimensions.w += 0; + dimensions.h += 0; + } + + std::string label = item->get_text(); + if (item->has_value()) { + // Append to label + MenuItemValueArguments args(item, selected, this->editing_); + label.append(this->menu_item_value_.value(&args)); + } + + int x1; + int y1; + int width; + int height; + display->get_text_bounds(0, 0, label.c_str(), this->font_, display::TextAlign::TOP_LEFT, &x1, &y1, &width, &height); + + dimensions.w = std::min((int16_t) width, bounds->w); + dimensions.h = std::min((int16_t) height, bounds->h); + + return dimensions; +} + +inline void GraphicalDisplayMenu::draw_item(display::Display *display, const display_menu_base::MenuItem *item, + const display::Rect *bounds, const bool selected) { + const auto background_color = selected ? this->foreground_color_ : this->background_color_; + const auto foreground_color = selected ? this->background_color_ : this->foreground_color_; + + // int background_width = std::max(bounds->width, available_width); + int background_width = bounds->w; + + if (selected) { + display->filled_rectangle(bounds->x, bounds->y, background_width, bounds->h, background_color); + } + + std::string label = item->get_text(); + if (item->has_value()) { + MenuItemValueArguments args(item, selected, this->editing_); + label.append(this->menu_item_value_.value(&args)); + } + + display->print(bounds->x, bounds->y, this->font_, foreground_color, display::TextAlign::TOP_LEFT, label.c_str()); +} + +void GraphicalDisplayMenu::draw_item(const display_menu_base::MenuItem *item, const uint8_t row, const bool selected) { + ESP_LOGE(TAG, "draw_item(MenuItem *item, uint8_t row, bool selected) called. The graphical_display_menu specific " + "draw_item should be called."); +} + +void GraphicalDisplayMenu::update() { this->on_redraw_callbacks_.call(); } + +} // namespace graphical_display_menu +} // namespace esphome diff --git a/esphome/components/graphical_display_menu/graphical_display_menu.h b/esphome/components/graphical_display_menu/graphical_display_menu.h new file mode 100644 index 0000000000..96f2bd79fd --- /dev/null +++ b/esphome/components/graphical_display_menu/graphical_display_menu.h @@ -0,0 +1,84 @@ +#pragma once + +#include "esphome/core/color.h" +#include "esphome/components/display_menu_base/display_menu_base.h" +#include "esphome/components/display_menu_base/menu_item.h" +#include "esphome/core/automation.h" +#include + +namespace esphome { + +// forward declare from display namespace +namespace display { +class Display; +class DisplayPage; +class BaseFont; +class Rect; +} // namespace display + +namespace graphical_display_menu { + +const Color COLOR_ON(255, 255, 255, 255); +const Color COLOR_OFF(0, 0, 0, 0); + +struct MenuItemValueArguments { + MenuItemValueArguments(const display_menu_base::MenuItem *item, bool is_item_selected, bool is_menu_editing) { + this->item = item; + this->is_item_selected = is_item_selected; + this->is_menu_editing = is_menu_editing; + } + + const display_menu_base::MenuItem *item; + bool is_item_selected; + bool is_menu_editing; +}; + +class GraphicalDisplayMenu : public display_menu_base::DisplayMenuComponent { + public: + void setup() override; + void dump_config() override; + + void set_display(display::Display *display); + void set_font(display::BaseFont *font); + template void set_menu_item_value(V menu_item_value) { this->menu_item_value_ = menu_item_value; } + void set_foreground_color(Color foreground_color); + void set_background_color(Color background_color); + + void add_on_redraw_callback(std::function &&cb) { this->on_redraw_callbacks_.add(std::move(cb)); } + + void draw(display::Display *display, const display::Rect *bounds); + + protected: + void draw_and_update() override; + void draw_menu() override; + void draw_menu_internal_(display::Display *display, const display::Rect *bounds); + void draw_item(const display_menu_base::MenuItem *item, uint8_t row, bool selected) override; + virtual display::Rect measure_item(display::Display *display, const display_menu_base::MenuItem *item, + const display::Rect *bounds, bool selected); + virtual void draw_item(display::Display *display, const display_menu_base::MenuItem *item, + const display::Rect *bounds, bool selected); + void update() override; + + void on_before_show() override; + void on_before_hide() override; + + std::unique_ptr display_page_{nullptr}; + const display::DisplayPage *previous_display_page_{nullptr}; + display::Display *display_{nullptr}; + display::BaseFont *font_{nullptr}; + TemplatableValue menu_item_value_; + Color foreground_color_{COLOR_ON}; + Color background_color_{COLOR_OFF}; + + CallbackManager on_redraw_callbacks_{}; +}; + +class GraphicalDisplayMenuOnRedrawTrigger : public Trigger { + public: + explicit GraphicalDisplayMenuOnRedrawTrigger(GraphicalDisplayMenu *parent) { + parent->add_on_redraw_callback([this, parent]() { this->trigger(parent); }); + } +}; + +} // namespace graphical_display_menu +} // namespace esphome diff --git a/esphome/core/defines.h b/esphome/core/defines.h index b93b8c9270..e75abdb88f 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -51,6 +51,7 @@ #define USE_UART_DEBUGGER #define USE_WIFI #define USE_WIFI_AP +#define USE_GRAPHICAL_DISPLAY_MENU // Arduino-specific feature flags #ifdef USE_ARDUINO diff --git a/tests/test1.yaml b/tests/test1.yaml index c90cdf6d90..559b7ab8fc 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -1141,10 +1141,12 @@ sensor: value: !lambda "return -1;" on_clockwise: - logger.log: Clockwise - - display_menu.down: + - display_menu.down: test_lcd_menu + - display_menu.down: test_graphical_display_menu on_anticlockwise: - logger.log: Anticlockwise - - display_menu.up: + - display_menu.up: test_lcd_menu + - display_menu.up: test_graphical_display_menu - platform: pulse_width name: Pulse Width pin: @@ -1781,13 +1783,22 @@ binary_sensor: on_press: - if: condition: - display_menu.is_active: + display_menu.is_active: test_lcd_menu then: - - display_menu.enter: + - display_menu.enter: test_lcd_menu else: - - display_menu.left: - - display_menu.right: - - display_menu.show: + - display_menu.left: test_lcd_menu + - display_menu.right: test_lcd_menu + - display_menu.show: test_lcd_menu + - if: + condition: + display_menu.is_active: test_graphical_display_menu + then: + - display_menu.enter: test_graphical_display_menu + else: + - display_menu.left: test_graphical_display_menu + - display_menu.right: test_graphical_display_menu + - display_menu.show: test_graphical_display_menu - platform: template name: Garage Door Open id: garage_door @@ -3204,6 +3215,7 @@ display: lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); - platform: st7735 + id: st7735_display model: INITR_BLACKTAB cs_pin: allow_other_uses: true @@ -3997,6 +4009,7 @@ ld2420: uart_id: ld2420_uart lcd_menu: + id: test_lcd_menu display_id: my_lcd_gpio mark_back: 0x5e mark_selected: 0x3e @@ -4028,7 +4041,7 @@ lcd_menu: text: Show Main on_value: then: - - display_menu.show_main: + - display_menu.show_main: test_lcd_menu - type: select text: Enum Item immediate_edit: true @@ -4058,7 +4071,7 @@ lcd_menu: text: Hide on_value: then: - - display_menu.hide: + - display_menu.hide: test_lcd_menu - type: switch text: Switch switch: my_switch @@ -4078,6 +4091,91 @@ lcd_menu: then: lambda: 'ESP_LOGI("lcd_menu", "custom prev: %s", it->get_text().c_str());' +font: + - file: "gfonts://Roboto" + id: roboto + size: 20 + +graphical_display_menu: + id: test_graphical_display_menu + display: st7735_display + font: roboto + active: false + mode: rotary + on_enter: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "root enter");' + on_leave: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "root leave");' + items: + - type: back + text: 'Back' + - type: label + - type: menu + text: 'Submenu 1' + items: + - type: back + text: 'Back' + - type: menu + text: 'Submenu 21' + items: + - type: back + text: 'Back' + - type: command + text: 'Show Main' + on_value: + then: + - display_menu.show_main: test_graphical_display_menu + - type: select + text: 'Enum Item' + immediate_edit: true + select: test_select + on_enter: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "select enter: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + on_leave: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "select leave: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + on_value: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "select value: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + - type: number + text: 'Number' + number: test_number + on_enter: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "number enter: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + on_leave: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "number leave: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + on_value: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "number value: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());' + - type: command + text: 'Hide' + on_value: + then: + - display_menu.hide: test_graphical_display_menu + - type: switch + text: 'Switch' + switch: my_switch + on_text: 'Bright' + off_text: 'Dark' + immediate_edit: false + on_value: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "switch value: %s", it->get_value_text().c_str());' + - type: custom + text: !lambda 'return "Custom";' + value_lambda: 'return "Val";' + on_next: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "custom next: %s", it->get_text().c_str());' + on_prev: + then: + lambda: 'ESP_LOGI("graphical_display_menu", "custom prev: %s", it->get_text().c_str());' + alarm_control_panel: - platform: template id: alarmcontrolpanel1 From 39d026299e6834fbd3182f0f717e5c96414b053b Mon Sep 17 00:00:00 2001 From: Evgeny Date: Tue, 12 Dec 2023 05:48:21 +0100 Subject: [PATCH 55/89] Added on_image callback to ESP32 Cam (#4860) --- esphome/components/esp32_camera/__init__.py | 17 +++++++++++++++ .../components/esp32_camera/esp32_camera.cpp | 4 ++-- .../components/esp32_camera/esp32_camera.h | 21 +++++++++++++++++-- tests/test4.yaml | 4 ++++ 4 files changed, 42 insertions(+), 4 deletions(-) diff --git a/esphome/components/esp32_camera/__init__.py b/esphome/components/esp32_camera/__init__.py index 4cbdf7ca5c..ee8a889f4c 100644 --- a/esphome/components/esp32_camera/__init__.py +++ b/esphome/components/esp32_camera/__init__.py @@ -25,6 +25,11 @@ AUTO_LOAD = ["psram"] esp32_camera_ns = cg.esphome_ns.namespace("esp32_camera") ESP32Camera = esp32_camera_ns.class_("ESP32Camera", cg.PollingComponent, cg.EntityBase) +ESP32CameraImageData = esp32_camera_ns.struct("CameraImageData") +# Triggers +ESP32CameraImageTrigger = esp32_camera_ns.class_( + "ESP32CameraImageTrigger", automation.Trigger.template() +) ESP32CameraStreamStartTrigger = esp32_camera_ns.class_( "ESP32CameraStreamStartTrigger", automation.Trigger.template(), @@ -139,6 +144,7 @@ CONF_IDLE_FRAMERATE = "idle_framerate" # stream trigger CONF_ON_STREAM_START = "on_stream_start" CONF_ON_STREAM_STOP = "on_stream_stop" +CONF_ON_IMAGE = "on_image" camera_range_param = cv.int_range(min=-2, max=2) @@ -221,6 +227,11 @@ CONFIG_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend( ), } ), + cv.Optional(CONF_ON_IMAGE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ESP32CameraImageTrigger), + } + ), } ).extend(cv.COMPONENT_SCHEMA) @@ -289,3 +300,9 @@ async def to_code(config): for conf in config.get(CONF_ON_STREAM_STOP, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation(trigger, [], conf) + + for conf in config.get(CONF_ON_IMAGE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation( + trigger, [(ESP32CameraImageData, "image")], conf + ) diff --git a/esphome/components/esp32_camera/esp32_camera.cpp b/esphome/components/esp32_camera/esp32_camera.cpp index e4020a902e..99cb811fe4 100644 --- a/esphome/components/esp32_camera/esp32_camera.cpp +++ b/esphome/components/esp32_camera/esp32_camera.cpp @@ -335,8 +335,8 @@ void ESP32Camera::set_idle_update_interval(uint32_t idle_update_interval) { } /* ---------------- public API (specific) ---------------- */ -void ESP32Camera::add_image_callback(std::function)> &&f) { - this->new_image_callback_.add(std::move(f)); +void ESP32Camera::add_image_callback(std::function)> &&callback) { + this->new_image_callback_.add(std::move(callback)); } void ESP32Camera::add_stream_start_callback(std::function &&callback) { this->stream_start_callback_.add(std::move(callback)); diff --git a/esphome/components/esp32_camera/esp32_camera.h b/esphome/components/esp32_camera/esp32_camera.h index 5f88c6fda8..0c25381039 100644 --- a/esphome/components/esp32_camera/esp32_camera.h +++ b/esphome/components/esp32_camera/esp32_camera.h @@ -86,6 +86,11 @@ class CameraImage { uint8_t requesters_; }; +struct CameraImageData { + uint8_t *data; + size_t length; +}; + /* ---------------- CameraImageReader class ---------------- */ class CameraImageReader { public: @@ -147,12 +152,12 @@ class ESP32Camera : public Component, public EntityBase { void dump_config() override; float get_setup_priority() const override; /* public API (specific) */ - void add_image_callback(std::function)> &&f); void start_stream(CameraRequester requester); void stop_stream(CameraRequester requester); void request_image(CameraRequester requester); void update_camera_parameters(); + void add_image_callback(std::function)> &&callback); void add_stream_start_callback(std::function &&callback); void add_stream_stop_callback(std::function &&callback); @@ -196,7 +201,7 @@ class ESP32Camera : public Component, public EntityBase { uint8_t stream_requesters_{0}; QueueHandle_t framebuffer_get_queue_; QueueHandle_t framebuffer_return_queue_; - CallbackManager)> new_image_callback_; + CallbackManager)> new_image_callback_{}; CallbackManager stream_start_callback_{}; CallbackManager stream_stop_callback_{}; @@ -207,6 +212,18 @@ class ESP32Camera : public Component, public EntityBase { // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) extern ESP32Camera *global_esp32_camera; +class ESP32CameraImageTrigger : public Trigger { + public: + explicit ESP32CameraImageTrigger(ESP32Camera *parent) { + parent->add_image_callback([this](const std::shared_ptr &image) { + CameraImageData camera_image_data{}; + camera_image_data.length = image->get_data_length(); + camera_image_data.data = image->get_data_buffer(); + this->trigger(camera_image_data); + }); + } +}; + class ESP32CameraStreamStartTrigger : public Trigger<> { public: explicit ESP32CameraStreamStartTrigger(ESP32Camera *parent) { diff --git a/tests/test4.yaml b/tests/test4.yaml index 05870d3b8f..9f15b84b12 100644 --- a/tests/test4.yaml +++ b/tests/test4.yaml @@ -861,6 +861,10 @@ esp32_camera: number: GPIO1 resolution: 640x480 jpeg_quality: 10 + on_image: + then: + - lambda: |- + ESP_LOGD("main", "image len=%d, data=%c", image.length, image.data[0]); esp32_camera_web_server: - port: 8080 From 7fb10547edb0ba87b2801c8851a045d62e309ab6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 Dec 2023 13:51:07 +0900 Subject: [PATCH 56/89] Bump actions/stale from 8.0.0 to 9.0.0 (#5899) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/stale.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index a2d3f2f77d..5f510ffe75 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -18,7 +18,7 @@ jobs: stale: runs-on: ubuntu-latest steps: - - uses: actions/stale@v8.0.0 + - uses: actions/stale@v9.0.0 with: days-before-pr-stale: 90 days-before-pr-close: 7 @@ -38,7 +38,7 @@ jobs: close-issues: runs-on: ubuntu-latest steps: - - uses: actions/stale@v8.0.0 + - uses: actions/stale@v9.0.0 with: days-before-pr-stale: -1 days-before-pr-close: -1 From 0117de5b78d49ba41f4be3fa07cde8fc85a7bf8c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 Dec 2023 13:51:22 +0900 Subject: [PATCH 57/89] Bump pylint from 3.0.2 to 3.0.3 (#5905) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index cb96f79587..eef69b7515 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,4 +1,4 @@ -pylint==3.0.2 +pylint==3.0.3 flake8==6.1.0 # also change in .pre-commit-config.yaml when updating black==23.11.0 # also change in .pre-commit-config.yaml when updating pyupgrade==3.15.0 # also change in .pre-commit-config.yaml when updating From d0dd0e38db46cbe37de8a6f894a16e4be8120113 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 Dec 2023 13:51:36 +0900 Subject: [PATCH 58/89] Bump frenck/action-yamllint from 1.4.1 to 1.4.2 (#5716) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/yaml-lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/yaml-lint.yml b/.github/workflows/yaml-lint.yml index a77bd2c078..c9f056b18c 100644 --- a/.github/workflows/yaml-lint.yml +++ b/.github/workflows/yaml-lint.yml @@ -19,4 +19,4 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v4.1.1 - name: Run yamllint - uses: frenck/action-yamllint@v1.4.1 + uses: frenck/action-yamllint@v1.4.2 From 47665164e80f22500e67994c470bb6fc7627d42c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 Dec 2023 13:52:05 +0900 Subject: [PATCH 59/89] Bump dessant/lock-threads from 4.0.1 to 5.0.1 (#5820) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/lock.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml index b455e3f4ea..e3d75f6d58 100644 --- a/.github/workflows/lock.yml +++ b/.github/workflows/lock.yml @@ -18,7 +18,7 @@ jobs: lock: runs-on: ubuntu-latest steps: - - uses: dessant/lock-threads@v4.0.1 + - uses: dessant/lock-threads@v5.0.1 with: pr-inactive-days: "1" pr-lock-reason: "" From ad79e4fe24fd24267391603f2657ecc92adf5762 Mon Sep 17 00:00:00 2001 From: Tucker Kern Date: Mon, 11 Dec 2023 22:13:26 -0700 Subject: [PATCH 60/89] Add support for fan preset modes (#5694) Co-authored-by: J. Nick Koston --- esphome/components/api/api.proto | 4 ++ esphome/components/api/api_connection.cpp | 6 ++ esphome/components/api/api_pb2.cpp | 52 +++++++++++++++++ esphome/components/api/api_pb2.h | 6 ++ esphome/components/copy/fan/copy_fan.cpp | 5 ++ esphome/components/fan/__init__.py | 44 ++++++++++++++ esphome/components/fan/automation.h | 18 ++++++ esphome/components/fan/fan.cpp | 58 +++++++++++++++++-- esphome/components/fan/fan.h | 9 +++ esphome/components/fan/fan_traits.h | 10 ++++ esphome/components/hbridge/fan/__init__.py | 7 ++- .../components/hbridge/fan/hbridge_fan.cpp | 11 +++- esphome/components/hbridge/fan/hbridge_fan.h | 7 ++- esphome/components/speed/fan/__init__.py | 9 ++- esphome/components/speed/fan/speed_fan.cpp | 11 +++- esphome/components/speed/fan/speed_fan.h | 7 ++- esphome/const.py | 2 + tests/test1.yaml | 28 ++++++++- 18 files changed, 277 insertions(+), 17 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 2f33750686..34d137981e 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -365,6 +365,7 @@ message ListEntitiesFanResponse { bool disabled_by_default = 9; string icon = 10; EntityCategory entity_category = 11; + repeated string supported_preset_modes = 12; } enum FanSpeed { FAN_SPEED_LOW = 0; @@ -387,6 +388,7 @@ message FanStateResponse { FanSpeed speed = 4 [deprecated = true]; FanDirection direction = 5; int32 speed_level = 6; + string preset_mode = 7; } message FanCommandRequest { option (id) = 31; @@ -405,6 +407,8 @@ message FanCommandRequest { FanDirection direction = 9; bool has_speed_level = 10; int32 speed_level = 11; + bool has_preset_mode = 12; + string preset_mode = 13; } // ==================== LIGHT ==================== diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 0389df215f..b8f5b05538 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -293,6 +293,8 @@ bool APIConnection::send_fan_state(fan::Fan *fan) { } if (traits.supports_direction()) resp.direction = static_cast(fan->direction); + if (traits.supports_preset_modes()) + resp.preset_mode = fan->preset_mode; return this->send_fan_state_response(resp); } bool APIConnection::send_fan_info(fan::Fan *fan) { @@ -307,6 +309,8 @@ bool APIConnection::send_fan_info(fan::Fan *fan) { msg.supports_speed = traits.supports_speed(); msg.supports_direction = traits.supports_direction(); msg.supported_speed_count = traits.supported_speed_count(); + for (auto const &preset : traits.supported_preset_modes()) + msg.supported_preset_modes.push_back(preset); msg.disabled_by_default = fan->is_disabled_by_default(); msg.icon = fan->get_icon(); msg.entity_category = static_cast(fan->get_entity_category()); @@ -328,6 +332,8 @@ void APIConnection::fan_command(const FanCommandRequest &msg) { } if (msg.has_direction) call.set_direction(static_cast(msg.direction)); + if (msg.has_preset_mode) + call.set_preset_mode(msg.preset_mode); call.perform(); } #endif diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 1e97a57bb1..01fb540e7e 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -1375,6 +1375,10 @@ bool ListEntitiesFanResponse::decode_length(uint32_t field_id, ProtoLengthDelimi this->icon = value.as_string(); return true; } + case 12: { + this->supported_preset_modes.push_back(value.as_string()); + return true; + } default: return false; } @@ -1401,6 +1405,9 @@ void ListEntitiesFanResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(9, this->disabled_by_default); buffer.encode_string(10, this->icon); buffer.encode_enum(11, this->entity_category); + for (auto &it : this->supported_preset_modes) { + buffer.encode_string(12, it, true); + } } #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesFanResponse::dump_to(std::string &out) const { @@ -1451,6 +1458,12 @@ void ListEntitiesFanResponse::dump_to(std::string &out) const { out.append(" entity_category: "); out.append(proto_enum_to_string(this->entity_category)); out.append("\n"); + + for (const auto &it : this->supported_preset_modes) { + out.append(" supported_preset_modes: "); + out.append("'").append(it).append("'"); + out.append("\n"); + } out.append("}"); } #endif @@ -1480,6 +1493,16 @@ bool FanStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { return false; } } +bool FanStateResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 7: { + this->preset_mode = value.as_string(); + return true; + } + default: + return false; + } +} bool FanStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { switch (field_id) { case 1: { @@ -1497,6 +1520,7 @@ void FanStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_enum(4, this->speed); buffer.encode_enum(5, this->direction); buffer.encode_int32(6, this->speed_level); + buffer.encode_string(7, this->preset_mode); } #ifdef HAS_PROTO_MESSAGE_DUMP void FanStateResponse::dump_to(std::string &out) const { @@ -1527,6 +1551,10 @@ void FanStateResponse::dump_to(std::string &out) const { sprintf(buffer, "%" PRId32, this->speed_level); out.append(buffer); out.append("\n"); + + out.append(" preset_mode: "); + out.append("'").append(this->preset_mode).append("'"); + out.append("\n"); out.append("}"); } #endif @@ -1572,6 +1600,20 @@ bool FanCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { this->speed_level = value.as_int32(); return true; } + case 12: { + this->has_preset_mode = value.as_bool(); + return true; + } + default: + return false; + } +} +bool FanCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 13: { + this->preset_mode = value.as_string(); + return true; + } default: return false; } @@ -1598,6 +1640,8 @@ void FanCommandRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_enum(9, this->direction); buffer.encode_bool(10, this->has_speed_level); buffer.encode_int32(11, this->speed_level); + buffer.encode_bool(12, this->has_preset_mode); + buffer.encode_string(13, this->preset_mode); } #ifdef HAS_PROTO_MESSAGE_DUMP void FanCommandRequest::dump_to(std::string &out) const { @@ -1648,6 +1692,14 @@ void FanCommandRequest::dump_to(std::string &out) const { sprintf(buffer, "%" PRId32, this->speed_level); out.append(buffer); out.append("\n"); + + out.append(" has_preset_mode: "); + out.append(YESNO(this->has_preset_mode)); + out.append("\n"); + + out.append(" preset_mode: "); + out.append("'").append(this->preset_mode).append("'"); + out.append("\n"); out.append("}"); } #endif diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index a63e90b7b7..fc57348863 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -472,6 +472,7 @@ class ListEntitiesFanResponse : public ProtoMessage { bool disabled_by_default{false}; std::string icon{}; enums::EntityCategory entity_category{}; + std::vector supported_preset_modes{}; void encode(ProtoWriteBuffer buffer) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; @@ -490,6 +491,7 @@ class FanStateResponse : public ProtoMessage { enums::FanSpeed speed{}; enums::FanDirection direction{}; int32_t speed_level{0}; + std::string preset_mode{}; void encode(ProtoWriteBuffer buffer) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; @@ -497,6 +499,7 @@ class FanStateResponse : public ProtoMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; class FanCommandRequest : public ProtoMessage { @@ -512,6 +515,8 @@ class FanCommandRequest : public ProtoMessage { enums::FanDirection direction{}; bool has_speed_level{false}; int32_t speed_level{0}; + bool has_preset_mode{false}; + std::string preset_mode{}; void encode(ProtoWriteBuffer buffer) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; @@ -519,6 +524,7 @@ class FanCommandRequest : public ProtoMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; class ListEntitiesLightResponse : public ProtoMessage { diff --git a/esphome/components/copy/fan/copy_fan.cpp b/esphome/components/copy/fan/copy_fan.cpp index 74d9da279f..15a7f5e025 100644 --- a/esphome/components/copy/fan/copy_fan.cpp +++ b/esphome/components/copy/fan/copy_fan.cpp @@ -12,6 +12,7 @@ void CopyFan::setup() { this->oscillating = source_->oscillating; this->speed = source_->speed; this->direction = source_->direction; + this->preset_mode = source_->preset_mode; this->publish_state(); }); @@ -19,6 +20,7 @@ void CopyFan::setup() { this->oscillating = source_->oscillating; this->speed = source_->speed; this->direction = source_->direction; + this->preset_mode = source_->preset_mode; this->publish_state(); } @@ -33,6 +35,7 @@ fan::FanTraits CopyFan::get_traits() { traits.set_speed(base.supports_speed()); traits.set_supported_speed_count(base.supported_speed_count()); traits.set_direction(base.supports_direction()); + traits.set_supported_preset_modes(base.supported_preset_modes()); return traits; } @@ -46,6 +49,8 @@ void CopyFan::control(const fan::FanCall &call) { call2.set_speed(*call.get_speed()); if (call.get_direction().has_value()) call2.set_direction(*call.get_direction()); + if (!call.get_preset_mode().empty()) + call2.set_preset_mode(call.get_preset_mode()); call2.perform(); } diff --git a/esphome/components/fan/__init__.py b/esphome/components/fan/__init__.py index 23df3c2214..fd0f2f66cb 100644 --- a/esphome/components/fan/__init__.py +++ b/esphome/components/fan/__init__.py @@ -18,6 +18,7 @@ from esphome.const import ( CONF_ON_SPEED_SET, CONF_ON_TURN_OFF, CONF_ON_TURN_ON, + CONF_ON_PRESET_SET, CONF_TRIGGER_ID, CONF_DIRECTION, CONF_RESTORE_MODE, @@ -57,6 +58,9 @@ CycleSpeedAction = fan_ns.class_("CycleSpeedAction", automation.Action) FanTurnOnTrigger = fan_ns.class_("FanTurnOnTrigger", automation.Trigger.template()) FanTurnOffTrigger = fan_ns.class_("FanTurnOffTrigger", automation.Trigger.template()) FanSpeedSetTrigger = fan_ns.class_("FanSpeedSetTrigger", automation.Trigger.template()) +FanPresetSetTrigger = fan_ns.class_( + "FanPresetSetTrigger", automation.Trigger.template() +) FanIsOnCondition = fan_ns.class_("FanIsOnCondition", automation.Condition.template()) FanIsOffCondition = fan_ns.class_("FanIsOffCondition", automation.Condition.template()) @@ -101,9 +105,46 @@ FAN_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).exte cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(FanSpeedSetTrigger), } ), + cv.Optional(CONF_ON_PRESET_SET): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(FanPresetSetTrigger), + } + ), } ) +_PRESET_MODES_SCHEMA = cv.All( + cv.ensure_list(cv.string_strict), + cv.Length(min=1), +) + + +def validate_preset_modes(value): + # Check against defined schema + value = _PRESET_MODES_SCHEMA(value) + + # Ensure preset names are unique + errors = [] + presets = set() + for i, preset in enumerate(value): + # If name does not exist yet add it + if preset not in presets: + presets.add(preset) + continue + + # Otherwise it's an error + errors.append( + cv.Invalid( + f"Found duplicate preset name '{preset}'. Presets must have unique names.", + [i], + ) + ) + + if errors: + raise cv.MultipleInvalid(errors) + + return value + async def setup_fan_core_(var, config): await setup_entity(var, config) @@ -154,6 +195,9 @@ async def setup_fan_core_(var, config): for conf in config.get(CONF_ON_SPEED_SET, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation(trigger, [], conf) + for conf in config.get(CONF_ON_PRESET_SET, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) async def register_fan(var, config): diff --git a/esphome/components/fan/automation.h b/esphome/components/fan/automation.h index 511acf5682..b5bdeb8a29 100644 --- a/esphome/components/fan/automation.h +++ b/esphome/components/fan/automation.h @@ -165,5 +165,23 @@ class FanSpeedSetTrigger : public Trigger<> { int last_speed_; }; +class FanPresetSetTrigger : public Trigger<> { + public: + FanPresetSetTrigger(Fan *state) { + state->add_on_state_callback([this, state]() { + auto preset_mode = state->preset_mode; + auto should_trigger = preset_mode != this->last_preset_mode_; + this->last_preset_mode_ = preset_mode; + if (should_trigger) { + this->trigger(); + } + }); + this->last_preset_mode_ = state->preset_mode; + } + + protected: + std::string last_preset_mode_; +}; + } // namespace fan } // namespace esphome diff --git a/esphome/components/fan/fan.cpp b/esphome/components/fan/fan.cpp index 87566bad4a..95e3ae0758 100644 --- a/esphome/components/fan/fan.cpp +++ b/esphome/components/fan/fan.cpp @@ -32,9 +32,12 @@ void FanCall::perform() { if (this->direction_.has_value()) { ESP_LOGD(TAG, " Direction: %s", LOG_STR_ARG(fan_direction_to_string(*this->direction_))); } - + if (!this->preset_mode_.empty()) { + ESP_LOGD(TAG, " Preset Mode: %s", this->preset_mode_.c_str()); + } this->parent_.control(*this); } + void FanCall::validate_() { auto traits = this->parent_.get_traits(); @@ -62,6 +65,15 @@ void FanCall::validate_() { ESP_LOGW(TAG, "'%s' - This fan does not support directions!", this->parent_.get_name().c_str()); this->direction_.reset(); } + + if (!this->preset_mode_.empty()) { + const auto &preset_modes = traits.supported_preset_modes(); + if (preset_modes.find(this->preset_mode_) == preset_modes.end()) { + ESP_LOGW(TAG, "'%s' - This fan does not support preset mode '%s'!", this->parent_.get_name().c_str(), + this->preset_mode_.c_str()); + this->preset_mode_.clear(); + } + } } FanCall FanRestoreState::to_call(Fan &fan) { @@ -70,6 +82,14 @@ FanCall FanRestoreState::to_call(Fan &fan) { call.set_oscillating(this->oscillating); call.set_speed(this->speed); call.set_direction(this->direction); + + if (fan.get_traits().supports_preset_modes()) { + // Use stored preset index to get preset name + const auto &preset_modes = fan.get_traits().supported_preset_modes(); + if (this->preset_mode < preset_modes.size()) { + call.set_preset_mode(*std::next(preset_modes.begin(), this->preset_mode)); + } + } return call; } void FanRestoreState::apply(Fan &fan) { @@ -77,6 +97,14 @@ void FanRestoreState::apply(Fan &fan) { fan.oscillating = this->oscillating; fan.speed = this->speed; fan.direction = this->direction; + + if (fan.get_traits().supports_preset_modes()) { + // Use stored preset index to get preset name + const auto &preset_modes = fan.get_traits().supported_preset_modes(); + if (this->preset_mode < preset_modes.size()) { + fan.preset_mode = *std::next(preset_modes.begin(), this->preset_mode); + } + } fan.publish_state(); } @@ -100,7 +128,9 @@ void Fan::publish_state() { if (traits.supports_direction()) { ESP_LOGD(TAG, " Direction: %s", LOG_STR_ARG(fan_direction_to_string(this->direction))); } - + if (traits.supports_preset_modes() && !this->preset_mode.empty()) { + ESP_LOGD(TAG, " Preset Mode: %s", this->preset_mode.c_str()); + } this->state_callback_.call(); this->save_state_(); } @@ -143,20 +173,36 @@ void Fan::save_state_() { state.oscillating = this->oscillating; state.speed = this->speed; state.direction = this->direction; + + if (this->get_traits().supports_preset_modes() && !this->preset_mode.empty()) { + const auto &preset_modes = this->get_traits().supported_preset_modes(); + // Store index of current preset mode + auto preset_iterator = preset_modes.find(this->preset_mode); + if (preset_iterator != preset_modes.end()) + state.preset_mode = std::distance(preset_modes.begin(), preset_iterator); + } + this->rtc_.save(&state); } void Fan::dump_traits_(const char *tag, const char *prefix) { - if (this->get_traits().supports_speed()) { + auto traits = this->get_traits(); + + if (traits.supports_speed()) { ESP_LOGCONFIG(tag, "%s Speed: YES", prefix); - ESP_LOGCONFIG(tag, "%s Speed count: %d", prefix, this->get_traits().supported_speed_count()); + ESP_LOGCONFIG(tag, "%s Speed count: %d", prefix, traits.supported_speed_count()); } - if (this->get_traits().supports_oscillation()) { + if (traits.supports_oscillation()) { ESP_LOGCONFIG(tag, "%s Oscillation: YES", prefix); } - if (this->get_traits().supports_direction()) { + if (traits.supports_direction()) { ESP_LOGCONFIG(tag, "%s Direction: YES", prefix); } + if (traits.supports_preset_modes()) { + ESP_LOGCONFIG(tag, "%s Supported presets:", prefix); + for (const std::string &s : traits.supported_preset_modes()) + ESP_LOGCONFIG(tag, "%s - %s", prefix, s.c_str()); + } } } // namespace fan diff --git a/esphome/components/fan/fan.h b/esphome/components/fan/fan.h index f9d317e675..b74187eb4a 100644 --- a/esphome/components/fan/fan.h +++ b/esphome/components/fan/fan.h @@ -72,6 +72,11 @@ class FanCall { return *this; } optional get_direction() const { return this->direction_; } + FanCall &set_preset_mode(const std::string &preset_mode) { + this->preset_mode_ = preset_mode; + return *this; + } + std::string get_preset_mode() const { return this->preset_mode_; } void perform(); @@ -83,6 +88,7 @@ class FanCall { optional oscillating_; optional speed_; optional direction_{}; + std::string preset_mode_{}; }; struct FanRestoreState { @@ -90,6 +96,7 @@ struct FanRestoreState { int speed; bool oscillating; FanDirection direction; + uint8_t preset_mode; /// Convert this struct to a fan call that can be performed. FanCall to_call(Fan &fan); @@ -107,6 +114,8 @@ class Fan : public EntityBase { int speed{0}; /// The current direction of the fan FanDirection direction{FanDirection::FORWARD}; + // The current preset mode of the fan + std::string preset_mode{}; FanCall turn_on(); FanCall turn_off(); diff --git a/esphome/components/fan/fan_traits.h b/esphome/components/fan/fan_traits.h index e69d8e2e53..2ef6f8b7cc 100644 --- a/esphome/components/fan/fan_traits.h +++ b/esphome/components/fan/fan_traits.h @@ -1,3 +1,6 @@ +#include +#include + #pragma once namespace esphome { @@ -25,12 +28,19 @@ class FanTraits { bool supports_direction() const { return this->direction_; } /// Set whether this fan supports changing direction void set_direction(bool direction) { this->direction_ = direction; } + /// Return the preset modes supported by the fan. + std::set supported_preset_modes() const { return this->preset_modes_; } + /// Set the preset modes supported by the fan. + void set_supported_preset_modes(const std::set &preset_modes) { this->preset_modes_ = preset_modes; } + /// Return if preset modes are supported + bool supports_preset_modes() const { return !this->preset_modes_.empty(); } protected: bool oscillation_{false}; bool speed_{false}; bool direction_{false}; int speed_count_{}; + std::set preset_modes_{}; }; } // namespace fan diff --git a/esphome/components/hbridge/fan/__init__.py b/esphome/components/hbridge/fan/__init__.py index 421883a1ff..424e944290 100644 --- a/esphome/components/hbridge/fan/__init__.py +++ b/esphome/components/hbridge/fan/__init__.py @@ -3,6 +3,7 @@ import esphome.config_validation as cv from esphome import automation from esphome.automation import maybe_simple_id from esphome.components import fan, output +from esphome.components.fan import validate_preset_modes from esphome.const import ( CONF_ID, CONF_DECAY_MODE, @@ -10,6 +11,7 @@ from esphome.const import ( CONF_PIN_A, CONF_PIN_B, CONF_ENABLE_PIN, + CONF_PRESET_MODES, ) from .. import hbridge_ns @@ -28,7 +30,6 @@ DECAY_MODE_OPTIONS = { # Actions BrakeAction = hbridge_ns.class_("BrakeAction", automation.Action) - CONFIG_SCHEMA = fan.FAN_SCHEMA.extend( { cv.GenerateID(CONF_ID): cv.declare_id(HBridgeFan), @@ -39,6 +40,7 @@ CONFIG_SCHEMA = fan.FAN_SCHEMA.extend( ), cv.Optional(CONF_SPEED_COUNT, default=100): cv.int_range(min=1), cv.Optional(CONF_ENABLE_PIN): cv.use_id(output.FloatOutput), + cv.Optional(CONF_PRESET_MODES): validate_preset_modes, } ).extend(cv.COMPONENT_SCHEMA) @@ -69,3 +71,6 @@ async def to_code(config): if CONF_ENABLE_PIN in config: enable_pin = await cg.get_variable(config[CONF_ENABLE_PIN]) cg.add(var.set_enable_pin(enable_pin)) + + if CONF_PRESET_MODES in config: + cg.add(var.set_preset_modes(config[CONF_PRESET_MODES])) diff --git a/esphome/components/hbridge/fan/hbridge_fan.cpp b/esphome/components/hbridge/fan/hbridge_fan.cpp index 44cf5ae049..605a9d4ef3 100644 --- a/esphome/components/hbridge/fan/hbridge_fan.cpp +++ b/esphome/components/hbridge/fan/hbridge_fan.cpp @@ -33,7 +33,12 @@ void HBridgeFan::setup() { restore->apply(*this); this->write_state_(); } + + // Construct traits + this->traits_ = fan::FanTraits(this->oscillating_ != nullptr, true, true, this->speed_count_); + this->traits_.set_supported_preset_modes(this->preset_modes_); } + void HBridgeFan::dump_config() { LOG_FAN("", "H-Bridge Fan", this); if (this->decay_mode_ == DECAY_MODE_SLOW) { @@ -42,9 +47,7 @@ void HBridgeFan::dump_config() { ESP_LOGCONFIG(TAG, " Decay Mode: Fast"); } } -fan::FanTraits HBridgeFan::get_traits() { - return fan::FanTraits(this->oscillating_ != nullptr, true, true, this->speed_count_); -} + void HBridgeFan::control(const fan::FanCall &call) { if (call.get_state().has_value()) this->state = *call.get_state(); @@ -54,10 +57,12 @@ void HBridgeFan::control(const fan::FanCall &call) { this->oscillating = *call.get_oscillating(); if (call.get_direction().has_value()) this->direction = *call.get_direction(); + this->preset_mode = call.get_preset_mode(); this->write_state_(); this->publish_state(); } + void HBridgeFan::write_state_() { float speed = this->state ? static_cast(this->speed) / static_cast(this->speed_count_) : 0.0f; if (speed == 0.0f) { // off means idle diff --git a/esphome/components/hbridge/fan/hbridge_fan.h b/esphome/components/hbridge/fan/hbridge_fan.h index 4389b97ccb..4234fccae3 100644 --- a/esphome/components/hbridge/fan/hbridge_fan.h +++ b/esphome/components/hbridge/fan/hbridge_fan.h @@ -1,5 +1,7 @@ #pragma once +#include + #include "esphome/core/automation.h" #include "esphome/components/output/binary_output.h" #include "esphome/components/output/float_output.h" @@ -20,10 +22,11 @@ class HBridgeFan : public Component, public fan::Fan { void set_pin_a(output::FloatOutput *pin_a) { pin_a_ = pin_a; } void set_pin_b(output::FloatOutput *pin_b) { pin_b_ = pin_b; } void set_enable_pin(output::FloatOutput *enable) { enable_ = enable; } + void set_preset_modes(const std::set &presets) { preset_modes_ = presets; } void setup() override; void dump_config() override; - fan::FanTraits get_traits() override; + fan::FanTraits get_traits() override { return this->traits_; } fan::FanCall brake(); @@ -34,6 +37,8 @@ class HBridgeFan : public Component, public fan::Fan { output::BinaryOutput *oscillating_{nullptr}; int speed_count_{}; DecayMode decay_mode_{DECAY_MODE_SLOW}; + fan::FanTraits traits_; + std::set preset_modes_{}; void control(const fan::FanCall &call) override; void write_state_(); diff --git a/esphome/components/speed/fan/__init__.py b/esphome/components/speed/fan/__init__.py index 978e68d1e9..3acfb005bd 100644 --- a/esphome/components/speed/fan/__init__.py +++ b/esphome/components/speed/fan/__init__.py @@ -1,14 +1,17 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import fan, output +from esphome.components.fan import validate_preset_modes from esphome.const import ( + CONF_PRESET_MODES, + CONF_DIRECTION_OUTPUT, CONF_OSCILLATION_OUTPUT, CONF_OUTPUT, - CONF_DIRECTION_OUTPUT, CONF_OUTPUT_ID, CONF_SPEED, CONF_SPEED_COUNT, ) + from .. import speed_ns SpeedFan = speed_ns.class_("SpeedFan", cg.Component, fan.Fan) @@ -23,6 +26,7 @@ CONFIG_SCHEMA = fan.FAN_SCHEMA.extend( "Configuring individual speeds is deprecated." ), cv.Optional(CONF_SPEED_COUNT, default=100): cv.int_range(min=1), + cv.Optional(CONF_PRESET_MODES): validate_preset_modes, } ).extend(cv.COMPONENT_SCHEMA) @@ -40,3 +44,6 @@ async def to_code(config): if CONF_DIRECTION_OUTPUT in config: direction_output = await cg.get_variable(config[CONF_DIRECTION_OUTPUT]) cg.add(var.set_direction(direction_output)) + + if CONF_PRESET_MODES in config: + cg.add(var.set_preset_modes(config[CONF_PRESET_MODES])) diff --git a/esphome/components/speed/fan/speed_fan.cpp b/esphome/components/speed/fan/speed_fan.cpp index 3a65f2c365..41b222acd6 100644 --- a/esphome/components/speed/fan/speed_fan.cpp +++ b/esphome/components/speed/fan/speed_fan.cpp @@ -12,11 +12,14 @@ void SpeedFan::setup() { restore->apply(*this); this->write_state_(); } + + // Construct traits + this->traits_ = fan::FanTraits(this->oscillating_ != nullptr, true, this->direction_ != nullptr, this->speed_count_); + this->traits_.set_supported_preset_modes(this->preset_modes_); } + void SpeedFan::dump_config() { LOG_FAN("", "Speed Fan", this); } -fan::FanTraits SpeedFan::get_traits() { - return fan::FanTraits(this->oscillating_ != nullptr, true, this->direction_ != nullptr, this->speed_count_); -} + void SpeedFan::control(const fan::FanCall &call) { if (call.get_state().has_value()) this->state = *call.get_state(); @@ -26,10 +29,12 @@ void SpeedFan::control(const fan::FanCall &call) { this->oscillating = *call.get_oscillating(); if (call.get_direction().has_value()) this->direction = *call.get_direction(); + this->preset_mode = call.get_preset_mode(); this->write_state_(); this->publish_state(); } + void SpeedFan::write_state_() { float speed = this->state ? static_cast(this->speed) / static_cast(this->speed_count_) : 0.0f; this->output_->set_level(speed); diff --git a/esphome/components/speed/fan/speed_fan.h b/esphome/components/speed/fan/speed_fan.h index 1fad53813a..ca0fe20e2a 100644 --- a/esphome/components/speed/fan/speed_fan.h +++ b/esphome/components/speed/fan/speed_fan.h @@ -1,5 +1,7 @@ #pragma once +#include + #include "esphome/core/component.h" #include "esphome/components/output/binary_output.h" #include "esphome/components/output/float_output.h" @@ -15,7 +17,8 @@ class SpeedFan : public Component, public fan::Fan { void dump_config() override; void set_oscillating(output::BinaryOutput *oscillating) { this->oscillating_ = oscillating; } void set_direction(output::BinaryOutput *direction) { this->direction_ = direction; } - fan::FanTraits get_traits() override; + void set_preset_modes(const std::set &presets) { this->preset_modes_ = presets; } + fan::FanTraits get_traits() override { return this->traits_; } protected: void control(const fan::FanCall &call) override; @@ -25,6 +28,8 @@ class SpeedFan : public Component, public fan::Fan { output::BinaryOutput *oscillating_{nullptr}; output::BinaryOutput *direction_{nullptr}; int speed_count_{}; + fan::FanTraits traits_; + std::set preset_modes_{}; }; } // namespace speed diff --git a/esphome/const.py b/esphome/const.py index 2bc088c063..f688cd75f9 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -504,6 +504,7 @@ CONF_ON_LOOP = "on_loop" CONF_ON_MESSAGE = "on_message" CONF_ON_MULTI_CLICK = "on_multi_click" CONF_ON_OPEN = "on_open" +CONF_ON_PRESET_SET = "on_preset_set" CONF_ON_PRESS = "on_press" CONF_ON_RAW_VALUE = "on_raw_value" CONF_ON_RELEASE = "on_release" @@ -601,6 +602,7 @@ CONF_PRESET = "preset" CONF_PRESET_BOOST = "preset_boost" CONF_PRESET_COMMAND_TOPIC = "preset_command_topic" CONF_PRESET_ECO = "preset_eco" +CONF_PRESET_MODES = "preset_modes" CONF_PRESET_SLEEP = "preset_sleep" CONF_PRESET_STATE_TOPIC = "preset_state_topic" CONF_PRESSURE = "pressure" diff --git a/tests/test1.yaml b/tests/test1.yaml index 559b7ab8fc..b77cff7619 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -2944,6 +2944,33 @@ fan: on_speed_set: then: - logger.log: Fan speed was changed! + - platform: speed + id: fan_speed_presets + icon: mdi:weather-windy + output: pca_6 + speed_count: 10 + name: Speed Fan w/ Presets + oscillation_output: gpio_19 + direction_output: gpio_26 + preset_modes: + - Preset 1 + - Preset 2 + on_preset_set: + then: + - logger.log: Preset mode was changed! + - platform: hbridge + id: fan_hbridge_presets + icon: mdi:weather-windy + speed_count: 4 + name: H-bridge Fan w/ Presets + pin_a: pca_6 + pin_b: pca_7 + preset_modes: + - Preset 1 + - Preset 2 + on_preset_set: + then: + - logger.log: Preset mode was changed! - platform: bedjet name: My Bedjet fan bedjet_id: my_bedjet_client @@ -4193,4 +4220,3 @@ alarm_control_panel: then: - lambda: !lambda |- ESP_LOGD("TEST", "State change %s", alarm_control_panel_state_to_string(id(alarmcontrolpanel1)->get_state())); - From cc7d167e8b0996ff24e75ea0feac386ec0ef6f1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=87etin=20Koca?= <_@cetinkoca.com> Date: Tue, 12 Dec 2023 08:17:56 +0300 Subject: [PATCH 61/89] Fix uninitialized climate target temperature (#5795) --- esphome/components/climate/climate.h | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/esphome/components/climate/climate.h b/esphome/components/climate/climate.h index f90db3f52a..1bbb17322d 100644 --- a/esphome/components/climate/climate.h +++ b/esphome/components/climate/climate.h @@ -160,6 +160,8 @@ struct ClimateDeviceRestoreState { */ class Climate : public EntityBase { public: + Climate() {} + /// The active mode of the climate device. ClimateMode mode{CLIMATE_MODE_OFF}; /// The active state of the climate device. @@ -172,9 +174,9 @@ class Climate : public EntityBase { float target_temperature; struct { /// The minimum target temperature of the climate device, for climate devices with split target temperature. - float target_temperature_low; + float target_temperature_low{NAN}; /// The maximum target temperature of the climate device, for climate devices with split target temperature. - float target_temperature_high; + float target_temperature_high{NAN}; }; }; From 04720b8440b43c8183df004720323d106e8ff51a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 Dec 2023 09:39:29 -1000 Subject: [PATCH 62/89] Bump aioesphomeapi from 19.3.0 to 20.0.0 (#5911) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 39715232d6..ae8d50f2ea 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ platformio==6.1.11 # When updating platformio, also update Dockerfile esptool==4.6.2 click==8.1.7 esphome-dashboard==20231107.0 -aioesphomeapi==19.3.0 +aioesphomeapi==20.0.0 zeroconf==0.128.4 python-magic==0.4.27 From 8e92bb79589701da72120b26966cc139911bf88c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 Dec 2023 21:59:10 +0000 Subject: [PATCH 63/89] Bump black from 23.11.0 to 23.12.0 (#5912) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston --- .pre-commit-config.yaml | 2 +- esphome/writer.py | 4 +++- requirements_test.txt | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index dc22265f1f..36ec1894d8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,7 +3,7 @@ # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/psf/black-pre-commit-mirror - rev: 23.11.0 + rev: 23.12.0 hooks: - id: black args: diff --git a/esphome/writer.py b/esphome/writer.py index ad506b6ae6..83e95614a6 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -203,7 +203,9 @@ def write_platformio_project(): write_platformio_ini(content) -DEFINES_H_FORMAT = ESPHOME_H_FORMAT = """\ +DEFINES_H_FORMAT = ( + ESPHOME_H_FORMAT +) = """\ #pragma once #include "esphome/core/macros.h" {} diff --git a/requirements_test.txt b/requirements_test.txt index eef69b7515..18c6dedf3e 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,6 +1,6 @@ pylint==3.0.3 flake8==6.1.0 # also change in .pre-commit-config.yaml when updating -black==23.11.0 # also change in .pre-commit-config.yaml when updating +black==23.12.0 # also change in .pre-commit-config.yaml when updating pyupgrade==3.15.0 # also change in .pre-commit-config.yaml when updating pre-commit From c6dc336c4acfb8a1d6142d342daf8ecccefc7fc5 Mon Sep 17 00:00:00 2001 From: NP v/d Spek Date: Tue, 12 Dec 2023 23:56:01 +0100 Subject: [PATCH 64/89] Updating the touchscreen interface structure (#4596) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Co-authored-by: NP v/d Spek Co-authored-by: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Co-authored-by: Gustavo Ambrozio --- CODEOWNERS | 8 +- .../__init__.py} | 8 +- .../ektf2232/{ => touchscreen}/ektf2232.cpp | 63 +----- .../ektf2232/{ => touchscreen}/ektf2232.h | 14 +- esphome/components/ft5x06/__init__.py | 6 + .../components/ft5x06/touchscreen/__init__.py | 26 +++ .../ft5x06/touchscreen/ft5x06_touchscreen.h | 124 +++++++++++ esphome/components/ft63x6/__init__.py | 1 + esphome/components/ft63x6/ft63x6.cpp | 99 +++++++++ esphome/components/ft63x6/ft63x6.h | 41 ++++ esphome/components/ft63x6/touchscreen.py | 44 ++++ .../components/gt911/touchscreen/__init__.py | 35 +-- .../gt911/touchscreen/gt911_touchscreen.cpp | 75 +++---- .../gt911/touchscreen/gt911_touchscreen.h | 15 +- esphome/components/ili9xxx/display.py | 6 +- esphome/components/inkplate6/display.py | 6 +- .../lilygo_t5_47/touchscreen/__init__.py | 8 +- .../touchscreen/lilygo_t5_47_touchscreen.cpp | 108 ++------- .../touchscreen/lilygo_t5_47_touchscreen.h | 16 +- esphome/components/touchscreen/__init__.py | 52 ++++- .../touchscreen_binary_sensor.cpp | 3 +- .../components/touchscreen/touchscreen.cpp | 131 +++++++++-- esphome/components/touchscreen/touchscreen.h | 109 +++++++-- .../tt21100/touchscreen/__init__.py | 8 +- .../tt21100/touchscreen/tt21100.cpp | 52 +---- .../components/tt21100/touchscreen/tt21100.h | 14 +- esphome/components/xpt2046/binary_sensor.py | 3 - esphome/components/xpt2046/touchscreen.py | 116 ---------- .../xpt2046/touchscreen/__init__.py | 93 ++++++++ .../xpt2046/touchscreen/xpt2046.cpp | 113 ++++++++++ .../components/xpt2046/touchscreen/xpt2046.h | 41 ++++ esphome/components/xpt2046/xpt2046.cpp | 207 ------------------ esphome/components/xpt2046/xpt2046.h | 107 --------- tests/test4.yaml | 79 ++++--- tests/test8.yaml | 2 + 35 files changed, 997 insertions(+), 836 deletions(-) rename esphome/components/ektf2232/{touchscreen.py => touchscreen/__init__.py} (89%) rename esphome/components/ektf2232/{ => touchscreen}/ektf2232.cpp (60%) rename esphome/components/ektf2232/{ => touchscreen}/ektf2232.h (67%) create mode 100644 esphome/components/ft5x06/__init__.py create mode 100644 esphome/components/ft5x06/touchscreen/__init__.py create mode 100644 esphome/components/ft5x06/touchscreen/ft5x06_touchscreen.h create mode 100644 esphome/components/ft63x6/__init__.py create mode 100644 esphome/components/ft63x6/ft63x6.cpp create mode 100644 esphome/components/ft63x6/ft63x6.h create mode 100644 esphome/components/ft63x6/touchscreen.py delete mode 100644 esphome/components/xpt2046/binary_sensor.py delete mode 100644 esphome/components/xpt2046/touchscreen.py create mode 100644 esphome/components/xpt2046/touchscreen/__init__.py create mode 100644 esphome/components/xpt2046/touchscreen/xpt2046.cpp create mode 100644 esphome/components/xpt2046/touchscreen/xpt2046.h delete mode 100644 esphome/components/xpt2046/xpt2046.cpp delete mode 100644 esphome/components/xpt2046/xpt2046.h diff --git a/CODEOWNERS b/CODEOWNERS index 43b7647fe6..c37eb3581b 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -88,7 +88,7 @@ esphome/components/ds1307/* @badbadc0ffee esphome/components/dsmr/* @glmnet @zuidwijk esphome/components/duty_time/* @dudanov esphome/components/ee895/* @Stock-M -esphome/components/ektf2232/* @jesserockz +esphome/components/ektf2232/touchscreen/* @jesserockz esphome/components/emc2101/* @ellull esphome/components/ens160/* @vincentscode esphome/components/ens210/* @itn3rd77 @@ -110,6 +110,8 @@ esphome/components/fastled_base/* @OttoWinter esphome/components/feedback/* @ianchi esphome/components/fingerprint_grow/* @OnFreund @loongyh esphome/components/fs3000/* @kahrendt +esphome/components/ft5x06/* @clydebarrow +esphome/components/ft63x6/* @gpambrozio esphome/components/gcja5/* @gcormier esphome/components/globals/* @esphome/core esphome/components/gp8403/* @jesserockz @@ -331,7 +333,7 @@ esphome/components/tmp1075/* @sybrenstuvel esphome/components/tmp117/* @Azimath esphome/components/tof10120/* @wstrzalka esphome/components/toshiba/* @kbx81 -esphome/components/touchscreen/* @jesserockz +esphome/components/touchscreen/* @jesserockz @nielsnl68 esphome/components/tsl2591/* @wjcarpenter esphome/components/tt21100/* @kroimon esphome/components/tuya/binary_sensor/* @jesserockz @@ -364,6 +366,6 @@ esphome/components/xiaomi_mhoc303/* @drug123 esphome/components/xiaomi_mhoc401/* @vevsvevs esphome/components/xiaomi_rtcgq02lm/* @jesserockz esphome/components/xl9535/* @mreditor97 -esphome/components/xpt2046/* @nielsnl68 @numo68 +esphome/components/xpt2046/touchscreen/* @nielsnl68 @numo68 esphome/components/zhlt01/* @cfeenstra1024 esphome/components/zio_ultrasonic/* @kahrendt diff --git a/esphome/components/ektf2232/touchscreen.py b/esphome/components/ektf2232/touchscreen/__init__.py similarity index 89% rename from esphome/components/ektf2232/touchscreen.py rename to esphome/components/ektf2232/touchscreen/__init__.py index d937265e7a..c1fefb7f09 100644 --- a/esphome/components/ektf2232/touchscreen.py +++ b/esphome/components/ektf2232/touchscreen/__init__.py @@ -12,7 +12,6 @@ ektf2232_ns = cg.esphome_ns.namespace("ektf2232") EKTF2232Touchscreen = ektf2232_ns.class_( "EKTF2232Touchscreen", touchscreen.Touchscreen, - cg.Component, i2c.I2CDevice, ) @@ -28,17 +27,14 @@ CONFIG_SCHEMA = touchscreen.TOUCHSCREEN_SCHEMA.extend( ), cv.Required(CONF_RTS_PIN): pins.gpio_output_pin_schema, } - ) - .extend(i2c.i2c_device_schema(0x15)) - .extend(cv.COMPONENT_SCHEMA) + ).extend(i2c.i2c_device_schema(0x15)) ) async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - await cg.register_component(var, config) - await i2c.register_i2c_device(var, config) await touchscreen.register_touchscreen(var, config) + await i2c.register_i2c_device(var, config) interrupt_pin = await cg.gpio_pin_expression(config[CONF_INTERRUPT_PIN]) cg.add(var.set_interrupt_pin(interrupt_pin)) diff --git a/esphome/components/ektf2232/ektf2232.cpp b/esphome/components/ektf2232/touchscreen/ektf2232.cpp similarity index 60% rename from esphome/components/ektf2232/ektf2232.cpp rename to esphome/components/ektf2232/touchscreen/ektf2232.cpp index 80f5f8a8e2..1a2c0389af 100644 --- a/esphome/components/ektf2232/ektf2232.cpp +++ b/esphome/components/ektf2232/touchscreen/ektf2232.cpp @@ -15,16 +15,12 @@ static const uint8_t GET_X_RES[4] = {0x53, 0x60, 0x00, 0x00}; static const uint8_t GET_Y_RES[4] = {0x53, 0x63, 0x00, 0x00}; static const uint8_t GET_POWER_STATE_CMD[4] = {0x53, 0x50, 0x00, 0x01}; -void EKTF2232TouchscreenStore::gpio_intr(EKTF2232TouchscreenStore *store) { store->touch = true; } - void EKTF2232Touchscreen::setup() { ESP_LOGCONFIG(TAG, "Setting up EKT2232 Touchscreen..."); this->interrupt_pin_->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); this->interrupt_pin_->setup(); - this->store_.pin = this->interrupt_pin_->to_isr(); - this->interrupt_pin_->attach_interrupt(EKTF2232TouchscreenStore::gpio_intr, &this->store_, - gpio::INTERRUPT_FALLING_EDGE); + this->attach_interrupt_(this->interrupt_pin_, gpio::INTERRUPT_FALLING_EDGE); this->rts_pin_->setup(); @@ -45,7 +41,7 @@ void EKTF2232Touchscreen::setup() { this->mark_failed(); return; } - this->x_resolution_ = ((received[2])) | ((received[3] & 0xf0) << 4); + this->x_raw_max_ = ((received[2])) | ((received[3] & 0xf0) << 4); this->write(GET_Y_RES, 4); if (this->read(received, 4)) { @@ -54,19 +50,14 @@ void EKTF2232Touchscreen::setup() { this->mark_failed(); return; } - this->y_resolution_ = ((received[2])) | ((received[3] & 0xf0) << 4); - this->store_.touch = false; + this->y_raw_max_ = ((received[2])) | ((received[3] & 0xf0) << 4); this->set_power_state(true); } -void EKTF2232Touchscreen::loop() { - if (!this->store_.touch) - return; - this->store_.touch = false; - +void EKTF2232Touchscreen::update_touches() { uint8_t touch_count = 0; - std::vector touches; + int16_t x_raw, y_raw; uint8_t raw[8]; this->read(raw, 8); @@ -75,45 +66,15 @@ void EKTF2232Touchscreen::loop() { touch_count++; } - if (touch_count == 0) { - for (auto *listener : this->touch_listeners_) - listener->release(); - return; - } - touch_count = std::min(touch_count, 2); ESP_LOGV(TAG, "Touch count: %d", touch_count); for (int i = 0; i < touch_count; i++) { uint8_t *d = raw + 1 + (i * 3); - uint32_t raw_x = (d[0] & 0xF0) << 4 | d[1]; - uint32_t raw_y = (d[0] & 0x0F) << 8 | d[2]; - - raw_x = raw_x * this->display_height_ - 1; - raw_y = raw_y * this->display_width_ - 1; - - TouchPoint tp; - switch (this->rotation_) { - case ROTATE_0_DEGREES: - tp.y = raw_x / this->x_resolution_; - tp.x = this->display_width_ - 1 - (raw_y / this->y_resolution_); - break; - case ROTATE_90_DEGREES: - tp.x = raw_x / this->x_resolution_; - tp.y = raw_y / this->y_resolution_; - break; - case ROTATE_180_DEGREES: - tp.y = this->display_height_ - 1 - (raw_x / this->x_resolution_); - tp.x = raw_y / this->y_resolution_; - break; - case ROTATE_270_DEGREES: - tp.x = this->display_height_ - 1 - (raw_x / this->x_resolution_); - tp.y = this->display_width_ - 1 - (raw_y / this->y_resolution_); - break; - } - - this->defer([this, tp]() { this->send_touch_(tp); }); + x_raw = (d[0] & 0xF0) << 4 | d[1]; + y_raw = (d[0] & 0x0F) << 8 | d[2]; + this->set_raw_touch_position_(i, x_raw, y_raw); } } @@ -126,7 +87,7 @@ void EKTF2232Touchscreen::set_power_state(bool enable) { bool EKTF2232Touchscreen::get_power_state() { uint8_t received[4]; this->write(GET_POWER_STATE_CMD, 4); - this->store_.touch = false; + this->store_.touched = false; this->read(received, 4); return (received[1] >> 3) & 1; } @@ -145,14 +106,14 @@ bool EKTF2232Touchscreen::soft_reset_() { uint8_t received[4]; uint16_t timeout = 1000; - while (!this->store_.touch && timeout > 0) { + while (!this->store_.touched && timeout > 0) { delay(1); timeout--; } if (timeout > 0) - this->store_.touch = true; + this->store_.touched = true; this->read(received, 4); - this->store_.touch = false; + this->store_.touched = false; return !memcmp(received, HELLO, 4); } diff --git a/esphome/components/ektf2232/ektf2232.h b/esphome/components/ektf2232/touchscreen/ektf2232.h similarity index 67% rename from esphome/components/ektf2232/ektf2232.h rename to esphome/components/ektf2232/touchscreen/ektf2232.h index e880b77f99..e9288d0a27 100644 --- a/esphome/components/ektf2232/ektf2232.h +++ b/esphome/components/ektf2232/touchscreen/ektf2232.h @@ -9,19 +9,11 @@ namespace esphome { namespace ektf2232 { -struct EKTF2232TouchscreenStore { - volatile bool touch; - ISRInternalGPIOPin pin; - - static void gpio_intr(EKTF2232TouchscreenStore *store); -}; - using namespace touchscreen; -class EKTF2232Touchscreen : public Touchscreen, public Component, public i2c::I2CDevice { +class EKTF2232Touchscreen : public Touchscreen, public i2c::I2CDevice { public: void setup() override; - void loop() override; void dump_config() override; void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; } @@ -33,12 +25,10 @@ class EKTF2232Touchscreen : public Touchscreen, public Component, public i2c::I2 protected: void hard_reset_(); bool soft_reset_(); + void update_touches() override; InternalGPIOPin *interrupt_pin_; GPIOPin *rts_pin_; - EKTF2232TouchscreenStore store_; - uint16_t x_resolution_; - uint16_t y_resolution_; }; } // namespace ektf2232 diff --git a/esphome/components/ft5x06/__init__.py b/esphome/components/ft5x06/__init__.py new file mode 100644 index 0000000000..dceea71dd0 --- /dev/null +++ b/esphome/components/ft5x06/__init__.py @@ -0,0 +1,6 @@ +import esphome.codegen as cg + +CODEOWNERS = ["@clydebarrow"] +DEPENDENCIES = ["i2c"] + +ft5x06_ns = cg.esphome_ns.namespace("ft5x06") diff --git a/esphome/components/ft5x06/touchscreen/__init__.py b/esphome/components/ft5x06/touchscreen/__init__.py new file mode 100644 index 0000000000..adeeac0d1a --- /dev/null +++ b/esphome/components/ft5x06/touchscreen/__init__.py @@ -0,0 +1,26 @@ +import esphome.codegen as cg +import esphome.config_validation as cv + +from esphome.components import i2c, touchscreen +from esphome.const import CONF_ID +from .. import ft5x06_ns + +FT5x06ButtonListener = ft5x06_ns.class_("FT5x06ButtonListener") +FT5x06Touchscreen = ft5x06_ns.class_( + "FT5x06Touchscreen", + touchscreen.Touchscreen, + cg.Component, + i2c.I2CDevice, +) + +CONFIG_SCHEMA = touchscreen.TOUCHSCREEN_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(FT5x06Touchscreen), + } +).extend(i2c.i2c_device_schema(0x48)) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await i2c.register_i2c_device(var, config) + await touchscreen.register_touchscreen(var, config) diff --git a/esphome/components/ft5x06/touchscreen/ft5x06_touchscreen.h b/esphome/components/ft5x06/touchscreen/ft5x06_touchscreen.h new file mode 100644 index 0000000000..497d6c906c --- /dev/null +++ b/esphome/components/ft5x06/touchscreen/ft5x06_touchscreen.h @@ -0,0 +1,124 @@ +#pragma once + +#include "esphome/components/i2c/i2c.h" +#include "esphome/components/touchscreen/touchscreen.h" +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace ft5x06 { + +static const char *const TAG = "ft5x06.touchscreen"; + +enum VendorId { + FT5X06_ID_UNKNOWN = 0, + FT5X06_ID_1 = 0x51, + FT5X06_ID_2 = 0x11, + FT5X06_ID_3 = 0xCD, +}; + +enum FTCmd : uint8_t { + FT5X06_MODE_REG = 0x00, + FT5X06_ORIGIN_REG = 0x08, + FT5X06_RESOLUTION_REG = 0x0C, + FT5X06_VENDOR_ID_REG = 0xA8, + FT5X06_TD_STATUS = 0x02, + FT5X06_TOUCH_DATA = 0x03, + FT5X06_I_MODE = 0xA4, + FT5X06_TOUCH_MAX = 0x4C, +}; + +enum FTMode : uint8_t { + FT5X06_OP_MODE = 0, + FT5X06_SYSINFO_MODE = 0x10, + FT5X06_TEST_MODE = 0x40, +}; + +static const size_t MAX_TOUCHES = 5; // max number of possible touches reported + +class FT5x06Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice { + public: + void setup() override { + esph_log_config(TAG, "Setting up FT5x06 Touchscreen..."); + // wait 200ms after reset. + this->set_timeout(200, [this] { this->continue_setup_(); }); + } + + void continue_setup_(void) { + uint8_t data[4]; + if (!this->set_mode_(FT5X06_OP_MODE)) + return; + + if (!this->err_check_(this->read_register(FT5X06_VENDOR_ID_REG, data, 1), "Read Vendor ID")) + return; + switch (data[0]) { + case FT5X06_ID_1: + case FT5X06_ID_2: + case FT5X06_ID_3: + this->vendor_id_ = (VendorId) data[0]; + esph_log_d(TAG, "Read vendor ID 0x%X", data[0]); + break; + + default: + esph_log_e(TAG, "Unknown vendor ID 0x%X", data[0]); + this->mark_failed(); + return; + } + // reading the chip registers to get max x/y does not seem to work. + this->x_raw_max_ = this->display_->get_width(); + this->y_raw_max_ = this->display_->get_height(); + esph_log_config(TAG, "FT5x06 Touchscreen setup complete"); + } + + void update_touches() override { + uint8_t touch_cnt; + uint8_t data[MAX_TOUCHES][6]; + + if (!this->read_byte(FT5X06_TD_STATUS, &touch_cnt) || touch_cnt > MAX_TOUCHES) { + esph_log_w(TAG, "Failed to read status"); + return; + } + if (touch_cnt == 0) + return; + + if (!this->read_bytes(FT5X06_TOUCH_DATA, (uint8_t *) data, touch_cnt * 6)) { + esph_log_w(TAG, "Failed to read touch data"); + return; + } + for (uint8_t i = 0; i != touch_cnt; i++) { + uint8_t status = data[i][0] >> 6; + uint8_t id = data[i][2] >> 3; + uint16_t x = encode_uint16(data[i][0] & 0x0F, data[i][1]); + uint16_t y = encode_uint16(data[i][2] & 0xF, data[i][3]); + + esph_log_d(TAG, "Read %X status, id: %d, pos %d/%d", status, id, x, y); + if (status == 0 || status == 2) { + this->set_raw_touch_position_(id, x, y); + } + } + } + + void dump_config() override { + esph_log_config(TAG, "FT5x06 Touchscreen:"); + esph_log_config(TAG, " Address: 0x%02X", this->address_); + esph_log_config(TAG, " Vendor ID: 0x%X", (int) this->vendor_id_); + } + + protected: + bool err_check_(i2c::ErrorCode err, const char *msg) { + if (err != i2c::ERROR_OK) { + this->mark_failed(); + esph_log_e(TAG, "%s failed - err 0x%X", msg, err); + return false; + } + return true; + } + bool set_mode_(FTMode mode) { + return this->err_check_(this->write_register(FT5X06_MODE_REG, (uint8_t *) &mode, 1), "Set mode"); + } + VendorId vendor_id_{FT5X06_ID_UNKNOWN}; +}; + +} // namespace ft5x06 +} // namespace esphome diff --git a/esphome/components/ft63x6/__init__.py b/esphome/components/ft63x6/__init__.py new file mode 100644 index 0000000000..b6d7d3580e --- /dev/null +++ b/esphome/components/ft63x6/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@gpambrozio"] diff --git a/esphome/components/ft63x6/ft63x6.cpp b/esphome/components/ft63x6/ft63x6.cpp new file mode 100644 index 0000000000..9198954253 --- /dev/null +++ b/esphome/components/ft63x6/ft63x6.cpp @@ -0,0 +1,99 @@ +/**************************************************************************/ +/*! + Author: Gustavo Ambrozio + Based on work by: Atsushi Sasaki (https://github.com/aselectroworks/Arduino-FT6336U) +*/ +/**************************************************************************/ + +#include "ft63x6.h" +#include "esphome/core/log.h" + +// Registers +// Reference: https://focuslcds.com/content/FT6236.pdf +namespace esphome { +namespace ft63x6 { + +static const uint8_t FT63X6_ADDR_TOUCH_COUNT = 0x02; + +static const uint8_t FT63X6_ADDR_TOUCH1_ID = 0x05; +static const uint8_t FT63X6_ADDR_TOUCH1_X = 0x03; +static const uint8_t FT63X6_ADDR_TOUCH1_Y = 0x05; + +static const uint8_t FT63X6_ADDR_TOUCH2_ID = 0x0B; +static const uint8_t FT63X6_ADDR_TOUCH2_X = 0x09; +static const uint8_t FT63X6_ADDR_TOUCH2_Y = 0x0B; + +static const char *const TAG = "FT63X6Touchscreen"; + +void FT63X6Touchscreen::setup() { + ESP_LOGCONFIG(TAG, "Setting up FT63X6Touchscreen Touchscreen..."); + if (this->interrupt_pin_ != nullptr) { + this->interrupt_pin_->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); + this->interrupt_pin_->setup(); + this->attach_interrupt_(this->interrupt_pin_, gpio::INTERRUPT_FALLING_EDGE); + } + + if (this->reset_pin_ != nullptr) { + this->reset_pin_->setup(); + } + + this->hard_reset_(); + + // Get touch resolution + this->x_raw_max_ = 320; + this->y_raw_max_ = 480; +} + +void FT63X6Touchscreen::update_touches() { + int touch_count = this->read_touch_count_(); + if (touch_count == 0) { + return; + } + + uint8_t touch_id = this->read_touch_id_(FT63X6_ADDR_TOUCH1_ID); // id1 = 0 or 1 + int16_t x = this->read_touch_coordinate_(FT63X6_ADDR_TOUCH1_X); + int16_t y = this->read_touch_coordinate_(FT63X6_ADDR_TOUCH1_Y); + this->set_raw_touch_position_(touch_id, x, y); + + if (touch_count >= 2) { + touch_id = this->read_touch_id_(FT63X6_ADDR_TOUCH2_ID); // id2 = 0 or 1(~id1 & 0x01) + x = this->read_touch_coordinate_(FT63X6_ADDR_TOUCH2_X); + y = this->read_touch_coordinate_(FT63X6_ADDR_TOUCH2_Y); + this->set_raw_touch_position_(touch_id, x, y); + } +} + +void FT63X6Touchscreen::hard_reset_() { + if (this->reset_pin_ != nullptr) { + this->reset_pin_->digital_write(false); + delay(10); + this->reset_pin_->digital_write(true); + } +} + +void FT63X6Touchscreen::dump_config() { + ESP_LOGCONFIG(TAG, "FT63X6 Touchscreen:"); + LOG_I2C_DEVICE(this); + LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_); + LOG_PIN(" Reset Pin: ", this->reset_pin_); +} + +uint8_t FT63X6Touchscreen::read_touch_count_() { return this->read_byte_(FT63X6_ADDR_TOUCH_COUNT); } + +// Touch functions +uint16_t FT63X6Touchscreen::read_touch_coordinate_(uint8_t coordinate) { + uint8_t read_buf[2]; + read_buf[0] = this->read_byte_(coordinate); + read_buf[1] = this->read_byte_(coordinate + 1); + return ((read_buf[0] & 0x0f) << 8) | read_buf[1]; +} +uint8_t FT63X6Touchscreen::read_touch_id_(uint8_t id_address) { return this->read_byte_(id_address) >> 4; } + +uint8_t FT63X6Touchscreen::read_byte_(uint8_t addr) { + uint8_t byte = 0; + this->read_byte(addr, &byte); + return byte; +} + +} // namespace ft63x6 +} // namespace esphome diff --git a/esphome/components/ft63x6/ft63x6.h b/esphome/components/ft63x6/ft63x6.h new file mode 100644 index 0000000000..79b1991041 --- /dev/null +++ b/esphome/components/ft63x6/ft63x6.h @@ -0,0 +1,41 @@ +/**************************************************************************/ +/*! + Author: Gustavo Ambrozio + Based on work by: Atsushi Sasaki (https://github.com/aselectroworks/Arduino-FT6336U) +*/ +/**************************************************************************/ + +#pragma once + +#include "esphome/components/i2c/i2c.h" +#include "esphome/components/touchscreen/touchscreen.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace ft63x6 { + +using namespace touchscreen; + +class FT63X6Touchscreen : public Touchscreen, public i2c::I2CDevice { + public: + void setup() override; + void dump_config() override; + + void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; } + void set_reset_pin(GPIOPin *pin) { this->reset_pin_ = pin; } + + protected: + void hard_reset_(); + uint8_t read_byte_(uint8_t addr); + void update_touches() override; + + InternalGPIOPin *interrupt_pin_{nullptr}; + GPIOPin *reset_pin_{nullptr}; + + uint8_t read_touch_count_(); + uint16_t read_touch_coordinate_(uint8_t coordinate); + uint8_t read_touch_id_(uint8_t id_address); +}; + +} // namespace ft63x6 +} // namespace esphome diff --git a/esphome/components/ft63x6/touchscreen.py b/esphome/components/ft63x6/touchscreen.py new file mode 100644 index 0000000000..d77d9ca287 --- /dev/null +++ b/esphome/components/ft63x6/touchscreen.py @@ -0,0 +1,44 @@ +import esphome.codegen as cg +import esphome.config_validation as cv + +from esphome import pins +from esphome.components import i2c, touchscreen +from esphome.const import CONF_ID, CONF_INTERRUPT_PIN, CONF_RESET_PIN + +CODEOWNERS = ["@gpambrozio"] +DEPENDENCIES = ["i2c"] + +ft6336u_ns = cg.esphome_ns.namespace("ft63x6") +FT63X6Touchscreen = ft6336u_ns.class_( + "FT63X6Touchscreen", + touchscreen.Touchscreen, + i2c.I2CDevice, +) + +CONF_FT63X6_ID = "ft63x6_id" + + +CONFIG_SCHEMA = touchscreen.TOUCHSCREEN_SCHEMA.extend( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(FT63X6Touchscreen), + cv.Optional(CONF_INTERRUPT_PIN): cv.All( + pins.internal_gpio_input_pin_schema + ), + cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, + } + ).extend(i2c.i2c_device_schema(0x38)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await touchscreen.register_touchscreen(var, config) + await i2c.register_i2c_device(var, config) + + if interrupt_pin_config := config.get(CONF_INTERRUPT_PIN): + interrupt_pin = await cg.gpio_pin_expression(interrupt_pin_config) + cg.add(var.set_interrupt_pin(interrupt_pin)) + if reset_pin_config := config.get(CONF_RESET_PIN): + reset_pin = await cg.gpio_pin_expression(reset_pin_config) + cg.add(var.set_reset_pin(reset_pin)) diff --git a/esphome/components/gt911/touchscreen/__init__.py b/esphome/components/gt911/touchscreen/__init__.py index 295e32b1b1..9a0d5cc169 100644 --- a/esphome/components/gt911/touchscreen/__init__.py +++ b/esphome/components/gt911/touchscreen/__init__.py @@ -3,7 +3,7 @@ import esphome.config_validation as cv from esphome import pins from esphome.components import i2c, touchscreen -from esphome.const import CONF_INTERRUPT_PIN, CONF_ID, CONF_ROTATION +from esphome.const import CONF_INTERRUPT_PIN, CONF_ID from .. import gt911_ns @@ -11,36 +11,21 @@ GT911ButtonListener = gt911_ns.class_("GT911ButtonListener") GT911Touchscreen = gt911_ns.class_( "GT911Touchscreen", touchscreen.Touchscreen, - cg.Component, i2c.I2CDevice, ) -ROTATIONS = { - 0: touchscreen.TouchRotation.ROTATE_0_DEGREES, - 90: touchscreen.TouchRotation.ROTATE_90_DEGREES, - 180: touchscreen.TouchRotation.ROTATE_180_DEGREES, - 270: touchscreen.TouchRotation.ROTATE_270_DEGREES, -} -CONFIG_SCHEMA = ( - touchscreen.TOUCHSCREEN_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(GT911Touchscreen), - cv.Optional(CONF_ROTATION): cv.enum(ROTATIONS), - cv.Required(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema, - } - ) - .extend(i2c.i2c_device_schema(0x5D)) - .extend(cv.COMPONENT_SCHEMA) -) +CONFIG_SCHEMA = touchscreen.TOUCHSCREEN_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(GT911Touchscreen), + cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema, + } +).extend(i2c.i2c_device_schema(0x5D)) async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - await cg.register_component(var, config) - await i2c.register_i2c_device(var, config) await touchscreen.register_touchscreen(var, config) + await i2c.register_i2c_device(var, config) - interrupt_pin = await cg.gpio_pin_expression(config[CONF_INTERRUPT_PIN]) - cg.add(var.set_interrupt_pin(interrupt_pin)) - if CONF_ROTATION in config: - cg.add(var.set_rotation(ROTATIONS[config[CONF_ROTATION]])) + if interrupt_pin := config.get(CONF_INTERRUPT_PIN): + cg.add(var.set_interrupt_pin(await cg.gpio_pin_expression(interrupt_pin))) diff --git a/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp b/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp index 4d3e7e7903..adc577f5da 100644 --- a/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp +++ b/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp @@ -12,6 +12,7 @@ static const uint8_t GET_TOUCH_STATE[2] = {0x81, 0x4E}; static const uint8_t CLEAR_TOUCH_STATE[3] = {0x81, 0x4E, 0x00}; static const uint8_t GET_TOUCHES[2] = {0x81, 0x4F}; static const uint8_t GET_SWITCHES[2] = {0x80, 0x4D}; +static const uint8_t GET_MAX_VALUES[2] = {0x80, 0x48}; static const size_t MAX_TOUCHES = 5; // max number of possible touches reported #define ERROR_CHECK(err) \ @@ -21,24 +22,35 @@ static const size_t MAX_TOUCHES = 5; // max number of possible touches reported return; \ } -void IRAM_ATTR HOT Store::gpio_intr(Store *store) { store->available = true; } - void GT911Touchscreen::setup() { i2c::ErrorCode err; ESP_LOGCONFIG(TAG, "Setting up GT911 Touchscreen..."); - // datasheet says NOT to use pullup/down on the int line. - this->interrupt_pin_->pin_mode(gpio::FLAG_INPUT); - this->interrupt_pin_->setup(); // check the configuration of the int line. - uint8_t data; + uint8_t data[4]; err = this->write(GET_SWITCHES, 2); if (err == i2c::ERROR_OK) { - err = this->read(&data, 1); + err = this->read(data, 1); if (err == i2c::ERROR_OK) { - ESP_LOGD(TAG, "Read from switches: 0x%02X", data); - this->interrupt_pin_->attach_interrupt(Store::gpio_intr, &this->store_, - (data & 1) ? gpio::INTERRUPT_FALLING_EDGE : gpio::INTERRUPT_RISING_EDGE); + ESP_LOGD(TAG, "Read from switches: 0x%02X", data[0]); + if (this->interrupt_pin_ != nullptr) { + // datasheet says NOT to use pullup/down on the int line. + this->interrupt_pin_->pin_mode(gpio::FLAG_INPUT); + this->interrupt_pin_->setup(); + this->attach_interrupt_(this->interrupt_pin_, + (data[0] & 1) ? gpio::INTERRUPT_FALLING_EDGE : gpio::INTERRUPT_RISING_EDGE); + } + } + } + if (err == i2c::ERROR_OK) { + err = this->write(GET_MAX_VALUES, 2); + if (err == i2c::ERROR_OK) { + err = this->read(data, sizeof(data)); + if (err == i2c::ERROR_OK) { + this->x_raw_max_ = encode_uint16(data[1], data[0]); + this->y_raw_max_ = encode_uint16(data[3], data[2]); + esph_log_d(TAG, "Read max_x/max_y %d/%d", this->x_raw_max_, this->y_raw_max_); + } } } if (err != i2c::ERROR_OK) { @@ -46,31 +58,28 @@ void GT911Touchscreen::setup() { this->mark_failed(); return; } + ESP_LOGCONFIG(TAG, "GT911 Touchscreen setup complete"); } -void GT911Touchscreen::loop() { +void GT911Touchscreen::update_touches() { i2c::ErrorCode err; - touchscreen::TouchPoint tp; uint8_t touch_state = 0; uint8_t data[MAX_TOUCHES + 1][8]; // 8 bytes each for each point, plus extra space for the key byte - if (!this->store_.available) - return; - this->store_.available = false; - err = this->write(GET_TOUCH_STATE, sizeof(GET_TOUCH_STATE), false); ERROR_CHECK(err); err = this->read(&touch_state, 1); ERROR_CHECK(err); this->write(CLEAR_TOUCH_STATE, sizeof(CLEAR_TOUCH_STATE)); - - if ((touch_state & 0x80) == 0) - return; uint8_t num_of_touches = touch_state & 0x07; + + if ((touch_state & 0x80) == 0 || num_of_touches > MAX_TOUCHES) { + this->skip_update_ = true; // skip send touch events, touchscreen is not ready yet. + return; + } + if (num_of_touches == 0) - this->send_release_(); - if (num_of_touches > MAX_TOUCHES) // should never happen return; err = this->write(GET_TOUCHES, sizeof(GET_TOUCHES), false); @@ -80,29 +89,10 @@ void GT911Touchscreen::loop() { ERROR_CHECK(err); for (uint8_t i = 0; i != num_of_touches; i++) { - tp.id = data[i][0]; + uint16_t id = data[i][0]; uint16_t x = encode_uint16(data[i][2], data[i][1]); uint16_t y = encode_uint16(data[i][4], data[i][3]); - - switch (this->rotation_) { - case touchscreen::ROTATE_0_DEGREES: - tp.x = x; - tp.y = y; - break; - case touchscreen::ROTATE_90_DEGREES: - tp.x = y; - tp.y = this->display_width_ - x; - break; - case touchscreen::ROTATE_180_DEGREES: - tp.x = this->display_width_ - x; - tp.y = this->display_height_ - y; - break; - case touchscreen::ROTATE_270_DEGREES: - tp.x = this->display_height_ - y; - tp.y = x; - break; - } - this->defer([this, tp]() { this->send_touch_(tp); }); + this->set_raw_touch_position_(id, x, y); } auto keys = data[num_of_touches][0]; for (size_t i = 0; i != 4; i++) { @@ -115,7 +105,6 @@ void GT911Touchscreen::dump_config() { ESP_LOGCONFIG(TAG, "GT911 Touchscreen:"); LOG_I2C_DEVICE(this); LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_); - ESP_LOGCONFIG(TAG, " Rotation: %d", (int) this->rotation_); } } // namespace gt911 diff --git a/esphome/components/gt911/touchscreen/gt911_touchscreen.h b/esphome/components/gt911/touchscreen/gt911_touchscreen.h index dc9248bb4a..44875de5f1 100644 --- a/esphome/components/gt911/touchscreen/gt911_touchscreen.h +++ b/esphome/components/gt911/touchscreen/gt911_touchscreen.h @@ -8,30 +8,23 @@ namespace esphome { namespace gt911 { -struct Store { - volatile bool available; - - static void gpio_intr(Store *store); -}; - class GT911ButtonListener { public: virtual void update_button(uint8_t index, bool state) = 0; }; -class GT911Touchscreen : public touchscreen::Touchscreen, public Component, public i2c::I2CDevice { +class GT911Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice { public: void setup() override; - void loop() override; void dump_config() override; - void set_rotation(touchscreen::TouchRotation rotation) { this->rotation_ = rotation; } void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; } void register_button_listener(GT911ButtonListener *listener) { this->button_listeners_.push_back(listener); } protected: - InternalGPIOPin *interrupt_pin_; - Store store_; + void update_touches() override; + + InternalGPIOPin *interrupt_pin_{}; std::vector button_listeners_; }; diff --git a/esphome/components/ili9xxx/display.py b/esphome/components/ili9xxx/display.py index f321b2ed63..cd68f1ae27 100644 --- a/esphome/components/ili9xxx/display.py +++ b/esphome/components/ili9xxx/display.py @@ -32,7 +32,11 @@ CODEOWNERS = ["@nielsnl68", "@clydebarrow"] ili9xxx_ns = cg.esphome_ns.namespace("ili9xxx") ILI9XXXDisplay = ili9xxx_ns.class_( - "ILI9XXXDisplay", cg.PollingComponent, spi.SPIDevice, display.DisplayBuffer + "ILI9XXXDisplay", + cg.PollingComponent, + spi.SPIDevice, + display.Display, + display.DisplayBuffer, ) ILI9XXXColorMode = ili9xxx_ns.enum("ILI9XXXColorMode") diff --git a/esphome/components/inkplate6/display.py b/esphome/components/inkplate6/display.py index a1ecfdd1d6..bcd9580448 100644 --- a/esphome/components/inkplate6/display.py +++ b/esphome/components/inkplate6/display.py @@ -39,7 +39,11 @@ CONF_VCOM_PIN = "vcom_pin" inkplate6_ns = cg.esphome_ns.namespace("inkplate6") Inkplate6 = inkplate6_ns.class_( - "Inkplate6", cg.PollingComponent, i2c.I2CDevice, display.DisplayBuffer + "Inkplate6", + cg.PollingComponent, + i2c.I2CDevice, + display.Display, + display.DisplayBuffer, ) InkplateModel = inkplate6_ns.enum("InkplateModel") diff --git a/esphome/components/lilygo_t5_47/touchscreen/__init__.py b/esphome/components/lilygo_t5_47/touchscreen/__init__.py index fe94120644..01b03c807f 100644 --- a/esphome/components/lilygo_t5_47/touchscreen/__init__.py +++ b/esphome/components/lilygo_t5_47/touchscreen/__init__.py @@ -13,7 +13,6 @@ DEPENDENCIES = ["i2c"] LilygoT547Touchscreen = lilygo_t5_47_ns.class_( "LilygoT547Touchscreen", touchscreen.Touchscreen, - cg.Component, i2c.I2CDevice, ) @@ -27,17 +26,14 @@ CONFIG_SCHEMA = touchscreen.TOUCHSCREEN_SCHEMA.extend( pins.internal_gpio_input_pin_schema ), } - ) - .extend(i2c.i2c_device_schema(0x5A)) - .extend(cv.COMPONENT_SCHEMA) + ).extend(i2c.i2c_device_schema(0x5A)) ) async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - await cg.register_component(var, config) - await i2c.register_i2c_device(var, config) await touchscreen.register_touchscreen(var, config) + await i2c.register_i2c_device(var, config) interrupt_pin = await cg.gpio_pin_expression(config[CONF_INTERRUPT_PIN]) cg.add(var.set_interrupt_pin(interrupt_pin)) diff --git a/esphome/components/lilygo_t5_47/touchscreen/lilygo_t5_47_touchscreen.cpp b/esphome/components/lilygo_t5_47/touchscreen/lilygo_t5_47_touchscreen.cpp index b89cf2a724..eb61b6f31e 100644 --- a/esphome/components/lilygo_t5_47/touchscreen/lilygo_t5_47_touchscreen.cpp +++ b/esphome/components/lilygo_t5_47/touchscreen/lilygo_t5_47_touchscreen.cpp @@ -23,15 +23,12 @@ static const uint8_t READ_TOUCH[1] = {0x07}; return; \ } -void Store::gpio_intr(Store *store) { store->touch = true; } - void LilygoT547Touchscreen::setup() { ESP_LOGCONFIG(TAG, "Setting up Lilygo T5 4.7 Touchscreen..."); this->interrupt_pin_->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); this->interrupt_pin_->setup(); - this->store_.pin = this->interrupt_pin_->to_isr(); - this->interrupt_pin_->attach_interrupt(Store::gpio_intr, &this->store_, gpio::INTERRUPT_FALLING_EDGE); + this->attach_interrupt_(this->interrupt_pin_, gpio::INTERRUPT_FALLING_EDGE); if (this->write(nullptr, 0) != i2c::ERROR_OK) { ESP_LOGE(TAG, "Failed to communicate!"); @@ -41,19 +38,14 @@ void LilygoT547Touchscreen::setup() { } this->write_register(POWER_REGISTER, WAKEUP_CMD, 1); + + this->x_raw_max_ = this->get_width_(); + this->y_raw_max_ = this->get_height_(); } -void LilygoT547Touchscreen::loop() { - if (!this->store_.touch) { - for (auto *listener : this->touch_listeners_) - listener->release(); - return; - } - this->store_.touch = false; - +void LilygoT547Touchscreen::update_touches() { uint8_t point = 0; uint8_t buffer[40] = {0}; - uint32_t sum_l = 0, sum_h = 0; i2c::ErrorCode err; err = this->write_register(TOUCH_REGISTER, READ_FLAGS, 1); @@ -69,102 +61,30 @@ void LilygoT547Touchscreen::loop() { point = buffer[5] & 0xF; - if (point == 0) { - for (auto *listener : this->touch_listeners_) - listener->release(); - return; - } else if (point == 1) { + if (point == 1) { err = this->write_register(TOUCH_REGISTER, READ_TOUCH, 1); ERROR_CHECK(err); err = this->read(&buffer[5], 2); ERROR_CHECK(err); - sum_l = buffer[5] << 8 | buffer[6]; } else if (point > 1) { err = this->write_register(TOUCH_REGISTER, READ_TOUCH, 1); ERROR_CHECK(err); err = this->read(&buffer[5], 5 * (point - 1) + 3); ERROR_CHECK(err); - - sum_l = buffer[5 * point + 1] << 8 | buffer[5 * point + 2]; } this->write_register(TOUCH_REGISTER, CLEAR_FLAGS, 2); - for (int i = 0; i < 5 * point; i++) - sum_h += buffer[i]; + if (point == 0) + point = 1; - if (sum_l != sum_h) - point = 0; - - if (point) { - uint8_t offset; - for (int i = 0; i < point; i++) { - if (i == 0) { - offset = 0; - } else { - offset = 4; - } - - TouchPoint tp; - - tp.id = (buffer[i * 5 + offset] >> 4) & 0x0F; - tp.state = buffer[i * 5 + offset] & 0x0F; - if (tp.state == 0x06) - tp.state = 0x07; - - uint16_t y = (uint16_t) ((buffer[i * 5 + 1 + offset] << 4) | ((buffer[i * 5 + 3 + offset] >> 4) & 0x0F)); - uint16_t x = (uint16_t) ((buffer[i * 5 + 2 + offset] << 4) | (buffer[i * 5 + 3 + offset] & 0x0F)); - - switch (this->rotation_) { - case ROTATE_0_DEGREES: - tp.y = this->display_height_ - y; - tp.x = x; - break; - case ROTATE_90_DEGREES: - tp.x = this->display_height_ - y; - tp.y = this->display_width_ - x; - break; - case ROTATE_180_DEGREES: - tp.y = y; - tp.x = this->display_width_ - x; - break; - case ROTATE_270_DEGREES: - tp.x = y; - tp.y = x; - break; - } - - this->defer([this, tp]() { this->send_touch_(tp); }); - } - } else { - TouchPoint tp; - tp.id = (buffer[0] >> 4) & 0x0F; - tp.state = 0x06; - - uint16_t y = (uint16_t) ((buffer[0 * 5 + 1] << 4) | ((buffer[0 * 5 + 3] >> 4) & 0x0F)); - uint16_t x = (uint16_t) ((buffer[0 * 5 + 2] << 4) | (buffer[0 * 5 + 3] & 0x0F)); - - switch (this->rotation_) { - case ROTATE_0_DEGREES: - tp.y = this->display_height_ - y; - tp.x = x; - break; - case ROTATE_90_DEGREES: - tp.x = this->display_height_ - y; - tp.y = this->display_width_ - x; - break; - case ROTATE_180_DEGREES: - tp.y = y; - tp.x = this->display_width_ - x; - break; - case ROTATE_270_DEGREES: - tp.x = y; - tp.y = x; - break; - } - - this->defer([this, tp]() { this->send_touch_(tp); }); + uint16_t id, x_raw, y_raw; + for (uint8_t i = 0; i < point; i++) { + id = (buffer[i * 5] >> 4) & 0x0F; + y_raw = (uint16_t) ((buffer[i * 5 + 1] << 4) | ((buffer[i * 5 + 3] >> 4) & 0x0F)); + x_raw = (uint16_t) ((buffer[i * 5 + 2] << 4) | (buffer[i * 5 + 3] & 0x0F)); + this->set_raw_touch_position_(id, x_raw, y_raw); } this->status_clear_warning(); diff --git a/esphome/components/lilygo_t5_47/touchscreen/lilygo_t5_47_touchscreen.h b/esphome/components/lilygo_t5_47/touchscreen/lilygo_t5_47_touchscreen.h index 3d00e0b117..6767bf0a71 100644 --- a/esphome/components/lilygo_t5_47/touchscreen/lilygo_t5_47_touchscreen.h +++ b/esphome/components/lilygo_t5_47/touchscreen/lilygo_t5_47_touchscreen.h @@ -6,29 +6,25 @@ #include "esphome/core/component.h" #include "esphome/core/hal.h" +#include + namespace esphome { namespace lilygo_t5_47 { -struct Store { - volatile bool touch; - ISRInternalGPIOPin pin; - - static void gpio_intr(Store *store); -}; - using namespace touchscreen; -class LilygoT547Touchscreen : public Touchscreen, public Component, public i2c::I2CDevice { +class LilygoT547Touchscreen : public Touchscreen, public i2c::I2CDevice { public: void setup() override; - void loop() override; + void dump_config() override; void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; } protected: + void update_touches() override; + InternalGPIOPin *interrupt_pin_; - Store store_; }; } // namespace lilygo_t5_47 diff --git a/esphome/components/touchscreen/__init__.py b/esphome/components/touchscreen/__init__.py index a4bdc8cafd..bc09c6364d 100644 --- a/esphome/components/touchscreen/__init__.py +++ b/esphome/components/touchscreen/__init__.py @@ -3,44 +3,84 @@ import esphome.codegen as cg from esphome.components import display from esphome import automation -from esphome.const import CONF_ON_TOUCH +from esphome.const import CONF_ON_TOUCH, CONF_ON_RELEASE from esphome.core import coroutine_with_priority -CODEOWNERS = ["@jesserockz"] +CODEOWNERS = ["@jesserockz", "@nielsnl68"] DEPENDENCIES = ["display"] IS_PLATFORM_COMPONENT = True touchscreen_ns = cg.esphome_ns.namespace("touchscreen") -Touchscreen = touchscreen_ns.class_("Touchscreen") +Touchscreen = touchscreen_ns.class_("Touchscreen", cg.PollingComponent) TouchRotation = touchscreen_ns.enum("TouchRotation") TouchPoint = touchscreen_ns.struct("TouchPoint") +TouchPoints_t = cg.std_vector.template(TouchPoint) +TouchPoints_t_const_ref = TouchPoints_t.operator("ref").operator("const") TouchListener = touchscreen_ns.class_("TouchListener") CONF_DISPLAY = "display" CONF_TOUCHSCREEN_ID = "touchscreen_id" +CONF_REPORT_INTERVAL = "report_interval" # not used yet: +CONF_ON_UPDATE = "on_update" + +CONF_MIRROR_X = "mirror_x" +CONF_MIRROR_Y = "mirror_y" +CONF_SWAP_XY = "swap_xy" +CONF_TRANSFORM = "transform" TOUCHSCREEN_SCHEMA = cv.Schema( { - cv.GenerateID(CONF_DISPLAY): cv.use_id(display.DisplayBuffer), + cv.GenerateID(CONF_DISPLAY): cv.use_id(display.Display), + cv.Optional(CONF_TRANSFORM): cv.Schema( + { + cv.Optional(CONF_SWAP_XY, default=False): cv.boolean, + cv.Optional(CONF_MIRROR_X, default=False): cv.boolean, + cv.Optional(CONF_MIRROR_Y, default=False): cv.boolean, + } + ), cv.Optional(CONF_ON_TOUCH): automation.validate_automation(single=True), + cv.Optional(CONF_ON_UPDATE): automation.validate_automation(single=True), + cv.Optional(CONF_ON_RELEASE): automation.validate_automation(single=True), } -) +).extend(cv.polling_component_schema("50ms")) async def register_touchscreen(var, config): + await cg.register_component(var, config) + disp = await cg.get_variable(config[CONF_DISPLAY]) cg.add(var.set_display(disp)) + if CONF_TRANSFORM in config: + transform = config[CONF_TRANSFORM] + cg.add(var.set_swap_xy(transform[CONF_SWAP_XY])) + cg.add(var.set_mirror_x(transform[CONF_MIRROR_X])) + cg.add(var.set_mirror_y(transform[CONF_MIRROR_Y])) + if CONF_ON_TOUCH in config: await automation.build_automation( var.get_touch_trigger(), - [(TouchPoint, "touch")], + [(TouchPoint, "touch"), (TouchPoints_t_const_ref, "touches")], config[CONF_ON_TOUCH], ) + if CONF_ON_UPDATE in config: + await automation.build_automation( + var.get_update_trigger(), + [(TouchPoints_t_const_ref, "touches")], + config[CONF_ON_UPDATE], + ) + + if CONF_ON_RELEASE in config: + await automation.build_automation( + var.get_release_trigger(), + [], + config[CONF_ON_RELEASE], + ) + @coroutine_with_priority(100.0) async def to_code(config): diff --git a/esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.cpp b/esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.cpp index 66df78b62a..6c26ae3626 100644 --- a/esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.cpp +++ b/esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.cpp @@ -14,11 +14,10 @@ void TouchscreenBinarySensor::touch(TouchPoint tp) { if (this->page_ != nullptr) { touched &= this->page_ == this->parent_->get_display()->get_active_page(); } - if (touched) { this->publish_state(true); } else { - release(); + this->release(); } } diff --git a/esphome/components/touchscreen/touchscreen.cpp b/esphome/components/touchscreen/touchscreen.cpp index 20828aad8b..140f46b6f6 100644 --- a/esphome/components/touchscreen/touchscreen.cpp +++ b/esphome/components/touchscreen/touchscreen.cpp @@ -7,27 +7,128 @@ namespace touchscreen { static const char *const TAG = "touchscreen"; -void Touchscreen::set_display(display::Display *display) { - this->display_ = display; - this->display_width_ = display->get_width(); - this->display_height_ = display->get_height(); - this->rotation_ = static_cast(display->get_rotation()); +void TouchscreenInterrupt::gpio_intr(TouchscreenInterrupt *store) { store->touched = true; } - if (this->rotation_ == ROTATE_90_DEGREES || this->rotation_ == ROTATE_270_DEGREES) { - std::swap(this->display_width_, this->display_height_); +void Touchscreen::attach_interrupt_(InternalGPIOPin *irq_pin, esphome::gpio::InterruptType type) { + irq_pin->attach_interrupt(TouchscreenInterrupt::gpio_intr, &this->store_, type); + this->store_.init = true; + this->store_.touched = false; +} + +void Touchscreen::update() { + if (!this->store_.init) { + this->store_.touched = true; + } else { + // no need to poll if we have interrupts. + this->stop_poller(); } } -void Touchscreen::send_release_() { - for (auto *listener : this->touch_listeners_) - listener->release(); +void Touchscreen::loop() { + if (this->store_.touched) { + this->first_touch_ = this->touches_.empty(); + this->need_update_ = false; + this->is_touched_ = false; + this->skip_update_ = false; + for (auto &tp : this->touches_) { + if (tp.second.state == STATE_PRESSED || tp.second.state == STATE_UPDATED) { + tp.second.state = tp.second.state | STATE_RELEASING; + } else { + tp.second.state = STATE_RELEASED; + } + tp.second.x_prev = tp.second.x; + tp.second.y_prev = tp.second.y; + } + this->update_touches(); + if (this->skip_update_) { + for (auto &tp : this->touches_) { + tp.second.state = tp.second.state & -STATE_RELEASING; + } + } else { + this->store_.touched = false; + this->defer([this]() { this->send_touches_(); }); + } + } } -void Touchscreen::send_touch_(TouchPoint tp) { - ESP_LOGV(TAG, "Touch (x=%d, y=%d)", tp.x, tp.y); - this->touch_trigger_.trigger(tp); - for (auto *listener : this->touch_listeners_) - listener->touch(tp); +void Touchscreen::set_raw_touch_position_(uint8_t id, int16_t x_raw, int16_t y_raw, int16_t z_raw) { + TouchPoint tp; + uint16_t x, y; + if (this->touches_.count(id) == 0) { + tp.state = STATE_PRESSED; + tp.id = id; + } else { + tp = this->touches_[id]; + tp.state = STATE_UPDATED; + } + tp.x_raw = x_raw; + tp.y_raw = y_raw; + tp.z_raw = z_raw; + + x = this->normalize_(x_raw, this->x_raw_min_, this->x_raw_max_, this->invert_x_); + y = this->normalize_(y_raw, this->y_raw_min_, this->y_raw_max_, this->invert_y_); + + if (this->swap_x_y_) { + std::swap(x, y); + } + + tp.x = (uint16_t) ((int) x * this->get_width_() / 0x1000); + tp.y = (uint16_t) ((int) y * this->get_height_() / 0x1000); + + if (tp.state == STATE_PRESSED) { + tp.x_org = tp.x; + tp.y_org = tp.y; + } + + this->touches_[id] = tp; + + this->is_touched_ = true; + if ((tp.x != tp.x_prev) || (tp.y != tp.y_prev)) { + this->need_update_ = true; + } +} + +void Touchscreen::send_touches_() { + if (!this->is_touched_) { + this->release_trigger_.trigger(); + for (auto *listener : this->touch_listeners_) + listener->release(); + this->touches_.clear(); + } else { + TouchPoints_t touches; + for (auto tp : this->touches_) { + touches.push_back(tp.second); + } + if (this->first_touch_) { + TouchPoint tp = this->touches_.begin()->second; + this->touch_trigger_.trigger(tp, touches); + for (auto *listener : this->touch_listeners_) { + listener->touch(tp); + } + } + if (this->need_update_) { + this->update_trigger_.trigger(touches); + for (auto *listener : this->touch_listeners_) { + listener->update(touches); + } + } + } +} + +int16_t Touchscreen::normalize_(int16_t val, int16_t min_val, int16_t max_val, bool inverted) { + int16_t ret; + + if (val <= min_val) { + ret = 0; + } else if (val >= max_val) { + ret = 0xfff; + } else { + ret = (int16_t) ((int) 0xfff * (val - min_val) / (max_val - min_val)); + } + + ret = (inverted) ? 0xfff - ret : ret; + + return ret; } } // namespace touchscreen diff --git a/esphome/components/touchscreen/touchscreen.h b/esphome/components/touchscreen/touchscreen.h index 6e07bcfea0..1fe304d967 100644 --- a/esphome/components/touchscreen/touchscreen.h +++ b/esphome/components/touchscreen/touchscreen.h @@ -1,54 +1,119 @@ #pragma once -#include "esphome/components/display/display_buffer.h" +#include "esphome/core/defines.h" +#include "esphome/components/display/display.h" + #include "esphome/core/automation.h" #include "esphome/core/hal.h" #include +#include namespace esphome { namespace touchscreen { +static const uint8_t STATE_RELEASED = 0x00; +static const uint8_t STATE_PRESSED = 0x01; +static const uint8_t STATE_UPDATED = 0x02; +static const uint8_t STATE_RELEASING = 0x04; + struct TouchPoint { - uint16_t x; - uint16_t y; uint8_t id; - uint8_t state; + int16_t x_raw{0}, y_raw{0}, z_raw{0}; + uint16_t x_prev{0}, y_prev{0}; + uint16_t x_org{0}, y_org{0}; + uint16_t x{0}, y{0}; + int8_t state{0}; +}; + +using TouchPoints_t = std::vector; + +struct TouchscreenInterrupt { + volatile bool touched{true}; + bool init{false}; + static void gpio_intr(TouchscreenInterrupt *store); }; class TouchListener { public: - virtual void touch(TouchPoint tp) = 0; + virtual void touch(TouchPoint tp) {} + virtual void update(const TouchPoints_t &tpoints) {} virtual void release() {} }; -enum TouchRotation { - ROTATE_0_DEGREES = 0, - ROTATE_90_DEGREES = 90, - ROTATE_180_DEGREES = 180, - ROTATE_270_DEGREES = 270, -}; - -class Touchscreen { +class Touchscreen : public PollingComponent { public: - void set_display(display::Display *display); + void set_display(display::Display *display) { this->display_ = display; } display::Display *get_display() const { return this->display_; } - Trigger *get_touch_trigger() { return &this->touch_trigger_; } + void set_mirror_x(bool invert_x) { this->invert_x_ = invert_x; } + void set_mirror_y(bool invert_y) { this->invert_y_ = invert_y; } + void set_swap_xy(bool swap) { this->swap_x_y_ = swap; } + + void set_calibration(int16_t x_min, int16_t x_max, int16_t y_min, int16_t y_max) { + this->x_raw_min_ = std::min(x_min, x_max); + this->x_raw_max_ = std::max(x_min, x_max); + this->y_raw_min_ = std::min(y_min, y_max); + this->y_raw_max_ = std::max(y_min, y_max); + if (x_min > x_max) + this->invert_x_ = true; + if (y_min > y_max) + this->invert_y_ = true; + } + + Trigger *get_touch_trigger() { return &this->touch_trigger_; } + Trigger *get_update_trigger() { return &this->update_trigger_; } + Trigger<> *get_release_trigger() { return &this->release_trigger_; } void register_listener(TouchListener *listener) { this->touch_listeners_.push_back(listener); } + virtual void update_touches() = 0; + + optional get_touch() { return this->touches_.begin()->second; } + + TouchPoints_t get_touches() { + TouchPoints_t touches; + for (auto i : this->touches_) { + touches.push_back(i.second); + } + return touches; + } + + void update() override; + void loop() override; + protected: /// Call this function to send touch points to the `on_touch` listener and the binary_sensors. - void send_touch_(TouchPoint tp); - void send_release_(); - uint16_t display_width_; - uint16_t display_height_; - display::Display *display_; - TouchRotation rotation_; - Trigger touch_trigger_; + void attach_interrupt_(InternalGPIOPin *irq_pin, esphome::gpio::InterruptType type); + + void set_raw_touch_position_(uint8_t id, int16_t x_raw, int16_t y_raw, int16_t z_raw = 0); + + void send_touches_(); + + int16_t normalize_(int16_t val, int16_t min_val, int16_t max_val, bool inverted = false); + + uint16_t get_width_() { return this->display_->get_width(); } + + uint16_t get_height_() { return this->display_->get_height(); } + + display::Display *display_{nullptr}; + + int16_t x_raw_min_{0}, x_raw_max_{0}, y_raw_min_{0}, y_raw_max_{0}; + bool invert_x_{false}, invert_y_{false}, swap_x_y_{false}; + + Trigger touch_trigger_; + Trigger update_trigger_; + Trigger<> release_trigger_; std::vector touch_listeners_; + + std::map touches_; + TouchscreenInterrupt store_; + + bool first_touch_{true}; + bool need_update_{false}; + bool is_touched_{false}; + bool skip_update_{false}; }; } // namespace touchscreen diff --git a/esphome/components/tt21100/touchscreen/__init__.py b/esphome/components/tt21100/touchscreen/__init__.py index d96d389e69..4458ad0974 100644 --- a/esphome/components/tt21100/touchscreen/__init__.py +++ b/esphome/components/tt21100/touchscreen/__init__.py @@ -12,7 +12,6 @@ DEPENDENCIES = ["i2c"] TT21100Touchscreen = tt21100_ns.class_( "TT21100Touchscreen", touchscreen.Touchscreen, - cg.Component, i2c.I2CDevice, ) TT21100ButtonListener = tt21100_ns.class_("TT21100ButtonListener") @@ -24,17 +23,14 @@ CONFIG_SCHEMA = touchscreen.TOUCHSCREEN_SCHEMA.extend( cv.Required(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema, cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, } - ) - .extend(i2c.i2c_device_schema(0x24)) - .extend(cv.COMPONENT_SCHEMA) + ).extend(i2c.i2c_device_schema(0x24)) ) async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - await cg.register_component(var, config) - await i2c.register_i2c_device(var, config) await touchscreen.register_touchscreen(var, config) + await i2c.register_i2c_device(var, config) interrupt_pin = await cg.gpio_pin_expression(config[CONF_INTERRUPT_PIN]) cg.add(var.set_interrupt_pin(interrupt_pin)) diff --git a/esphome/components/tt21100/touchscreen/tt21100.cpp b/esphome/components/tt21100/touchscreen/tt21100.cpp index 28a8c2d754..ff688fd0b0 100644 --- a/esphome/components/tt21100/touchscreen/tt21100.cpp +++ b/esphome/components/tt21100/touchscreen/tt21100.cpp @@ -44,8 +44,6 @@ struct TT21100TouchReport { TT21100TouchRecord touch_record[MAX_TOUCH_POINTS]; } __attribute__((packed)); -void TT21100TouchscreenStore::gpio_intr(TT21100TouchscreenStore *store) { store->touch = true; } - float TT21100Touchscreen::get_setup_priority() const { return setup_priority::HARDWARE - 1.0f; } void TT21100Touchscreen::setup() { @@ -54,9 +52,8 @@ void TT21100Touchscreen::setup() { // Register interrupt pin this->interrupt_pin_->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); this->interrupt_pin_->setup(); - this->store_.pin = this->interrupt_pin_->to_isr(); - this->interrupt_pin_->attach_interrupt(TT21100TouchscreenStore::gpio_intr, &this->store_, - gpio::INTERRUPT_FALLING_EDGE); + + this->attach_interrupt_(this->interrupt_pin_, gpio::INTERRUPT_FALLING_EDGE); // Perform reset if necessary if (this->reset_pin_ != nullptr) { @@ -65,19 +62,11 @@ void TT21100Touchscreen::setup() { } // Update display dimensions if they were updated during display setup - this->display_width_ = this->display_->get_width(); - this->display_height_ = this->display_->get_height(); - this->rotation_ = static_cast(this->display_->get_rotation()); - - // Trigger initial read to activate the interrupt - this->store_.touch = true; + this->x_raw_max_ = this->get_width_(); + this->y_raw_max_ = this->get_height_(); } -void TT21100Touchscreen::loop() { - if (!this->store_.touch) - return; - this->store_.touch = false; - +void TT21100Touchscreen::update_touches() { // Read report length uint16_t data_len; this->read((uint8_t *) &data_len, sizeof(data_len)); @@ -111,12 +100,6 @@ void TT21100Touchscreen::loop() { uint8_t touch_count = (data_len - (sizeof(*report) - sizeof(report->touch_record))) / sizeof(TT21100TouchRecord); - if (touch_count == 0) { - for (auto *listener : this->touch_listeners_) - listener->release(); - return; - } - for (int i = 0; i < touch_count; i++) { auto *touch = &report->touch_record[i]; @@ -126,30 +109,7 @@ void TT21100Touchscreen::loop() { i, touch->touch_type, touch->tip, touch->event_id, touch->touch_id, touch->x, touch->y, touch->pressure, touch->major_axis_length, touch->orientation); - TouchPoint tp; - switch (this->rotation_) { - case ROTATE_0_DEGREES: - // Origin is top right, so mirror X by default - tp.x = this->display_width_ - touch->x; - tp.y = touch->y; - break; - case ROTATE_90_DEGREES: - tp.x = touch->y; - tp.y = touch->x; - break; - case ROTATE_180_DEGREES: - tp.x = touch->x; - tp.y = this->display_height_ - touch->y; - break; - case ROTATE_270_DEGREES: - tp.x = this->display_height_ - touch->y; - tp.y = this->display_width_ - touch->x; - break; - } - tp.id = touch->tip; - tp.state = touch->pressure; - - this->defer([this, tp]() { this->send_touch_(tp); }); + this->set_raw_touch_position_(touch->tip, touch->x, touch->y, touch->pressure); } } } diff --git a/esphome/components/tt21100/touchscreen/tt21100.h b/esphome/components/tt21100/touchscreen/tt21100.h index 306360975f..5d1b2efe3c 100644 --- a/esphome/components/tt21100/touchscreen/tt21100.h +++ b/esphome/components/tt21100/touchscreen/tt21100.h @@ -5,27 +5,21 @@ #include "esphome/core/component.h" #include "esphome/core/hal.h" +#include + namespace esphome { namespace tt21100 { using namespace touchscreen; -struct TT21100TouchscreenStore { - volatile bool touch; - ISRInternalGPIOPin pin; - - static void gpio_intr(TT21100TouchscreenStore *store); -}; - class TT21100ButtonListener { public: virtual void update_button(uint8_t index, uint16_t state) = 0; }; -class TT21100Touchscreen : public Touchscreen, public Component, public i2c::I2CDevice { +class TT21100Touchscreen : public Touchscreen, public i2c::I2CDevice { public: void setup() override; - void loop() override; void dump_config() override; float get_setup_priority() const override; @@ -37,7 +31,7 @@ class TT21100Touchscreen : public Touchscreen, public Component, public i2c::I2C protected: void reset_(); - TT21100TouchscreenStore store_; + void update_touches() override; InternalGPIOPin *interrupt_pin_; GPIOPin *reset_pin_{nullptr}; diff --git a/esphome/components/xpt2046/binary_sensor.py b/esphome/components/xpt2046/binary_sensor.py deleted file mode 100644 index 5a6cfe4919..0000000000 --- a/esphome/components/xpt2046/binary_sensor.py +++ /dev/null @@ -1,3 +0,0 @@ -import esphome.config_validation as cv - -CONFIG_SCHEMA = cv.invalid("Rename this platform component to Touchscreen.") diff --git a/esphome/components/xpt2046/touchscreen.py b/esphome/components/xpt2046/touchscreen.py deleted file mode 100644 index 150d1cf396..0000000000 --- a/esphome/components/xpt2046/touchscreen.py +++ /dev/null @@ -1,116 +0,0 @@ -import esphome.codegen as cg -import esphome.config_validation as cv - -from esphome import pins -from esphome.components import spi, touchscreen -from esphome.const import CONF_ID, CONF_INTERRUPT_PIN, CONF_IRQ_PIN, CONF_THRESHOLD - -CODEOWNERS = ["@numo68", "@nielsnl68"] -DEPENDENCIES = ["spi"] - -XPT2046_ns = cg.esphome_ns.namespace("xpt2046") -XPT2046Component = XPT2046_ns.class_( - "XPT2046Component", - touchscreen.Touchscreen, - cg.PollingComponent, - spi.SPIDevice, -) - -CONF_REPORT_INTERVAL = "report_interval" -CONF_CALIBRATION_X_MIN = "calibration_x_min" -CONF_CALIBRATION_X_MAX = "calibration_x_max" -CONF_CALIBRATION_Y_MIN = "calibration_y_min" -CONF_CALIBRATION_Y_MAX = "calibration_y_max" -CONF_SWAP_X_Y = "swap_x_y" - -# obsolete Keys -CONF_DIMENSION_X = "dimension_x" -CONF_DIMENSION_Y = "dimension_y" - - -def validate_xpt2046(config): - if ( - abs( - cv.int_(config[CONF_CALIBRATION_X_MAX]) - - cv.int_(config[CONF_CALIBRATION_X_MIN]) - ) - < 1000 - ): - raise cv.Invalid("Calibration X values difference < 1000") - - if ( - abs( - cv.int_(config[CONF_CALIBRATION_Y_MAX]) - - cv.int_(config[CONF_CALIBRATION_Y_MIN]) - ) - < 1000 - ): - raise cv.Invalid("Calibration Y values difference < 1000") - - return config - - -def report_interval(value): - if value == "never": - return 4294967295 # uint32_t max - return cv.positive_time_period_milliseconds(value) - - -CONFIG_SCHEMA = touchscreen.TOUCHSCREEN_SCHEMA.extend( - cv.Schema( - { - cv.GenerateID(): cv.declare_id(XPT2046Component), - cv.Optional(CONF_INTERRUPT_PIN): cv.All( - pins.internal_gpio_input_pin_schema - ), - cv.Optional(CONF_CALIBRATION_X_MIN, default=0): cv.int_range( - min=0, max=4095 - ), - cv.Optional(CONF_CALIBRATION_X_MAX, default=4095): cv.int_range( - min=0, max=4095 - ), - cv.Optional(CONF_CALIBRATION_Y_MIN, default=0): cv.int_range( - min=0, max=4095 - ), - cv.Optional(CONF_CALIBRATION_Y_MAX, default=4095): cv.int_range( - min=0, max=4095 - ), - cv.Optional(CONF_THRESHOLD, default=400): cv.int_range(min=0, max=4095), - cv.Optional(CONF_REPORT_INTERVAL, default="never"): report_interval, - cv.Optional(CONF_SWAP_X_Y, default=False): cv.boolean, - # obsolete Keys - cv.Optional(CONF_IRQ_PIN): cv.invalid("Rename IRQ_PIN to INTERUPT_PIN"), - cv.Optional(CONF_DIMENSION_X): cv.invalid( - "This key is now obsolete, please remove it" - ), - cv.Optional(CONF_DIMENSION_Y): cv.invalid( - "This key is now obsolete, please remove it" - ), - }, - ) - .extend(cv.polling_component_schema("50ms")) - .extend(spi.spi_device_schema()), -).add_extra(validate_xpt2046) - - -async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - await cg.register_component(var, config) - await spi.register_spi_device(var, config) - await touchscreen.register_touchscreen(var, config) - - cg.add(var.set_threshold(config[CONF_THRESHOLD])) - cg.add(var.set_report_interval(config[CONF_REPORT_INTERVAL])) - cg.add(var.set_swap_x_y(config[CONF_SWAP_X_Y])) - cg.add( - var.set_calibration( - config[CONF_CALIBRATION_X_MIN], - config[CONF_CALIBRATION_X_MAX], - config[CONF_CALIBRATION_Y_MIN], - config[CONF_CALIBRATION_Y_MAX], - ) - ) - - if CONF_INTERRUPT_PIN in config: - pin = await cg.gpio_pin_expression(config[CONF_INTERRUPT_PIN]) - cg.add(var.set_irq_pin(pin)) diff --git a/esphome/components/xpt2046/touchscreen/__init__.py b/esphome/components/xpt2046/touchscreen/__init__.py new file mode 100644 index 0000000000..9f08f38c3f --- /dev/null +++ b/esphome/components/xpt2046/touchscreen/__init__.py @@ -0,0 +1,93 @@ +import esphome.codegen as cg +import esphome.config_validation as cv + +from esphome import pins +from esphome.components import spi, touchscreen +from esphome.const import CONF_ID, CONF_THRESHOLD, CONF_INTERRUPT_PIN + +CODEOWNERS = ["@numo68", "@nielsnl68"] +DEPENDENCIES = ["spi"] + +XPT2046_ns = cg.esphome_ns.namespace("xpt2046") +XPT2046Component = XPT2046_ns.class_( + "XPT2046Component", + touchscreen.Touchscreen, + spi.SPIDevice, +) + + +CONF_CALIBRATION_X_MIN = "calibration_x_min" +CONF_CALIBRATION_X_MAX = "calibration_x_max" +CONF_CALIBRATION_Y_MIN = "calibration_y_min" +CONF_CALIBRATION_Y_MAX = "calibration_y_max" + + +def validate_xpt2046(config): + if ( + abs( + cv.int_(config[CONF_CALIBRATION_X_MAX]) + - cv.int_(config[CONF_CALIBRATION_X_MIN]) + ) + < 1000 + ): + raise cv.Invalid("Calibration X values difference < 1000") + + if ( + abs( + cv.int_(config[CONF_CALIBRATION_Y_MAX]) + - cv.int_(config[CONF_CALIBRATION_Y_MIN]) + ) + < 1000 + ): + raise cv.Invalid("Calibration Y values difference < 1000") + + return config + + +CONFIG_SCHEMA = cv.All( + touchscreen.TOUCHSCREEN_SCHEMA.extend( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(XPT2046Component), + cv.Optional(CONF_INTERRUPT_PIN): cv.All( + pins.internal_gpio_input_pin_schema + ), + cv.Optional(CONF_CALIBRATION_X_MIN, default=0): cv.int_range( + min=0, max=4095 + ), + cv.Optional(CONF_CALIBRATION_X_MAX, default=4095): cv.int_range( + min=0, max=4095 + ), + cv.Optional(CONF_CALIBRATION_Y_MIN, default=0): cv.int_range( + min=0, max=4095 + ), + cv.Optional(CONF_CALIBRATION_Y_MAX, default=4095): cv.int_range( + min=0, max=4095 + ), + cv.Optional(CONF_THRESHOLD, default=400): cv.int_range(min=0, max=4095), + }, + ) + ).extend(spi.spi_device_schema()), + validate_xpt2046, +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await touchscreen.register_touchscreen(var, config) + await spi.register_spi_device(var, config) + + cg.add(var.set_threshold(config[CONF_THRESHOLD])) + + cg.add( + var.set_calibration( + config[CONF_CALIBRATION_X_MIN], + config[CONF_CALIBRATION_X_MAX], + config[CONF_CALIBRATION_Y_MIN], + config[CONF_CALIBRATION_Y_MAX], + ) + ) + + if CONF_INTERRUPT_PIN in config: + pin = await cg.gpio_pin_expression(config[CONF_INTERRUPT_PIN]) + cg.add(var.set_irq_pin(pin)) diff --git a/esphome/components/xpt2046/touchscreen/xpt2046.cpp b/esphome/components/xpt2046/touchscreen/xpt2046.cpp new file mode 100644 index 0000000000..1a9c202af0 --- /dev/null +++ b/esphome/components/xpt2046/touchscreen/xpt2046.cpp @@ -0,0 +1,113 @@ +#include "xpt2046.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" + +#include + +namespace esphome { +namespace xpt2046 { + +static const char *const TAG = "xpt2046"; + +void XPT2046Component::setup() { + if (this->irq_pin_ != nullptr) { + // The pin reports a touch with a falling edge. Unfortunately the pin goes also changes state + // while the channels are read and wiring it as an interrupt is not straightforward and would + // need careful masking. A GPIO poll is cheap so we'll just use that. + + this->irq_pin_->setup(); // INPUT + this->irq_pin_->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); + this->irq_pin_->setup(); + this->attach_interrupt_(this->irq_pin_, gpio::INTERRUPT_FALLING_EDGE); + } + this->spi_setup(); + this->read_adc_(0xD0); // ADC powerdown, enable PENIRQ pin +} + +void XPT2046Component::update_touches() { + int16_t data[6], x_raw, y_raw, z_raw; + bool touch = false; + + enable(); + + int16_t touch_pressure_1 = this->read_adc_(0xB1 /* touch_pressure_1 */); + int16_t touch_pressure_2 = this->read_adc_(0xC1 /* touch_pressure_2 */); + ESP_LOGVV(TAG, "touch_pressure %d, %d", touch_pressure_1, touch_pressure_2); + z_raw = touch_pressure_1 + 0Xfff - touch_pressure_2; + + touch = (z_raw >= this->threshold_); + if (touch) { + read_adc_(0xD1 /* X */); // dummy Y measure, 1st is always noisy + data[0] = this->read_adc_(0x91 /* Y */); + data[1] = this->read_adc_(0xD1 /* X */); // make 3 x-y measurements + data[2] = this->read_adc_(0x91 /* Y */); + data[3] = this->read_adc_(0xD1 /* X */); + data[4] = this->read_adc_(0x91 /* Y */); + } + + data[5] = this->read_adc_(0xD0 /* X */); // Last X touch power down + + disable(); + + if (touch) { + x_raw = best_two_avg(data[1], data[3], data[5]); + y_raw = best_two_avg(data[0], data[2], data[4]); + + ESP_LOGV(TAG, "Touchscreen Update [%d, %d], z = %d", x_raw, y_raw, z_raw); + + this->set_raw_touch_position_(0, x_raw, y_raw, z_raw); + } +} + +void XPT2046Component::dump_config() { + ESP_LOGCONFIG(TAG, "XPT2046:"); + + LOG_PIN(" IRQ Pin: ", this->irq_pin_); + ESP_LOGCONFIG(TAG, " X min: %d", this->x_raw_min_); + ESP_LOGCONFIG(TAG, " X max: %d", this->x_raw_max_); + ESP_LOGCONFIG(TAG, " Y min: %d", this->y_raw_min_); + ESP_LOGCONFIG(TAG, " Y max: %d", this->y_raw_max_); + + ESP_LOGCONFIG(TAG, " Swap X/Y: %s", YESNO(this->swap_x_y_)); + ESP_LOGCONFIG(TAG, " Invert X: %s", YESNO(this->invert_x_)); + ESP_LOGCONFIG(TAG, " Invert Y: %s", YESNO(this->invert_y_)); + + ESP_LOGCONFIG(TAG, " threshold: %d", this->threshold_); + + LOG_UPDATE_INTERVAL(this); +} + +float XPT2046Component::get_setup_priority() const { return setup_priority::DATA; } + +int16_t XPT2046Component::best_two_avg(int16_t value1, int16_t value2, int16_t value3) { + int16_t delta_a, delta_b, delta_c; + int16_t reta = 0; + + delta_a = (value1 > value2) ? value1 - value2 : value2 - value1; + delta_b = (value1 > value3) ? value1 - value3 : value3 - value1; + delta_c = (value3 > value2) ? value3 - value2 : value2 - value3; + + if (delta_a <= delta_b && delta_a <= delta_c) { + reta = (value1 + value2) >> 1; + } else if (delta_b <= delta_a && delta_b <= delta_c) { + reta = (value1 + value3) >> 1; + } else { + reta = (value2 + value3) >> 1; + } + + return reta; +} + +int16_t XPT2046Component::read_adc_(uint8_t ctrl) { // NOLINT + uint8_t data[2]; + + this->write_byte(ctrl); + delay(1); + data[0] = this->read_byte(); + data[1] = this->read_byte(); + + return ((data[0] << 8) | data[1]) >> 3; +} + +} // namespace xpt2046 +} // namespace esphome diff --git a/esphome/components/xpt2046/touchscreen/xpt2046.h b/esphome/components/xpt2046/touchscreen/xpt2046.h new file mode 100644 index 0000000000..ff866bc86b --- /dev/null +++ b/esphome/components/xpt2046/touchscreen/xpt2046.h @@ -0,0 +1,41 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "esphome/components/spi/spi.h" +#include "esphome/components/touchscreen/touchscreen.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace xpt2046 { + +using namespace touchscreen; + +class XPT2046Component : public Touchscreen, + public spi::SPIDevice { + public: + /// Set the threshold for the touch detection. + void set_threshold(int16_t threshold) { this->threshold_ = threshold; } + /// Set the pin used to detect the touch. + void set_irq_pin(InternalGPIOPin *pin) { this->irq_pin_ = pin; } + + void setup() override; + void dump_config() override; + float get_setup_priority() const override; + + protected: + static int16_t best_two_avg(int16_t value1, int16_t value2, int16_t value3); + + int16_t read_adc_(uint8_t ctrl); + + void update_touches() override; + + int16_t threshold_; + + InternalGPIOPin *irq_pin_{nullptr}; +}; + +} // namespace xpt2046 +} // namespace esphome diff --git a/esphome/components/xpt2046/xpt2046.cpp b/esphome/components/xpt2046/xpt2046.cpp deleted file mode 100644 index 078a1b01e9..0000000000 --- a/esphome/components/xpt2046/xpt2046.cpp +++ /dev/null @@ -1,207 +0,0 @@ -#include "xpt2046.h" -#include "esphome/core/log.h" -#include "esphome/core/helpers.h" - -#include -#include - -namespace esphome { -namespace xpt2046 { - -static const char *const TAG = "xpt2046"; - -void XPT2046TouchscreenStore::gpio_intr(XPT2046TouchscreenStore *store) { store->touch = true; } - -void XPT2046Component::setup() { - if (this->irq_pin_ != nullptr) { - // The pin reports a touch with a falling edge. Unfortunately the pin goes also changes state - // while the channels are read and wiring it as an interrupt is not straightforward and would - // need careful masking. A GPIO poll is cheap so we'll just use that. - - this->irq_pin_->setup(); // INPUT - this->irq_pin_->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); - this->irq_pin_->setup(); - this->irq_pin_->attach_interrupt(XPT2046TouchscreenStore::gpio_intr, &this->store_, gpio::INTERRUPT_FALLING_EDGE); - } - spi_setup(); - read_adc_(0xD0); // ADC powerdown, enable PENIRQ pin -} - -void XPT2046Component::loop() { - if ((this->irq_pin_ != nullptr) && (this->store_.touch || this->touched)) { - this->store_.touch = false; - check_touch_(); - } -} - -void XPT2046Component::update() { - if (this->irq_pin_ == nullptr) - check_touch_(); -} - -void XPT2046Component::check_touch_() { - int16_t data[6]; - bool touch = false; - uint32_t now = millis(); - - enable(); - - int16_t touch_pressure_1 = read_adc_(0xB1 /* touch_pressure_1 */); - int16_t touch_pressure_2 = read_adc_(0xC1 /* touch_pressure_2 */); - - this->z_raw = touch_pressure_1 + 0Xfff - touch_pressure_2; - - touch = (this->z_raw >= this->threshold_); - if (touch) { - read_adc_(0xD1 /* X */); // dummy Y measure, 1st is always noisy - data[0] = read_adc_(0x91 /* Y */); - data[1] = read_adc_(0xD1 /* X */); // make 3 x-y measurements - data[2] = read_adc_(0x91 /* Y */); - data[3] = read_adc_(0xD1 /* X */); - data[4] = read_adc_(0x91 /* Y */); - } - - data[5] = read_adc_(0xD0 /* X */); // Last X touch power down - - disable(); - - if (touch) { - this->x_raw = best_two_avg(data[1], data[3], data[5]); - this->y_raw = best_two_avg(data[0], data[2], data[4]); - - ESP_LOGVV(TAG, "Update [x, y] = [%d, %d], z = %d", this->x_raw, this->y_raw, this->z_raw); - - TouchPoint touchpoint; - - touchpoint.x = normalize(this->x_raw, this->x_raw_min_, this->x_raw_max_); - touchpoint.y = normalize(this->y_raw, this->y_raw_min_, this->y_raw_max_); - - if (this->swap_x_y_) { - std::swap(touchpoint.x, touchpoint.y); - } - - if (this->invert_x_) { - touchpoint.x = 0xfff - touchpoint.x; - } - - if (this->invert_y_) { - touchpoint.y = 0xfff - touchpoint.y; - } - - switch (static_cast(this->display_->get_rotation())) { - case ROTATE_0_DEGREES: - break; - case ROTATE_90_DEGREES: - std::swap(touchpoint.x, touchpoint.y); - touchpoint.y = 0xfff - touchpoint.y; - break; - case ROTATE_180_DEGREES: - touchpoint.x = 0xfff - touchpoint.x; - touchpoint.y = 0xfff - touchpoint.y; - break; - case ROTATE_270_DEGREES: - std::swap(touchpoint.x, touchpoint.y); - touchpoint.x = 0xfff - touchpoint.x; - break; - } - - touchpoint.x = (int16_t) ((int) touchpoint.x * this->display_->get_width() / 0xfff); - touchpoint.y = (int16_t) ((int) touchpoint.y * this->display_->get_height() / 0xfff); - - if (!this->touched || (now - this->last_pos_ms_) >= this->report_millis_) { - ESP_LOGV(TAG, "Touching at [%03X, %03X] => [%3d, %3d]", this->x_raw, this->y_raw, touchpoint.x, touchpoint.y); - - this->defer([this, touchpoint]() { this->send_touch_(touchpoint); }); - - this->x = touchpoint.x; - this->y = touchpoint.y; - this->touched = true; - this->last_pos_ms_ = now; - } - } - - if (!touch && this->touched) { - this->x_raw = this->y_raw = this->z_raw = 0; - ESP_LOGV(TAG, "Released [%d, %d]", this->x, this->y); - this->touched = false; - for (auto *listener : this->touch_listeners_) - listener->release(); - } -} - -void XPT2046Component::set_calibration(int16_t x_min, int16_t x_max, int16_t y_min, int16_t y_max) { // NOLINT - this->x_raw_min_ = std::min(x_min, x_max); - this->x_raw_max_ = std::max(x_min, x_max); - this->y_raw_min_ = std::min(y_min, y_max); - this->y_raw_max_ = std::max(y_min, y_max); - this->invert_x_ = (x_min > x_max); - this->invert_y_ = (y_min > y_max); -} - -void XPT2046Component::dump_config() { - ESP_LOGCONFIG(TAG, "XPT2046:"); - - LOG_PIN(" IRQ Pin: ", this->irq_pin_); - ESP_LOGCONFIG(TAG, " X min: %d", this->x_raw_min_); - ESP_LOGCONFIG(TAG, " X max: %d", this->x_raw_max_); - ESP_LOGCONFIG(TAG, " Y min: %d", this->y_raw_min_); - ESP_LOGCONFIG(TAG, " Y max: %d", this->y_raw_max_); - - ESP_LOGCONFIG(TAG, " Swap X/Y: %s", YESNO(this->swap_x_y_)); - ESP_LOGCONFIG(TAG, " Invert X: %s", YESNO(this->invert_x_)); - ESP_LOGCONFIG(TAG, " Invert Y: %s", YESNO(this->invert_y_)); - - ESP_LOGCONFIG(TAG, " threshold: %d", this->threshold_); - ESP_LOGCONFIG(TAG, " Report interval: %" PRIu32, this->report_millis_); - - LOG_UPDATE_INTERVAL(this); -} - -float XPT2046Component::get_setup_priority() const { return setup_priority::DATA; } - -int16_t XPT2046Component::best_two_avg(int16_t x, int16_t y, int16_t z) { // NOLINT - int16_t da, db, dc; // NOLINT - int16_t reta = 0; - - da = (x > y) ? x - y : y - x; - db = (x > z) ? x - z : z - x; - dc = (z > y) ? z - y : y - z; - - if (da <= db && da <= dc) { - reta = (x + y) >> 1; - } else if (db <= da && db <= dc) { - reta = (x + z) >> 1; - } else { - reta = (y + z) >> 1; - } - - return reta; -} - -int16_t XPT2046Component::normalize(int16_t val, int16_t min_val, int16_t max_val) { - int16_t ret; - - if (val <= min_val) { - ret = 0; - } else if (val >= max_val) { - ret = 0xfff; - } else { - ret = (int16_t) ((int) 0xfff * (val - min_val) / (max_val - min_val)); - } - - return ret; -} - -int16_t XPT2046Component::read_adc_(uint8_t ctrl) { // NOLINT - uint8_t data[2]; - - write_byte(ctrl); - delay(1); - data[0] = read_byte(); - data[1] = read_byte(); - - return ((data[0] << 8) | data[1]) >> 3; -} - -} // namespace xpt2046 -} // namespace esphome diff --git a/esphome/components/xpt2046/xpt2046.h b/esphome/components/xpt2046/xpt2046.h deleted file mode 100644 index e7d9caba21..0000000000 --- a/esphome/components/xpt2046/xpt2046.h +++ /dev/null @@ -1,107 +0,0 @@ -#pragma once - -#include "esphome/core/component.h" -#include "esphome/core/automation.h" -#include "esphome/components/spi/spi.h" -#include "esphome/components/touchscreen/touchscreen.h" -#include "esphome/core/helpers.h" -#include "esphome/core/log.h" - -namespace esphome { -namespace xpt2046 { - -using namespace touchscreen; - -struct XPT2046TouchscreenStore { - volatile bool touch; - static void gpio_intr(XPT2046TouchscreenStore *store); -}; - -class XPT2046Component : public Touchscreen, - public PollingComponent, - public spi::SPIDevice { - public: - /// Set the logical touch screen dimensions. - void set_dimensions(int16_t x, int16_t y) { - this->display_width_ = x; - this->display_height_ = y; - } - /// Set the coordinates for the touch screen edges. - void set_calibration(int16_t x_min, int16_t x_max, int16_t y_min, int16_t y_max); - /// If true the x and y axes will be swapped - void set_swap_x_y(bool val) { this->swap_x_y_ = val; } - - /// Set the interval to report the touch point perodically. - void set_report_interval(uint32_t interval) { this->report_millis_ = interval; } - uint32_t get_report_interval() { return this->report_millis_; } - - /// Set the threshold for the touch detection. - void set_threshold(int16_t threshold) { this->threshold_ = threshold; } - /// Set the pin used to detect the touch. - void set_irq_pin(InternalGPIOPin *pin) { this->irq_pin_ = pin; } - - void setup() override; - void dump_config() override; - float get_setup_priority() const override; - - /** Detect the touch if the irq pin is specified. - * - * If the touch is detected and the component does not already know about it - * the update() is called immediately. If the irq pin is not specified - * the loop() is a no-op. - */ - void loop() override; - - /** Read and process the values from the hardware. - * - * Read the raw x, y and touch pressure values from the chip, detect the touch, - * and if touched, transform to the user x and y coordinates. If the state has - * changed or if the value should be reported again due to the - * report interval, run the action and inform the virtual buttons. - */ - void update() override; - - /**@{*/ - /** Coordinates of the touch position. - * - * The values are set immediately before the on_state action with touched == true - * is triggered. The action with touched == false sends the coordinates of the last - * reported touch. - */ - int16_t x{0}, y{0}; - /**@}*/ - - /// True if the component currently detects the touch - bool touched{false}; - - /**@{*/ - /** Raw sensor values of the coordinates and the pressure. - * - * The values are set each time the update() method is called. - */ - int16_t x_raw{0}, y_raw{0}, z_raw{0}; - /**@}*/ - - protected: - static int16_t best_two_avg(int16_t x, int16_t y, int16_t z); - static int16_t normalize(int16_t val, int16_t min_val, int16_t max_val); - - int16_t read_adc_(uint8_t ctrl); - void check_touch_(); - - int16_t threshold_; - int16_t x_raw_min_, x_raw_max_, y_raw_min_, y_raw_max_; - - bool invert_x_, invert_y_; - bool swap_x_y_; - - uint32_t report_millis_; - uint32_t last_pos_ms_{0}; - - InternalGPIOPin *irq_pin_{nullptr}; - XPT2046TouchscreenStore store_; -}; - -} // namespace xpt2046 -} // namespace esphome diff --git a/tests/test4.yaml b/tests/test4.yaml index 9f15b84b12..fc719abecb 100644 --- a/tests/test4.yaml +++ b/tests/test4.yaml @@ -464,6 +464,7 @@ binary_sensor: sx1509: sx1509_hub number: 3 + - platform: touchscreen touchscreen_id: lilygo_touchscreen id: touch_key1 @@ -483,6 +484,7 @@ binary_sensor: pin: max6956: max6956_1 number: 4 + mode: input: true pullup: true @@ -506,6 +508,7 @@ binary_sensor: input: true inverted: false + climate: - platform: tuya id: tuya_climate @@ -595,6 +598,8 @@ display: it.rectangle(1, 1, it.get_width()-2, it.get_height()-2, green); it.rectangle(2, 2, it.get_width()-4, it.get_height()-4, blue); it.rectangle(3, 3, it.get_width()-6, it.get_height()-6, red); + auto touch = id(ft63_touchscreen)->get_touch(); + if (touch) { ESP_LOGD("touch", "%d/%d", touch.value().x, touch.value().y); } rotation: 0° update_interval: 16ms @@ -677,53 +682,53 @@ display: update_interval: 60s display_data_1_pin: - number: 5 + number: GPIO5 allow_other_uses: true display_data_2_pin: - number: 18 + number: GPIO18 allow_other_uses: true display_data_3_pin: - number: 19 + number: GPIO19 allow_other_uses: true display_data_5_pin: - number: 25 + number: GPIO25 allow_other_uses: true display_data_4_pin: - number: 23 + number: GPIO23 allow_other_uses: true display_data_6_pin: - number: 26 + number: GPIO26 allow_other_uses: true display_data_7_pin: - number: 27 + number: GPIO27 allow_other_uses: true ckv_pin: - allow_other_uses: true number: GPIO1 + allow_other_uses: true sph_pin: - allow_other_uses: true number: GPIO1 + allow_other_uses: true gmod_pin: - allow_other_uses: true number: GPIO1 + allow_other_uses: true gpio0_enable_pin: - allow_other_uses: true number: GPIO1 + allow_other_uses: true oe_pin: - allow_other_uses: true number: GPIO1 + allow_other_uses: true spv_pin: - allow_other_uses: true number: GPIO1 + allow_other_uses: true powerup_pin: - allow_other_uses: true number: GPIO1 + allow_other_uses: true wakeup_pin: - allow_other_uses: true number: GPIO1 + allow_other_uses: true vcom_pin: - allow_other_uses: true number: GPIO1 + allow_other_uses: true number: - platform: tuya @@ -816,21 +821,16 @@ esp32_camera: allow_other_uses: true - number: GPIO35 allow_other_uses: true - - - number: GPIO34 - - - number: GPIO5 + - number: GPIO34 + - number: GPIO5 allow_other_uses: true - - - number: GPIO39 - - - number: GPIO18 + - number: GPIO39 allow_other_uses: true - - - number: GPIO36 + - number: GPIO18 allow_other_uses: true - - - number: GPIO19 + - number: GPIO36 + allow_other_uses: true + - number: GPIO19 allow_other_uses: true vsync_pin: allow_other_uses: true @@ -910,18 +910,16 @@ touchscreen: spi_id: spi_id_2 cs_pin: allow_other_uses: true - number: 17 + number: GPIO17 interrupt_pin: - number: 16 + number: GPIO16 display: inkplate_display update_interval: 50ms - report_interval: 1s threshold: 400 calibration_x_min: 3860 calibration_x_max: 280 calibration_y_min: 340 calibration_y_max: 3860 - swap_x_y: false on_touch: - logger.log: format: Touch at (%d, %d) @@ -938,10 +936,25 @@ touchscreen: format: Touch at (%d, %d) args: [touch.x, touch.y] - platform: gt911 - interrupt_pin: GPIO3 + interrupt_pin: + number: GPIO3 display: inkplate_display + - platform: ft63x6 + id: ft63_touchscreen + interrupt_pin: + allow_other_uses: true + number: GPIO39 + reset_pin: + allow_other_uses: true + number: GPIO5 + display: inkplate_display + on_touch: + - logger.log: + format: Touch at (%d, %d) + args: [touch.x, touch.y] + i2s_audio: i2s_lrclk_pin: allow_other_uses: true diff --git a/tests/test8.yaml b/tests/test8.yaml index 5e4a41080a..558e86e1f9 100644 --- a/tests/test8.yaml +++ b/tests/test8.yaml @@ -54,6 +54,7 @@ spi_device: display: - platform: ili9xxx + id: displ8 model: ili9342 cs_pin: GPIO5 dc_pin: GPIO4 @@ -67,6 +68,7 @@ i2c: touchscreen: - platform: tt21100 + display: displ8 interrupt_pin: number: GPIO3 ignore_strapping_warning: true From 259a6d52e1de45ce15aa5229c672e924419da84a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Br=C3=A6mer?= <55437145+TH-Braemer@users.noreply.github.com> Date: Wed, 13 Dec 2023 00:26:08 +0100 Subject: [PATCH 65/89] A02yyuw (#5729) --- CODEOWNERS | 1 + esphome/components/a02yyuw/__init__.py | 1 + esphome/components/a02yyuw/a02yyuw.cpp | 43 ++++++++++++++++++++++++++ esphome/components/a02yyuw/a02yyuw.h | 27 ++++++++++++++++ esphome/components/a02yyuw/sensor.py | 41 ++++++++++++++++++++++++ tests/test4.yaml | 13 ++++++++ 6 files changed, 126 insertions(+) create mode 100644 esphome/components/a02yyuw/__init__.py create mode 100644 esphome/components/a02yyuw/a02yyuw.cpp create mode 100644 esphome/components/a02yyuw/a02yyuw.h create mode 100644 esphome/components/a02yyuw/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index c37eb3581b..d509c98433 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -12,6 +12,7 @@ esphome/core/* @esphome/core # Integrations esphome/components/a01nyub/* @MrSuicideParrot +esphome/components/a02yyuw/* @TH-Braemer esphome/components/absolute_humidity/* @DAVe3283 esphome/components/ac_dimmer/* @glmnet esphome/components/adc/* @esphome/core diff --git a/esphome/components/a02yyuw/__init__.py b/esphome/components/a02yyuw/__init__.py new file mode 100644 index 0000000000..6724dbb970 --- /dev/null +++ b/esphome/components/a02yyuw/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@TH-Braemer"] diff --git a/esphome/components/a02yyuw/a02yyuw.cpp b/esphome/components/a02yyuw/a02yyuw.cpp new file mode 100644 index 0000000000..ee378c3283 --- /dev/null +++ b/esphome/components/a02yyuw/a02yyuw.cpp @@ -0,0 +1,43 @@ +// Datasheet https://wiki.dfrobot.com/_A02YYUW_Waterproof_Ultrasonic_Sensor_SKU_SEN0311 + +#include "a02yyuw.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace a02yyuw { + +static const char *const TAG = "a02yyuw.sensor"; + +void A02yyuwComponent::loop() { + uint8_t data; + while (this->available() > 0) { + this->read_byte(&data); + if (this->buffer_.empty() && (data != 0xff)) + continue; + buffer_.push_back(data); + if (this->buffer_.size() == 4) + this->check_buffer_(); + } +} + +void A02yyuwComponent::check_buffer_() { + uint8_t checksum = this->buffer_[0] + this->buffer_[1] + this->buffer_[2]; + if (this->buffer_[3] == checksum) { + float distance = (this->buffer_[1] << 8) + this->buffer_[2]; + if (distance > 30) { + ESP_LOGV(TAG, "Distance from sensor: %f mm", distance); + this->publish_state(distance); + } else { + ESP_LOGW(TAG, "Invalid data read from sensor: %s", format_hex_pretty(this->buffer_).c_str()); + } + } else { + ESP_LOGW(TAG, "checksum failed: %02x != %02x", checksum, this->buffer_[3]); + } + this->buffer_.clear(); +} + +void A02yyuwComponent::dump_config() { LOG_SENSOR("", "A02yyuw Sensor", this); } + +} // namespace a02yyuw +} // namespace esphome diff --git a/esphome/components/a02yyuw/a02yyuw.h b/esphome/components/a02yyuw/a02yyuw.h new file mode 100644 index 0000000000..6ff370fdc3 --- /dev/null +++ b/esphome/components/a02yyuw/a02yyuw.h @@ -0,0 +1,27 @@ +#pragma once + +#include + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/uart/uart.h" + +namespace esphome { +namespace a02yyuw { + +class A02yyuwComponent : public sensor::Sensor, public Component, public uart::UARTDevice { + public: + // Nothing really public. + + // ========== INTERNAL METHODS ========== + void loop() override; + void dump_config() override; + + protected: + void check_buffer_(); + + std::vector buffer_; +}; + +} // namespace a02yyuw +} // namespace esphome diff --git a/esphome/components/a02yyuw/sensor.py b/esphome/components/a02yyuw/sensor.py new file mode 100644 index 0000000000..5232b04546 --- /dev/null +++ b/esphome/components/a02yyuw/sensor.py @@ -0,0 +1,41 @@ +import esphome.codegen as cg +from esphome.components import sensor, uart +from esphome.const import ( + STATE_CLASS_MEASUREMENT, + ICON_ARROW_EXPAND_VERTICAL, + DEVICE_CLASS_DISTANCE, +) + +CODEOWNERS = ["@TH-Braemer"] +DEPENDENCIES = ["uart"] +UNIT_MILLIMETERS = "mm" + +a02yyuw_ns = cg.esphome_ns.namespace("a02yyuw") +A02yyuwComponent = a02yyuw_ns.class_( + "A02yyuwComponent", sensor.Sensor, cg.Component, uart.UARTDevice +) + +CONFIG_SCHEMA = sensor.sensor_schema( + A02yyuwComponent, + unit_of_measurement=UNIT_MILLIMETERS, + icon=ICON_ARROW_EXPAND_VERTICAL, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + device_class=DEVICE_CLASS_DISTANCE, +).extend(uart.UART_DEVICE_SCHEMA) + +FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( + "a02yyuw", + baud_rate=9600, + require_tx=False, + require_rx=True, + data_bits=8, + parity=None, + stop_bits=1, +) + + +async def to_code(config): + var = await sensor.new_sensor(config) + await cg.register_component(var, config) + await uart.register_uart_device(var, config) diff --git a/tests/test4.yaml b/tests/test4.yaml index fc719abecb..089caf073b 100644 --- a/tests/test4.yaml +++ b/tests/test4.yaml @@ -84,6 +84,14 @@ uart: allow_other_uses: true number: GPIO26 baud_rate: 9600 + - id: uart_a02yyuw + tx_pin: + allow_other_uses: true + number: GPIO22 + rx_pin: + allow_other_uses: true + number: GPIO23 + baud_rate: 9600 - id: uart_he60r tx_pin: number: GPIO18 @@ -357,6 +365,11 @@ sensor: name: "a01nyub Distance" uart_id: uart9600 state_topic: "esphome/sensor/a01nyub_sensor/state" + - platform: a02yyuw + id: a02yyuw_sensor + name: "a02yyuw Distance" + uart_id: uart_a02yyuw + state_topic: "esphome/sensor/a02yyuw_sensor/state" # # platform sensor.apds9960 requires component apds9960 From 03baaa94a8d506212ff3fdf9ada258887e717cc7 Mon Sep 17 00:00:00 2001 From: Theo Hussey Date: Tue, 12 Dec 2023 23:28:59 +0000 Subject: [PATCH 66/89] Fix AHT10 / AHT20 communication (#5198) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/aht10/aht10.cpp | 55 ++++++++++++++++++------------ esphome/components/aht10/aht10.h | 6 ++++ esphome/components/aht10/sensor.py | 11 ++++++ 3 files changed, 51 insertions(+), 21 deletions(-) diff --git a/esphome/components/aht10/aht10.cpp b/esphome/components/aht10/aht10.cpp index 1ca06b458a..4d69a67487 100644 --- a/esphome/components/aht10/aht10.cpp +++ b/esphome/components/aht10/aht10.cpp @@ -21,36 +21,49 @@ namespace esphome { namespace aht10 { static const char *const TAG = "aht10"; -static const uint8_t AHT10_CALIBRATE_CMD[] = {0xE1}; +static const size_t SIZE_CALIBRATE_CMD = 3; +static const uint8_t AHT10_CALIBRATE_CMD[] = {0xE1, 0x08, 0x00}; +static const uint8_t AHT20_CALIBRATE_CMD[] = {0xBE, 0x08, 0x00}; static const uint8_t AHT10_MEASURE_CMD[] = {0xAC, 0x33, 0x00}; static const uint8_t AHT10_DEFAULT_DELAY = 5; // ms, for calibration and temperature measurement static const uint8_t AHT10_HUMIDITY_DELAY = 30; // ms static const uint8_t AHT10_ATTEMPTS = 3; // safety margin, normally 3 attempts are enough: 3*30=90ms +static const uint8_t AHT10_CAL_ATTEMPTS = 10; +static const uint8_t AHT10_STATUS_BUSY = 0x80; void AHT10Component::setup() { - ESP_LOGCONFIG(TAG, "Setting up AHT10..."); + const uint8_t *calibrate_cmd; + switch (this->variant_) { + case AHT10Variant::AHT20: + calibrate_cmd = AHT20_CALIBRATE_CMD; + ESP_LOGCONFIG(TAG, "Setting up AHT20"); + break; + case AHT10Variant::AHT10: + default: + calibrate_cmd = AHT10_CALIBRATE_CMD; + ESP_LOGCONFIG(TAG, "Setting up AHT10"); + } - if (!this->write_bytes(0, AHT10_CALIBRATE_CMD, sizeof(AHT10_CALIBRATE_CMD))) { + if (this->write(calibrate_cmd, SIZE_CALIBRATE_CMD) != i2c::ERROR_OK) { ESP_LOGE(TAG, "Communication with AHT10 failed!"); this->mark_failed(); return; } - uint8_t data = 0; - if (this->write(&data, 1) != i2c::ERROR_OK) { - ESP_LOGD(TAG, "Communication with AHT10 failed!"); - this->mark_failed(); - return; - } - delay(AHT10_DEFAULT_DELAY); - if (this->read(&data, 1) != i2c::ERROR_OK) { - ESP_LOGD(TAG, "Communication with AHT10 failed!"); - this->mark_failed(); - return; - } - if (this->read(&data, 1) != i2c::ERROR_OK) { - ESP_LOGD(TAG, "Communication with AHT10 failed!"); - this->mark_failed(); - return; + uint8_t data = AHT10_STATUS_BUSY; + int cal_attempts = 0; + while (data & AHT10_STATUS_BUSY) { + delay(AHT10_DEFAULT_DELAY); + if (this->read(&data, 1) != i2c::ERROR_OK) { + ESP_LOGE(TAG, "Communication with AHT10 failed!"); + this->mark_failed(); + return; + } + ++cal_attempts; + if (cal_attempts > AHT10_CAL_ATTEMPTS) { + ESP_LOGE(TAG, "AHT10 calibration timed out!"); + this->mark_failed(); + return; + } } if ((data & 0x68) != 0x08) { // Bit[6:5] = 0b00, NORMAL mode and Bit[3] = 0b1, CALIBRATED ESP_LOGE(TAG, "AHT10 calibration failed!"); @@ -62,7 +75,7 @@ void AHT10Component::setup() { } void AHT10Component::update() { - if (!this->write_bytes(0, AHT10_MEASURE_CMD, sizeof(AHT10_MEASURE_CMD))) { + if (this->write(AHT10_MEASURE_CMD, sizeof(AHT10_MEASURE_CMD)) != i2c::ERROR_OK) { ESP_LOGE(TAG, "Communication with AHT10 failed!"); this->status_set_warning(); return; @@ -89,7 +102,7 @@ void AHT10Component::update() { break; } else { ESP_LOGD(TAG, "ATH10 Unrealistic humidity (0x0), retrying..."); - if (!this->write_bytes(0, AHT10_MEASURE_CMD, sizeof(AHT10_MEASURE_CMD))) { + if (this->write(AHT10_MEASURE_CMD, sizeof(AHT10_MEASURE_CMD)) != i2c::ERROR_OK) { ESP_LOGE(TAG, "Communication with AHT10 failed!"); this->status_set_warning(); return; diff --git a/esphome/components/aht10/aht10.h b/esphome/components/aht10/aht10.h index 4d0eaa5919..3840609d56 100644 --- a/esphome/components/aht10/aht10.h +++ b/esphome/components/aht10/aht10.h @@ -1,5 +1,7 @@ #pragma once +#include + #include "esphome/core/component.h" #include "esphome/components/sensor/sensor.h" #include "esphome/components/i2c/i2c.h" @@ -7,12 +9,15 @@ namespace esphome { namespace aht10 { +enum AHT10Variant { AHT10, AHT20 }; + class AHT10Component : public PollingComponent, public i2c::I2CDevice { public: void setup() override; void update() override; void dump_config() override; float get_setup_priority() const override; + void set_variant(AHT10Variant variant) { this->variant_ = variant; } void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } void set_humidity_sensor(sensor::Sensor *humidity_sensor) { humidity_sensor_ = humidity_sensor; } @@ -20,6 +25,7 @@ class AHT10Component : public PollingComponent, public i2c::I2CDevice { protected: sensor::Sensor *temperature_sensor_{nullptr}; sensor::Sensor *humidity_sensor_{nullptr}; + AHT10Variant variant_{}; }; } // namespace aht10 diff --git a/esphome/components/aht10/sensor.py b/esphome/components/aht10/sensor.py index a52773b6d7..31b07c0e73 100644 --- a/esphome/components/aht10/sensor.py +++ b/esphome/components/aht10/sensor.py @@ -10,6 +10,7 @@ from esphome.const import ( STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, UNIT_PERCENT, + CONF_VARIANT, ) DEPENDENCIES = ["i2c"] @@ -17,6 +18,12 @@ DEPENDENCIES = ["i2c"] aht10_ns = cg.esphome_ns.namespace("aht10") AHT10Component = aht10_ns.class_("AHT10Component", cg.PollingComponent, i2c.I2CDevice) +AHT10Variant = aht10_ns.enum("AHT10Variant") +AHT10_VARIANTS = { + "AHT10": AHT10Variant.AHT10, + "AHT20": AHT10Variant.AHT20, +} + CONFIG_SCHEMA = ( cv.Schema( { @@ -33,6 +40,9 @@ CONFIG_SCHEMA = ( device_class=DEVICE_CLASS_HUMIDITY, state_class=STATE_CLASS_MEASUREMENT, ), + cv.Optional(CONF_VARIANT, default="AHT10"): cv.enum( + AHT10_VARIANTS, upper=True + ), } ) .extend(cv.polling_component_schema("60s")) @@ -44,6 +54,7 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await i2c.register_i2c_device(var, config) + cg.add(var.set_variant(config[CONF_VARIANT])) if temperature := config.get(CONF_TEMPERATURE): sens = await sensor.new_sensor(temperature) From 29002c8f45f69a9caf0887d3de07b9db67149f0d Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Wed, 13 Dec 2023 10:38:37 +1100 Subject: [PATCH 67/89] Fix crash when handling pin_check error (#5915) --- esphome/config.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/esphome/config.py b/esphome/config.py index 3461223490..745883c2ef 100644 --- a/esphome/config.py +++ b/esphome/config.py @@ -111,7 +111,12 @@ class Config(OrderedDict, fv.FinalValidateConfig): last_root = max( i for i, v in enumerate(error.path) if v is cv.ROOT_CONFIG_PATH ) - error.path = error.path[last_root + 1 :] + # can't change the path so re-create the error + error = vol.Invalid( + message=error.error_message, + path=error.path[last_root + 1 :], + error_type=error.error_type, + ) self.errors.append(error) def add_validation_step(self, step: "ConfigValidationStep"): From 8789925fe89d6f9e98083111335530ae64a3d9ea Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 12 Dec 2023 13:48:14 -1000 Subject: [PATCH 68/89] Bump aioesphomeapi to 20.1.0 (#5914) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index ae8d50f2ea..0d08da2b45 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ platformio==6.1.11 # When updating platformio, also update Dockerfile esptool==4.6.2 click==8.1.7 esphome-dashboard==20231107.0 -aioesphomeapi==20.0.0 +aioesphomeapi==20.1.0 zeroconf==0.128.4 python-magic==0.4.27 From 69026f759958b6203fd021e255d42ba7664e2730 Mon Sep 17 00:00:00 2001 From: Mathias Pihl Date: Wed, 13 Dec 2023 00:50:55 +0100 Subject: [PATCH 69/89] Remove setpoint-change from error when calculating derivative in pid controller (#4737) Co-authored-by: Mathias Pihl --- esphome/components/pid/pid_controller.cpp | 11 ++++++++--- esphome/components/pid/pid_controller.h | 3 ++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/esphome/components/pid/pid_controller.cpp b/esphome/components/pid/pid_controller.cpp index 30f6038325..1a16f14542 100644 --- a/esphome/components/pid/pid_controller.cpp +++ b/esphome/components/pid/pid_controller.cpp @@ -16,7 +16,7 @@ float PIDController::update(float setpoint, float process_value) { calculate_proportional_term_(); calculate_integral_term_(); - calculate_derivative_term_(); + calculate_derivative_term_(setpoint); // u(t) := p(t) + i(t) + d(t) float output = proportional_term_ + integral_term_ + derivative_term_; @@ -69,13 +69,18 @@ void PIDController::calculate_integral_term_() { integral_term_ = accumulated_integral_; } -void PIDController::calculate_derivative_term_() { +void PIDController::calculate_derivative_term_(float setpoint) { // derivative_term_ // d(t) := K_d * de(t)/dt float derivative = 0.0f; - if (dt_ != 0.0f) + if (dt_ != 0.0f) { + // remove changes to setpoint from error + if (!std::isnan(previous_setpoint_) && previous_setpoint_ != setpoint) + previous_error_ -= previous_setpoint_ - setpoint; derivative = (error_ - previous_error_) / dt_; + } previous_error_ = error_; + previous_setpoint_ = setpoint; // smooth the derivative samples derivative = weighted_average_(derivative_list_, derivative, derivative_samples_); diff --git a/esphome/components/pid/pid_controller.h b/esphome/components/pid/pid_controller.h index 05ce5f9224..e2a7030b57 100644 --- a/esphome/components/pid/pid_controller.h +++ b/esphome/components/pid/pid_controller.h @@ -49,12 +49,13 @@ struct PIDController { void calculate_proportional_term_(); void calculate_integral_term_(); - void calculate_derivative_term_(); + void calculate_derivative_term_(float setpoint); float weighted_average_(std::deque &list, float new_value, int samples); float calculate_relative_time_(); /// Error from previous update used for derivative term float previous_error_ = 0; + float previous_setpoint_ = NAN; /// Accumulated integral value float accumulated_integral_ = 0; uint32_t last_time_ = 0; From d0bcba3b3fef78a394c6937ba20d2ca88bc30150 Mon Sep 17 00:00:00 2001 From: Nicolas Gilles Date: Wed, 13 Dec 2023 00:55:20 +0100 Subject: [PATCH 70/89] ir_transmitter: add support for repeated commands in NEC protocol (#4995) Co-authored-by: Nicolas Gilles --- esphome/components/remote_base/__init__.py | 5 +++ .../components/remote_base/nec_protocol.cpp | 42 +++++++++++++++---- esphome/components/remote_base/nec_protocol.h | 3 ++ esphome/const.py | 1 + 4 files changed, 44 insertions(+), 7 deletions(-) diff --git a/esphome/components/remote_base/__init__.py b/esphome/components/remote_base/__init__.py index a2411b1b12..3accd5038c 100644 --- a/esphome/components/remote_base/__init__.py +++ b/esphome/components/remote_base/__init__.py @@ -3,6 +3,7 @@ import esphome.config_validation as cv from esphome import automation from esphome.components import binary_sensor from esphome.const import ( + CONF_COMMAND_REPEATS, CONF_DATA, CONF_TRIGGER_ID, CONF_NBITS, @@ -638,6 +639,7 @@ NEC_SCHEMA = cv.Schema( { cv.Required(CONF_ADDRESS): cv.hex_uint16_t, cv.Required(CONF_COMMAND): cv.hex_uint16_t, + cv.Optional(CONF_COMMAND_REPEATS, default=1): cv.uint16_t, } ) @@ -650,6 +652,7 @@ def nec_binary_sensor(var, config): NECData, ("address", config[CONF_ADDRESS]), ("command", config[CONF_COMMAND]), + ("command_repeats", config[CONF_COMMAND_REPEATS]), ) ) ) @@ -671,6 +674,8 @@ async def nec_action(var, config, args): cg.add(var.set_address(template_)) template_ = await cg.templatable(config[CONF_COMMAND], args, cg.uint16) cg.add(var.set_command(template_)) + template_ = await cg.templatable(config[CONF_COMMAND_REPEATS], args, cg.uint16) + cg.add(var.set_command_repeats(template_)) # Pioneer diff --git a/esphome/components/remote_base/nec_protocol.cpp b/esphome/components/remote_base/nec_protocol.cpp index d5c68784ee..6ea9a8583c 100644 --- a/esphome/components/remote_base/nec_protocol.cpp +++ b/esphome/components/remote_base/nec_protocol.cpp @@ -13,10 +13,14 @@ static const uint32_t BIT_ONE_LOW_US = 1690; static const uint32_t BIT_ZERO_LOW_US = 560; void NECProtocol::encode(RemoteTransmitData *dst, const NECData &data) { - dst->reserve(68); + ESP_LOGD(TAG, "Sending NEC: address=0x%04X, command=0x%04X command_repeats=%d", data.address, data.command, + data.command_repeats); + + dst->reserve(2 + 32 + 32 * data.command_repeats + 2); dst->set_carrier_frequency(38000); dst->item(HEADER_HIGH_US, HEADER_LOW_US); + for (uint16_t mask = 1; mask; mask <<= 1) { if (data.address & mask) { dst->item(BIT_HIGH_US, BIT_ONE_LOW_US); @@ -25,11 +29,13 @@ void NECProtocol::encode(RemoteTransmitData *dst, const NECData &data) { } } - for (uint16_t mask = 1; mask; mask <<= 1) { - if (data.command & mask) { - dst->item(BIT_HIGH_US, BIT_ONE_LOW_US); - } else { - dst->item(BIT_HIGH_US, BIT_ZERO_LOW_US); + for (uint16_t repeats = 0; repeats < data.command_repeats; repeats++) { + for (uint16_t mask = 1; mask; mask <<= 1) { + if (data.command & mask) { + dst->item(BIT_HIGH_US, BIT_ONE_LOW_US); + } else { + dst->item(BIT_HIGH_US, BIT_ZERO_LOW_US); + } } } @@ -39,6 +45,7 @@ optional NECProtocol::decode(RemoteReceiveData src) { NECData data{ .address = 0, .command = 0, + .command_repeats = 1, }; if (!src.expect_item(HEADER_HIGH_US, HEADER_LOW_US)) return {}; @@ -63,11 +70,32 @@ optional NECProtocol::decode(RemoteReceiveData src) { } } + while (src.peek_item(BIT_HIGH_US, BIT_ONE_LOW_US) || src.peek_item(BIT_HIGH_US, BIT_ZERO_LOW_US)) { + uint16_t command = 0; + for (uint16_t mask = 1; mask; mask <<= 1) { + if (src.expect_item(BIT_HIGH_US, BIT_ONE_LOW_US)) { + command |= mask; + } else if (src.expect_item(BIT_HIGH_US, BIT_ZERO_LOW_US)) { + command &= ~mask; + } else { + return {}; + } + } + + // Make sure the extra/repeated data matches original command + if (command != data.command) { + return {}; + } + + data.command_repeats += 1; + } + src.expect_mark(BIT_HIGH_US); return data; } void NECProtocol::dump(const NECData &data) { - ESP_LOGI(TAG, "Received NEC: address=0x%04X, command=0x%04X", data.address, data.command); + ESP_LOGI(TAG, "Received NEC: address=0x%04X, command=0x%04X command_repeats=%d", data.address, data.command, + data.command_repeats); } } // namespace remote_base diff --git a/esphome/components/remote_base/nec_protocol.h b/esphome/components/remote_base/nec_protocol.h index 593a3efe17..71e1bccba8 100644 --- a/esphome/components/remote_base/nec_protocol.h +++ b/esphome/components/remote_base/nec_protocol.h @@ -8,6 +8,7 @@ namespace remote_base { struct NECData { uint16_t address; uint16_t command; + uint16_t command_repeats; bool operator==(const NECData &rhs) const { return address == rhs.address && command == rhs.command; } }; @@ -25,11 +26,13 @@ template class NECAction : public RemoteTransmitterActionBaseaddress_.value(x...); data.command = this->command_.value(x...); + data.command_repeats = this->command_repeats_.value(x...); NECProtocol().encode(dst, data); } }; diff --git a/esphome/const.py b/esphome/const.py index f688cd75f9..1aabe81db3 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -131,6 +131,7 @@ CONF_COLOR_PALETTE = "color_palette" CONF_COLOR_TEMPERATURE = "color_temperature" CONF_COLORS = "colors" CONF_COMMAND = "command" +CONF_COMMAND_REPEATS = "command_repeats" CONF_COMMAND_RETAIN = "command_retain" CONF_COMMAND_TOPIC = "command_topic" CONF_COMMENT = "comment" From a72725f4b40266646aa46886d042428d76504ae3 Mon Sep 17 00:00:00 2001 From: Cossid <83468485+Cossid@users.noreply.github.com> Date: Tue, 12 Dec 2023 18:04:17 -0600 Subject: [PATCH 71/89] BP1658CJ - Fix timing for all platforms, now consistent with other drivers (#5799) --- esphome/components/bp1658cj/bp1658cj.cpp | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/esphome/components/bp1658cj/bp1658cj.cpp b/esphome/components/bp1658cj/bp1658cj.cpp index 05c3f790c2..4b74cc85f5 100644 --- a/esphome/components/bp1658cj/bp1658cj.cpp +++ b/esphome/components/bp1658cj/bp1658cj.cpp @@ -90,40 +90,41 @@ void BP1658CJ::set_channel_value_(uint8_t channel, uint16_t value) { void BP1658CJ::write_bit_(bool value) { this->data_pin_->digital_write(value); - this->clock_pin_->digital_write(true); - delayMicroseconds(BP1658CJ_DELAY); - + this->clock_pin_->digital_write(true); + delayMicroseconds(BP1658CJ_DELAY); this->clock_pin_->digital_write(false); + delayMicroseconds(BP1658CJ_DELAY); } void BP1658CJ::write_byte_(uint8_t data) { for (uint8_t mask = 0x80; mask; mask >>= 1) { this->write_bit_(data & mask); - delayMicroseconds(BP1658CJ_DELAY); } // ack bit this->data_pin_->pin_mode(gpio::FLAG_INPUT); this->clock_pin_->digital_write(true); - delayMicroseconds(BP1658CJ_DELAY); - this->clock_pin_->digital_write(false); + delayMicroseconds(BP1658CJ_DELAY); this->data_pin_->pin_mode(gpio::FLAG_OUTPUT); } void BP1658CJ::write_buffer_(uint8_t *buffer, uint8_t size) { this->data_pin_->digital_write(false); + delayMicroseconds(BP1658CJ_DELAY); this->clock_pin_->digital_write(false); + delayMicroseconds(BP1658CJ_DELAY); for (uint32_t i = 0; i < size; i++) { this->write_byte_(buffer[i]); - delayMicroseconds(BP1658CJ_DELAY); } this->clock_pin_->digital_write(true); + delayMicroseconds(BP1658CJ_DELAY); this->data_pin_->digital_write(true); + delayMicroseconds(BP1658CJ_DELAY); } } // namespace bp1658cj From 6c7a133faa722f0a7d9c808094aba0ef1321bd70 Mon Sep 17 00:00:00 2001 From: Stefan Rado <628587+kroimon@users.noreply.github.com> Date: Wed, 13 Dec 2023 02:23:02 +0100 Subject: [PATCH 72/89] Add humidity support to climate (#5732) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/api/api.proto | 8 ++ esphome/components/api/api_connection.cpp | 10 +++ esphome/components/api/api_pb2.cpp | 79 ++++++++++++++++++- esphome/components/api/api_pb2.h | 8 ++ .../bang_bang/bang_bang_climate.cpp | 13 +++ .../components/bang_bang/bang_bang_climate.h | 4 + esphome/components/bang_bang/climate.py | 6 ++ esphome/components/climate/__init__.py | 43 ++++++++++ esphome/components/climate/automation.h | 2 + esphome/components/climate/climate.cpp | 54 ++++++++++++- esphome/components/climate/climate.h | 19 +++++ esphome/components/climate/climate_traits.h | 17 ++++ esphome/components/mqtt/mqtt_climate.cpp | 42 +++++++++- esphome/components/mqtt/mqtt_climate.h | 3 + esphome/components/mqtt/mqtt_const.h | 4 + esphome/components/pid/climate.py | 7 +- esphome/components/pid/pid_climate.cpp | 13 +++ esphome/components/pid/pid_climate.h | 3 + esphome/components/thermostat/climate.py | 6 ++ .../thermostat/thermostat_climate.cpp | 15 ++++ .../thermostat/thermostat_climate.h | 3 + esphome/const.py | 4 + tests/test3.yaml | 3 + 23 files changed, 359 insertions(+), 7 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 34d137981e..04db649aef 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -859,6 +859,10 @@ message ListEntitiesClimateResponse { string icon = 19; EntityCategory entity_category = 20; float visual_current_temperature_step = 21; + bool supports_current_humidity = 22; + bool supports_target_humidity = 23; + float visual_min_humidity = 24; + float visual_max_humidity = 25; } message ClimateStateResponse { option (id) = 47; @@ -879,6 +883,8 @@ message ClimateStateResponse { string custom_fan_mode = 11; ClimatePreset preset = 12; string custom_preset = 13; + float current_humidity = 14; + float target_humidity = 15; } message ClimateCommandRequest { option (id) = 48; @@ -907,6 +913,8 @@ message ClimateCommandRequest { ClimatePreset preset = 19; bool has_custom_preset = 20; string custom_preset = 21; + bool has_target_humidity = 22; + float target_humidity = 23; } // ==================== NUMBER ==================== diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index b8f5b05538..d5ab00a822 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -560,6 +560,10 @@ bool APIConnection::send_climate_state(climate::Climate *climate) { resp.custom_preset = climate->custom_preset.value(); if (traits.get_supports_swing_modes()) resp.swing_mode = static_cast(climate->swing_mode); + if (traits.get_supports_current_humidity()) + resp.current_humidity = climate->current_humidity; + if (traits.get_supports_target_humidity()) + resp.target_humidity = climate->target_humidity; return this->send_climate_state_response(resp); } bool APIConnection::send_climate_info(climate::Climate *climate) { @@ -576,7 +580,9 @@ bool APIConnection::send_climate_info(climate::Climate *climate) { msg.entity_category = static_cast(climate->get_entity_category()); msg.supports_current_temperature = traits.get_supports_current_temperature(); + msg.supports_current_humidity = traits.get_supports_current_humidity(); msg.supports_two_point_target_temperature = traits.get_supports_two_point_target_temperature(); + msg.supports_target_humidity = traits.get_supports_target_humidity(); for (auto mode : traits.get_supported_modes()) msg.supported_modes.push_back(static_cast(mode)); @@ -585,6 +591,8 @@ bool APIConnection::send_climate_info(climate::Climate *climate) { msg.visual_max_temperature = traits.get_visual_max_temperature(); msg.visual_target_temperature_step = traits.get_visual_target_temperature_step(); msg.visual_current_temperature_step = traits.get_visual_current_temperature_step(); + msg.visual_min_humidity = traits.get_visual_min_humidity(); + msg.visual_max_humidity = traits.get_visual_max_humidity(); msg.legacy_supports_away = traits.supports_preset(climate::CLIMATE_PRESET_AWAY); msg.supports_action = traits.get_supports_action(); @@ -615,6 +623,8 @@ void APIConnection::climate_command(const ClimateCommandRequest &msg) { call.set_target_temperature_low(msg.target_temperature_low); if (msg.has_target_temperature_high) call.set_target_temperature_high(msg.target_temperature_high); + if (msg.has_target_humidity) + call.set_target_humidity(msg.target_humidity); if (msg.has_fan_mode) call.set_fan_mode(static_cast(msg.fan_mode)); if (msg.has_custom_fan_mode) diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 01fb540e7e..8dd34e7ef1 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -3611,6 +3611,14 @@ bool ListEntitiesClimateResponse::decode_varint(uint32_t field_id, ProtoVarInt v this->entity_category = value.as_enum(); return true; } + case 22: { + this->supports_current_humidity = value.as_bool(); + return true; + } + case 23: { + this->supports_target_humidity = value.as_bool(); + return true; + } default: return false; } @@ -3667,6 +3675,14 @@ bool ListEntitiesClimateResponse::decode_32bit(uint32_t field_id, Proto32Bit val this->visual_current_temperature_step = value.as_float(); return true; } + case 24: { + this->visual_min_humidity = value.as_float(); + return true; + } + case 25: { + this->visual_max_humidity = value.as_float(); + return true; + } default: return false; } @@ -3705,6 +3721,10 @@ void ListEntitiesClimateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(19, this->icon); buffer.encode_enum(20, this->entity_category); buffer.encode_float(21, this->visual_current_temperature_step); + buffer.encode_bool(22, this->supports_current_humidity); + buffer.encode_bool(23, this->supports_target_humidity); + buffer.encode_float(24, this->visual_min_humidity); + buffer.encode_float(25, this->visual_max_humidity); } #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesClimateResponse::dump_to(std::string &out) const { @@ -3810,7 +3830,24 @@ void ListEntitiesClimateResponse::dump_to(std::string &out) const { sprintf(buffer, "%g", this->visual_current_temperature_step); out.append(buffer); out.append("\n"); - out.append("}"); + + out.append(" supports_current_humidity: "); + out.append(YESNO(this->supports_current_humidity)); + out.append("\n"); + + out.append(" supports_target_humidity: "); + out.append(YESNO(this->supports_target_humidity)); + out.append("\n"); + + out.append(" visual_min_humidity: "); + sprintf(buffer, "%g", this->visual_min_humidity); + out.append(buffer); + out.append("\n"); + + out.append(" visual_max_humidity: "); + sprintf(buffer, "%g", this->visual_max_humidity); + out.append(buffer); + out.append("\n"); } #endif bool ClimateStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { @@ -3879,6 +3916,14 @@ bool ClimateStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { this->target_temperature_high = value.as_float(); return true; } + case 14: { + this->current_humidity = value.as_float(); + return true; + } + case 15: { + this->target_humidity = value.as_float(); + return true; + } default: return false; } @@ -3897,6 +3942,8 @@ void ClimateStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(11, this->custom_fan_mode); buffer.encode_enum(12, this->preset); buffer.encode_string(13, this->custom_preset); + buffer.encode_float(14, this->current_humidity); + buffer.encode_float(15, this->target_humidity); } #ifdef HAS_PROTO_MESSAGE_DUMP void ClimateStateResponse::dump_to(std::string &out) const { @@ -3958,7 +4005,16 @@ void ClimateStateResponse::dump_to(std::string &out) const { out.append(" custom_preset: "); out.append("'").append(this->custom_preset).append("'"); out.append("\n"); - out.append("}"); + + out.append(" current_humidity: "); + sprintf(buffer, "%g", this->current_humidity); + out.append(buffer); + out.append("\n"); + + out.append(" target_humidity: "); + sprintf(buffer, "%g", this->target_humidity); + out.append(buffer); + out.append("\n"); } #endif bool ClimateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { @@ -4023,6 +4079,10 @@ bool ClimateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) this->has_custom_preset = value.as_bool(); return true; } + case 22: { + this->has_target_humidity = value.as_bool(); + return true; + } default: return false; } @@ -4059,6 +4119,10 @@ bool ClimateCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { this->target_temperature_high = value.as_float(); return true; } + case 23: { + this->target_humidity = value.as_float(); + return true; + } default: return false; } @@ -4085,6 +4149,8 @@ void ClimateCommandRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_enum(19, this->preset); buffer.encode_bool(20, this->has_custom_preset); buffer.encode_string(21, this->custom_preset); + buffer.encode_bool(22, this->has_target_humidity); + buffer.encode_float(23, this->target_humidity); } #ifdef HAS_PROTO_MESSAGE_DUMP void ClimateCommandRequest::dump_to(std::string &out) const { @@ -4177,6 +4243,15 @@ void ClimateCommandRequest::dump_to(std::string &out) const { out.append(" custom_preset: "); out.append("'").append(this->custom_preset).append("'"); out.append("\n"); + + out.append(" has_target_humidity: "); + out.append(YESNO(this->has_target_humidity)); + out.append("\n"); + + out.append(" target_humidity: "); + sprintf(buffer, "%g", this->target_humidity); + out.append(buffer); + out.append("\n"); out.append("}"); } #endif diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index fc57348863..02fc7b88f8 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -985,6 +985,10 @@ class ListEntitiesClimateResponse : public ProtoMessage { std::string icon{}; enums::EntityCategory entity_category{}; float visual_current_temperature_step{0.0f}; + bool supports_current_humidity{false}; + bool supports_target_humidity{false}; + float visual_min_humidity{0.0f}; + float visual_max_humidity{0.0f}; void encode(ProtoWriteBuffer buffer) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; @@ -1010,6 +1014,8 @@ class ClimateStateResponse : public ProtoMessage { std::string custom_fan_mode{}; enums::ClimatePreset preset{}; std::string custom_preset{}; + float current_humidity{0.0f}; + float target_humidity{0.0f}; void encode(ProtoWriteBuffer buffer) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; @@ -1043,6 +1049,8 @@ class ClimateCommandRequest : public ProtoMessage { enums::ClimatePreset preset{}; bool has_custom_preset{false}; std::string custom_preset{}; + bool has_target_humidity{false}; + float target_humidity{0.0f}; void encode(ProtoWriteBuffer buffer) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; diff --git a/esphome/components/bang_bang/bang_bang_climate.cpp b/esphome/components/bang_bang/bang_bang_climate.cpp index 20cb87025a..13b0cd2a09 100644 --- a/esphome/components/bang_bang/bang_bang_climate.cpp +++ b/esphome/components/bang_bang/bang_bang_climate.cpp @@ -15,6 +15,16 @@ void BangBangClimate::setup() { this->publish_state(); }); this->current_temperature = this->sensor_->state; + + // register for humidity values and get initial state + if (this->humidity_sensor_ != nullptr) { + this->humidity_sensor_->add_on_state_callback([this](float state) { + this->current_humidity = state; + this->publish_state(); + }); + this->current_humidity = this->humidity_sensor_->state; + } + // restore set points auto restore = this->restore_state_(); if (restore.has_value()) { @@ -47,6 +57,8 @@ void BangBangClimate::control(const climate::ClimateCall &call) { climate::ClimateTraits BangBangClimate::traits() { auto traits = climate::ClimateTraits(); traits.set_supports_current_temperature(true); + if (this->humidity_sensor_ != nullptr) + traits.set_supports_current_humidity(true); traits.set_supported_modes({ climate::CLIMATE_MODE_OFF, }); @@ -171,6 +183,7 @@ void BangBangClimate::set_away_config(const BangBangClimateTargetTempConfig &awa BangBangClimate::BangBangClimate() : idle_trigger_(new Trigger<>()), cool_trigger_(new Trigger<>()), heat_trigger_(new Trigger<>()) {} void BangBangClimate::set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; } +void BangBangClimate::set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; } Trigger<> *BangBangClimate::get_idle_trigger() const { return this->idle_trigger_; } Trigger<> *BangBangClimate::get_cool_trigger() const { return this->cool_trigger_; } void BangBangClimate::set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; } diff --git a/esphome/components/bang_bang/bang_bang_climate.h b/esphome/components/bang_bang/bang_bang_climate.h index 84bcd51f34..96368af34c 100644 --- a/esphome/components/bang_bang/bang_bang_climate.h +++ b/esphome/components/bang_bang/bang_bang_climate.h @@ -24,6 +24,7 @@ class BangBangClimate : public climate::Climate, public Component { void dump_config() override; void set_sensor(sensor::Sensor *sensor); + void set_humidity_sensor(sensor::Sensor *humidity_sensor); Trigger<> *get_idle_trigger() const; Trigger<> *get_cool_trigger() const; void set_supports_cool(bool supports_cool); @@ -48,6 +49,9 @@ class BangBangClimate : public climate::Climate, public Component { /// The sensor used for getting the current temperature sensor::Sensor *sensor_{nullptr}; + /// The sensor used for getting the current humidity + sensor::Sensor *humidity_sensor_{nullptr}; + /** The trigger to call when the controller should switch to idle mode. * * In idle mode, the controller is assumed to have both heating and cooling disabled. diff --git a/esphome/components/bang_bang/climate.py b/esphome/components/bang_bang/climate.py index ac0c328000..9dde0ae1ac 100644 --- a/esphome/components/bang_bang/climate.py +++ b/esphome/components/bang_bang/climate.py @@ -8,6 +8,7 @@ from esphome.const import ( CONF_DEFAULT_TARGET_TEMPERATURE_HIGH, CONF_DEFAULT_TARGET_TEMPERATURE_LOW, CONF_HEAT_ACTION, + CONF_HUMIDITY_SENSOR, CONF_ID, CONF_IDLE_ACTION, CONF_SENSOR, @@ -22,6 +23,7 @@ CONFIG_SCHEMA = cv.All( { cv.GenerateID(): cv.declare_id(BangBangClimate), cv.Required(CONF_SENSOR): cv.use_id(sensor.Sensor), + cv.Optional(CONF_HUMIDITY_SENSOR): cv.use_id(sensor.Sensor), cv.Required(CONF_DEFAULT_TARGET_TEMPERATURE_LOW): cv.temperature, cv.Required(CONF_DEFAULT_TARGET_TEMPERATURE_HIGH): cv.temperature, cv.Required(CONF_IDLE_ACTION): automation.validate_automation(single=True), @@ -47,6 +49,10 @@ async def to_code(config): sens = await cg.get_variable(config[CONF_SENSOR]) cg.add(var.set_sensor(sens)) + if CONF_HUMIDITY_SENSOR in config: + sens = await cg.get_variable(config[CONF_HUMIDITY_SENSOR]) + cg.add(var.set_humidity_sensor(sens)) + normal_config = BangBangClimateTargetTempConfig( config[CONF_DEFAULT_TARGET_TEMPERATURE_LOW], config[CONF_DEFAULT_TARGET_TEMPERATURE_HIGH], diff --git a/esphome/components/climate/__init__.py b/esphome/components/climate/__init__.py index 85242eb344..c9c3900a0c 100644 --- a/esphome/components/climate/__init__.py +++ b/esphome/components/climate/__init__.py @@ -8,6 +8,7 @@ from esphome.const import ( CONF_AWAY, CONF_AWAY_COMMAND_TOPIC, CONF_AWAY_STATE_TOPIC, + CONF_CURRENT_HUMIDITY_STATE_TOPIC, CONF_CURRENT_TEMPERATURE_STATE_TOPIC, CONF_CUSTOM_FAN_MODE, CONF_CUSTOM_PRESET, @@ -28,6 +29,8 @@ from esphome.const import ( CONF_SWING_MODE, CONF_SWING_MODE_COMMAND_TOPIC, CONF_SWING_MODE_STATE_TOPIC, + CONF_TARGET_HUMIDITY_COMMAND_TOPIC, + CONF_TARGET_HUMIDITY_STATE_TOPIC, CONF_TARGET_TEMPERATURE, CONF_TARGET_TEMPERATURE_COMMAND_TOPIC, CONF_TARGET_TEMPERATURE_STATE_TOPIC, @@ -106,6 +109,9 @@ CLIMATE_SWING_MODES = { validate_climate_swing_mode = cv.enum(CLIMATE_SWING_MODES, upper=True) CONF_CURRENT_TEMPERATURE = "current_temperature" +CONF_MIN_HUMIDITY = "min_humidity" +CONF_MAX_HUMIDITY = "max_humidity" +CONF_TARGET_HUMIDITY = "target_humidity" visual_temperature = cv.float_with_unit( "visual_temperature", "(°C|° C|°|C|° K|° K|K|°F|° F|F)?" @@ -153,6 +159,8 @@ CLIMATE_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA). cv.Optional(CONF_MIN_TEMPERATURE): cv.temperature, cv.Optional(CONF_MAX_TEMPERATURE): cv.temperature, cv.Optional(CONF_TEMPERATURE_STEP): VISUAL_TEMPERATURE_STEP_SCHEMA, + cv.Optional(CONF_MIN_HUMIDITY): cv.percentage_int, + cv.Optional(CONF_MAX_HUMIDITY): cv.percentage_int, } ), cv.Optional(CONF_ACTION_STATE_TOPIC): cv.All( @@ -167,6 +175,9 @@ CLIMATE_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA). cv.Optional(CONF_CURRENT_TEMPERATURE_STATE_TOPIC): cv.All( cv.requires_component("mqtt"), cv.publish_topic ), + cv.Optional(CONF_CURRENT_HUMIDITY_STATE_TOPIC): cv.All( + cv.requires_component("mqtt"), cv.publish_topic + ), cv.Optional(CONF_FAN_MODE_COMMAND_TOPIC): cv.All( cv.requires_component("mqtt"), cv.publish_topic ), @@ -209,6 +220,12 @@ CLIMATE_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA). cv.Optional(CONF_TARGET_TEMPERATURE_LOW_STATE_TOPIC): cv.All( cv.requires_component("mqtt"), cv.publish_topic ), + cv.Optional(CONF_TARGET_HUMIDITY_COMMAND_TOPIC): cv.All( + cv.requires_component("mqtt"), cv.publish_topic + ), + cv.Optional(CONF_TARGET_HUMIDITY_STATE_TOPIC): cv.All( + cv.requires_component("mqtt"), cv.publish_topic + ), cv.Optional(CONF_ON_CONTROL): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ControlTrigger), @@ -238,6 +255,10 @@ async def setup_climate_core_(var, config): visual[CONF_TEMPERATURE_STEP][CONF_CURRENT_TEMPERATURE], ) ) + if CONF_MIN_HUMIDITY in visual: + cg.add(var.set_visual_min_humidity_override(visual[CONF_MIN_HUMIDITY])) + if CONF_MAX_HUMIDITY in visual: + cg.add(var.set_visual_max_humidity_override(visual[CONF_MAX_HUMIDITY])) if CONF_MQTT_ID in config: mqtt_ = cg.new_Pvariable(config[CONF_MQTT_ID], var) @@ -255,6 +276,12 @@ async def setup_climate_core_(var, config): config[CONF_CURRENT_TEMPERATURE_STATE_TOPIC] ) ) + if CONF_CURRENT_HUMIDITY_STATE_TOPIC in config: + cg.add( + mqtt_.set_custom_current_humidity_state_topic( + config[CONF_CURRENT_HUMIDITY_STATE_TOPIC] + ) + ) if CONF_FAN_MODE_COMMAND_TOPIC in config: cg.add( mqtt_.set_custom_fan_mode_command_topic( @@ -323,6 +350,18 @@ async def setup_climate_core_(var, config): config[CONF_TARGET_TEMPERATURE_LOW_STATE_TOPIC] ) ) + if CONF_TARGET_HUMIDITY_COMMAND_TOPIC in config: + cg.add( + mqtt_.set_custom_target_humidity_command_topic( + config[CONF_TARGET_HUMIDITY_COMMAND_TOPIC] + ) + ) + if CONF_TARGET_HUMIDITY_STATE_TOPIC in config: + cg.add( + mqtt_.set_custom_target_humidity_state_topic( + config[CONF_TARGET_HUMIDITY_STATE_TOPIC] + ) + ) for conf in config.get(CONF_ON_STATE, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) @@ -351,6 +390,7 @@ CLIMATE_CONTROL_ACTION_SCHEMA = cv.Schema( cv.Optional(CONF_TARGET_TEMPERATURE): cv.templatable(cv.temperature), cv.Optional(CONF_TARGET_TEMPERATURE_LOW): cv.templatable(cv.temperature), cv.Optional(CONF_TARGET_TEMPERATURE_HIGH): cv.templatable(cv.temperature), + cv.Optional(CONF_TARGET_HUMIDITY): cv.templatable(cv.percentage_int), cv.Optional(CONF_AWAY): cv.invalid("Use preset instead"), cv.Exclusive(CONF_FAN_MODE, "fan_mode"): cv.templatable( validate_climate_fan_mode @@ -387,6 +427,9 @@ async def climate_control_to_code(config, action_id, template_arg, args): config[CONF_TARGET_TEMPERATURE_HIGH], args, float ) cg.add(var.set_target_temperature_high(template_)) + if CONF_TARGET_HUMIDITY in config: + template_ = await cg.templatable(config[CONF_TARGET_HUMIDITY], args, float) + cg.add(var.set_target_humidity(template_)) if CONF_FAN_MODE in config: template_ = await cg.templatable(config[CONF_FAN_MODE], args, ClimateFanMode) cg.add(var.set_fan_mode(template_)) diff --git a/esphome/components/climate/automation.h b/esphome/components/climate/automation.h index 382871e1e7..a4d13ade58 100644 --- a/esphome/components/climate/automation.h +++ b/esphome/components/climate/automation.h @@ -14,6 +14,7 @@ template class ControlAction : public Action { TEMPLATABLE_VALUE(float, target_temperature) TEMPLATABLE_VALUE(float, target_temperature_low) TEMPLATABLE_VALUE(float, target_temperature_high) + TEMPLATABLE_VALUE(float, target_humidity) TEMPLATABLE_VALUE(bool, away) TEMPLATABLE_VALUE(ClimateFanMode, fan_mode) TEMPLATABLE_VALUE(std::string, custom_fan_mode) @@ -27,6 +28,7 @@ template class ControlAction : public Action { call.set_target_temperature(this->target_temperature_.optional_value(x...)); call.set_target_temperature_low(this->target_temperature_low_.optional_value(x...)); call.set_target_temperature_high(this->target_temperature_high_.optional_value(x...)); + call.set_target_humidity(this->target_humidity_.optional_value(x...)); if (away_.has_value()) { call.set_preset(away_.value(x...) ? CLIMATE_PRESET_AWAY : CLIMATE_PRESET_HOME); } diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index ea24cab954..1822707152 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -45,6 +45,9 @@ void ClimateCall::perform() { if (this->target_temperature_high_.has_value()) { ESP_LOGD(TAG, " Target Temperature High: %.2f", *this->target_temperature_high_); } + if (this->target_humidity_.has_value()) { + ESP_LOGD(TAG, " Target Humidity: %.0f", *this->target_humidity_); + } this->parent_->control(*this); } void ClimateCall::validate_() { @@ -262,10 +265,16 @@ ClimateCall &ClimateCall::set_target_temperature_high(float target_temperature_h this->target_temperature_high_ = target_temperature_high; return *this; } +ClimateCall &ClimateCall::set_target_humidity(float target_humidity) { + this->target_humidity_ = target_humidity; + return *this; +} + const optional &ClimateCall::get_mode() const { return this->mode_; } const optional &ClimateCall::get_target_temperature() const { return this->target_temperature_; } const optional &ClimateCall::get_target_temperature_low() const { return this->target_temperature_low_; } const optional &ClimateCall::get_target_temperature_high() const { return this->target_temperature_high_; } +const optional &ClimateCall::get_target_humidity() const { return this->target_humidity_; } const optional &ClimateCall::get_fan_mode() const { return this->fan_mode_; } const optional &ClimateCall::get_custom_fan_mode() const { return this->custom_fan_mode_; } const optional &ClimateCall::get_preset() const { return this->preset_; } @@ -283,6 +292,10 @@ ClimateCall &ClimateCall::set_target_temperature(optional target_temperat this->target_temperature_ = target_temperature; return *this; } +ClimateCall &ClimateCall::set_target_humidity(optional target_humidity) { + this->target_humidity_ = target_humidity; + return *this; +} ClimateCall &ClimateCall::set_mode(optional mode) { this->mode_ = mode; return *this; @@ -343,6 +356,9 @@ void Climate::save_state_() { } else { state.target_temperature = this->target_temperature; } + if (traits.get_supports_target_humidity()) { + state.target_humidity = this->target_humidity; + } if (traits.get_supports_fan_modes() && fan_mode.has_value()) { state.uses_custom_fan_mode = false; state.fan_mode = this->fan_mode.value(); @@ -408,6 +424,12 @@ void Climate::publish_state() { } else { ESP_LOGD(TAG, " Target Temperature: %.2f°C", this->target_temperature); } + if (traits.get_supports_current_humidity()) { + ESP_LOGD(TAG, " Current Humidity: %.0f%%", this->current_humidity); + } + if (traits.get_supports_target_humidity()) { + ESP_LOGD(TAG, " Target Humidity: %.0f%%", this->target_humidity); + } // Send state to frontend this->state_callback_.call(*this); @@ -427,6 +449,12 @@ ClimateTraits Climate::get_traits() { traits.set_visual_target_temperature_step(*this->visual_target_temperature_step_override_); traits.set_visual_current_temperature_step(*this->visual_current_temperature_step_override_); } + if (this->visual_min_humidity_override_.has_value()) { + traits.set_visual_min_humidity(*this->visual_min_humidity_override_); + } + if (this->visual_max_humidity_override_.has_value()) { + traits.set_visual_max_humidity(*this->visual_max_humidity_override_); + } return traits; } @@ -441,6 +469,12 @@ void Climate::set_visual_temperature_step_override(float target, float current) this->visual_target_temperature_step_override_ = target; this->visual_current_temperature_step_override_ = current; } +void Climate::set_visual_min_humidity_override(float visual_min_humidity_override) { + this->visual_min_humidity_override_ = visual_min_humidity_override; +} +void Climate::set_visual_max_humidity_override(float visual_max_humidity_override) { + this->visual_max_humidity_override_ = visual_max_humidity_override; +} ClimateCall Climate::make_call() { return ClimateCall(this); } @@ -454,6 +488,9 @@ ClimateCall ClimateDeviceRestoreState::to_call(Climate *climate) { } else { call.set_target_temperature(this->target_temperature); } + if (traits.get_supports_target_humidity()) { + call.set_target_humidity(this->target_humidity); + } if (traits.get_supports_fan_modes() || !traits.get_supported_custom_fan_modes().empty()) { call.set_fan_mode(this->fan_mode); } @@ -474,6 +511,9 @@ void ClimateDeviceRestoreState::apply(Climate *climate) { } else { climate->target_temperature = this->target_temperature; } + if (traits.get_supports_target_humidity()) { + climate->target_humidity = this->target_humidity; + } if (traits.get_supports_fan_modes() && !this->uses_custom_fan_mode) { climate->fan_mode = this->fan_mode; } @@ -530,17 +570,25 @@ void Climate::dump_traits_(const char *tag) { auto traits = this->get_traits(); ESP_LOGCONFIG(tag, "ClimateTraits:"); ESP_LOGCONFIG(tag, " [x] Visual settings:"); - ESP_LOGCONFIG(tag, " - Min: %.1f", traits.get_visual_min_temperature()); - ESP_LOGCONFIG(tag, " - Max: %.1f", traits.get_visual_max_temperature()); - ESP_LOGCONFIG(tag, " - Step:"); + ESP_LOGCONFIG(tag, " - Min temperature: %.1f", traits.get_visual_min_temperature()); + ESP_LOGCONFIG(tag, " - Max temperature: %.1f", traits.get_visual_max_temperature()); + ESP_LOGCONFIG(tag, " - Temperature step:"); ESP_LOGCONFIG(tag, " Target: %.1f", traits.get_visual_target_temperature_step()); ESP_LOGCONFIG(tag, " Current: %.1f", traits.get_visual_current_temperature_step()); + ESP_LOGCONFIG(tag, " - Min humidity: %.0f", traits.get_visual_min_humidity()); + ESP_LOGCONFIG(tag, " - Max humidity: %.0f", traits.get_visual_max_humidity()); if (traits.get_supports_current_temperature()) { ESP_LOGCONFIG(tag, " [x] Supports current temperature"); } + if (traits.get_supports_current_humidity()) { + ESP_LOGCONFIG(tag, " [x] Supports current humidity"); + } if (traits.get_supports_two_point_target_temperature()) { ESP_LOGCONFIG(tag, " [x] Supports two-point target temperature"); } + if (traits.get_supports_target_humidity()) { + ESP_LOGCONFIG(tag, " [x] Supports target humidity"); + } if (traits.get_supports_action()) { ESP_LOGCONFIG(tag, " [x] Supports action"); } diff --git a/esphome/components/climate/climate.h b/esphome/components/climate/climate.h index 1bbb17322d..7c2a0b1ed3 100644 --- a/esphome/components/climate/climate.h +++ b/esphome/components/climate/climate.h @@ -64,6 +64,10 @@ class ClimateCall { * For climate devices with two point target temperature control */ ClimateCall &set_target_temperature_high(optional target_temperature_high); + /// Set the target humidity of the climate device. + ClimateCall &set_target_humidity(float target_humidity); + /// Set the target humidity of the climate device. + ClimateCall &set_target_humidity(optional target_humidity); /// Set the fan mode of the climate device. ClimateCall &set_fan_mode(ClimateFanMode fan_mode); /// Set the fan mode of the climate device. @@ -93,6 +97,7 @@ class ClimateCall { const optional &get_target_temperature() const; const optional &get_target_temperature_low() const; const optional &get_target_temperature_high() const; + const optional &get_target_humidity() const; const optional &get_fan_mode() const; const optional &get_swing_mode() const; const optional &get_custom_fan_mode() const; @@ -107,6 +112,7 @@ class ClimateCall { optional target_temperature_; optional target_temperature_low_; optional target_temperature_high_; + optional target_humidity_; optional fan_mode_; optional swing_mode_; optional custom_fan_mode_; @@ -136,6 +142,7 @@ struct ClimateDeviceRestoreState { float target_temperature_high; }; }; + float target_humidity; /// Convert this struct to a climate call that can be performed. ClimateCall to_call(Climate *climate); @@ -164,11 +171,16 @@ class Climate : public EntityBase { /// The active mode of the climate device. ClimateMode mode{CLIMATE_MODE_OFF}; + /// The active state of the climate device. ClimateAction action{CLIMATE_ACTION_OFF}; + /// The current temperature of the climate device, as reported from the integration. float current_temperature{NAN}; + /// The current humidity of the climate device, as reported from the integration. + float current_humidity{NAN}; + union { /// The target temperature of the climate device. float target_temperature; @@ -180,6 +192,9 @@ class Climate : public EntityBase { }; }; + /// The target humidity of the climate device. + float target_humidity; + /// The active fan mode of the climate device. optional fan_mode; @@ -233,6 +248,8 @@ class Climate : public EntityBase { void set_visual_min_temperature_override(float visual_min_temperature_override); void set_visual_max_temperature_override(float visual_max_temperature_override); void set_visual_temperature_step_override(float target, float current); + void set_visual_min_humidity_override(float visual_min_humidity_override); + void set_visual_max_humidity_override(float visual_max_humidity_override); protected: friend ClimateCall; @@ -282,6 +299,8 @@ class Climate : public EntityBase { optional visual_max_temperature_override_{}; optional visual_target_temperature_step_override_{}; optional visual_current_temperature_step_override_{}; + optional visual_min_humidity_override_{}; + optional visual_max_humidity_override_{}; }; } // namespace climate diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index e8c2db6c06..fd5b025a03 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -44,10 +44,18 @@ class ClimateTraits { void set_supports_current_temperature(bool supports_current_temperature) { supports_current_temperature_ = supports_current_temperature; } + bool get_supports_current_humidity() const { return supports_current_humidity_; } + void set_supports_current_humidity(bool supports_current_humidity) { + supports_current_humidity_ = supports_current_humidity; + } bool get_supports_two_point_target_temperature() const { return supports_two_point_target_temperature_; } void set_supports_two_point_target_temperature(bool supports_two_point_target_temperature) { supports_two_point_target_temperature_ = supports_two_point_target_temperature; } + bool get_supports_target_humidity() const { return supports_target_humidity_; } + void set_supports_target_humidity(bool supports_target_humidity) { + supports_target_humidity_ = supports_target_humidity; + } void set_supported_modes(std::set modes) { supported_modes_ = std::move(modes); } void add_supported_mode(ClimateMode mode) { supported_modes_.insert(mode); } ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20") @@ -153,6 +161,11 @@ class ClimateTraits { int8_t get_target_temperature_accuracy_decimals() const; int8_t get_current_temperature_accuracy_decimals() const; + float get_visual_min_humidity() const { return visual_min_humidity_; } + void set_visual_min_humidity(float visual_min_humidity) { visual_min_humidity_ = visual_min_humidity; } + float get_visual_max_humidity() const { return visual_max_humidity_; } + void set_visual_max_humidity(float visual_max_humidity) { visual_max_humidity_ = visual_max_humidity; } + protected: void set_mode_support_(climate::ClimateMode mode, bool supported) { if (supported) { @@ -177,7 +190,9 @@ class ClimateTraits { } bool supports_current_temperature_{false}; + bool supports_current_humidity_{false}; bool supports_two_point_target_temperature_{false}; + bool supports_target_humidity_{false}; std::set supported_modes_ = {climate::CLIMATE_MODE_OFF}; bool supports_action_{false}; std::set supported_fan_modes_; @@ -190,6 +205,8 @@ class ClimateTraits { float visual_max_temperature_{30}; float visual_target_temperature_step_{0.1}; float visual_current_temperature_step_{0.1}; + float visual_min_humidity_{30}; + float visual_max_humidity_{99}; }; } // namespace climate diff --git a/esphome/components/mqtt/mqtt_climate.cpp b/esphome/components/mqtt/mqtt_climate.cpp index 44c490c308..49a8f06734 100644 --- a/esphome/components/mqtt/mqtt_climate.cpp +++ b/esphome/components/mqtt/mqtt_climate.cpp @@ -17,9 +17,12 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo auto traits = this->device_->get_traits(); // current_temperature_topic if (traits.get_supports_current_temperature()) { - // current_temperature_topic root[MQTT_CURRENT_TEMPERATURE_TOPIC] = this->get_current_temperature_state_topic(); } + // current_humidity_topic + if (traits.get_supports_current_humidity()) { + root[MQTT_CURRENT_HUMIDITY_TOPIC] = this->get_current_humidity_state_topic(); + } // mode_command_topic root[MQTT_MODE_COMMAND_TOPIC] = this->get_mode_command_topic(); // mode_state_topic @@ -57,6 +60,13 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo root[MQTT_TEMPERATURE_STATE_TOPIC] = this->get_target_temperature_state_topic(); } + if (traits.get_supports_target_humidity()) { + // target_humidity_command_topic + root[MQTT_TARGET_HUMIDITY_COMMAND_TOPIC] = this->get_target_humidity_command_topic(); + // target_humidity_state_topic + root[MQTT_TARGET_HUMIDITY_STATE_TOPIC] = this->get_target_humidity_state_topic(); + } + // min_temp root[MQTT_MIN_TEMP] = traits.get_visual_min_temperature(); // max_temp @@ -66,6 +76,11 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo // temperature units are always coerced to Celsius internally root[MQTT_TEMPERATURE_UNIT] = "C"; + // min_humidity + root[MQTT_MIN_HUMIDITY] = traits.get_visual_min_humidity(); + // max_humidity + root[MQTT_MAX_HUMIDITY] = traits.get_visual_max_humidity(); + if (traits.get_supports_presets() || !traits.get_supported_custom_presets().empty()) { // preset_mode_command_topic root[MQTT_PRESET_MODE_COMMAND_TOPIC] = this->get_preset_command_topic(); @@ -192,6 +207,20 @@ void MQTTClimateComponent::setup() { }); } + if (traits.get_supports_target_humidity()) { + this->subscribe(this->get_target_humidity_command_topic(), + [this](const std::string &topic, const std::string &payload) { + auto val = parse_number(payload); + if (!val.has_value()) { + ESP_LOGW(TAG, "Can't convert '%s' to number!", payload.c_str()); + return; + } + auto call = this->device_->make_call(); + call.set_target_humidity(*val); + call.perform(); + }); + } + if (traits.get_supports_presets() || !traits.get_supported_custom_presets().empty()) { this->subscribe(this->get_preset_command_topic(), [this](const std::string &topic, const std::string &payload) { auto call = this->device_->make_call(); @@ -273,6 +302,17 @@ bool MQTTClimateComponent::publish_state_() { success = false; } + if (traits.get_supports_current_humidity() && !std::isnan(this->device_->current_humidity)) { + std::string payload = value_accuracy_to_string(this->device_->current_humidity, 0); + if (!this->publish(this->get_current_humidity_state_topic(), payload)) + success = false; + } + if (traits.get_supports_target_humidity() && !std::isnan(this->device_->target_humidity)) { + std::string payload = value_accuracy_to_string(this->device_->target_humidity, 0); + if (!this->publish(this->get_target_humidity_state_topic(), payload)) + success = false; + } + if (traits.get_supports_presets() || !traits.get_supported_custom_presets().empty()) { std::string payload; if (this->device_->preset.has_value()) { diff --git a/esphome/components/mqtt/mqtt_climate.h b/esphome/components/mqtt/mqtt_climate.h index a93070fe66..4e54230e68 100644 --- a/esphome/components/mqtt/mqtt_climate.h +++ b/esphome/components/mqtt/mqtt_climate.h @@ -20,6 +20,7 @@ class MQTTClimateComponent : public mqtt::MQTTComponent { void setup() override; MQTT_COMPONENT_CUSTOM_TOPIC(current_temperature, state) + MQTT_COMPONENT_CUSTOM_TOPIC(current_humidity, state) MQTT_COMPONENT_CUSTOM_TOPIC(mode, state) MQTT_COMPONENT_CUSTOM_TOPIC(mode, command) MQTT_COMPONENT_CUSTOM_TOPIC(target_temperature, state) @@ -28,6 +29,8 @@ class MQTTClimateComponent : public mqtt::MQTTComponent { MQTT_COMPONENT_CUSTOM_TOPIC(target_temperature_low, command) MQTT_COMPONENT_CUSTOM_TOPIC(target_temperature_high, state) MQTT_COMPONENT_CUSTOM_TOPIC(target_temperature_high, command) + MQTT_COMPONENT_CUSTOM_TOPIC(target_humidity, state) + MQTT_COMPONENT_CUSTOM_TOPIC(target_humidity, command) MQTT_COMPONENT_CUSTOM_TOPIC(away, state) MQTT_COMPONENT_CUSTOM_TOPIC(away, command) MQTT_COMPONENT_CUSTOM_TOPIC(action, state) diff --git a/esphome/components/mqtt/mqtt_const.h b/esphome/components/mqtt/mqtt_const.h index 7f74197ab4..3d9e0b4c00 100644 --- a/esphome/components/mqtt/mqtt_const.h +++ b/esphome/components/mqtt/mqtt_const.h @@ -51,6 +51,8 @@ constexpr const char *const MQTT_CODE_ARM_REQUIRED = "cod_arm_req"; constexpr const char *const MQTT_CODE_DISARM_REQUIRED = "cod_dis_req"; constexpr const char *const MQTT_CURRENT_TEMPERATURE_TOPIC = "curr_temp_t"; constexpr const char *const MQTT_CURRENT_TEMPERATURE_TEMPLATE = "curr_temp_tpl"; +constexpr const char *const MQTT_CURRENT_HUMIDITY_TOPIC = "curr_hum_t"; +constexpr const char *const MQTT_CURRENT_HUMIDITY_TEMPLATE = "curr_hum_tpl"; constexpr const char *const MQTT_DEVICE = "dev"; constexpr const char *const MQTT_DEVICE_CLASS = "dev_cla"; constexpr const char *const MQTT_DOCKED_TOPIC = "dock_t"; @@ -305,6 +307,8 @@ constexpr const char *const MQTT_CODE_ARM_REQUIRED = "code_arm_required"; constexpr const char *const MQTT_CODE_DISARM_REQUIRED = "code_disarm_required"; constexpr const char *const MQTT_CURRENT_TEMPERATURE_TOPIC = "current_temperature_topic"; constexpr const char *const MQTT_CURRENT_TEMPERATURE_TEMPLATE = "current_temperature_template"; +constexpr const char *const MQTT_CURRENT_HUMIDITY_TOPIC = "current_humidity_topic"; +constexpr const char *const MQTT_CURRENT_HUMIDITY_TEMPLATE = "current_humidity_template"; constexpr const char *const MQTT_DEVICE = "device"; constexpr const char *const MQTT_DEVICE_CLASS = "device_class"; constexpr const char *const MQTT_DOCKED_TOPIC = "docked_topic"; diff --git a/esphome/components/pid/climate.py b/esphome/components/pid/climate.py index 7cd414f912..2c4ef688a5 100644 --- a/esphome/components/pid/climate.py +++ b/esphome/components/pid/climate.py @@ -2,7 +2,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import automation from esphome.components import climate, sensor, output -from esphome.const import CONF_ID, CONF_SENSOR +from esphome.const import CONF_HUMIDITY_SENSOR, CONF_ID, CONF_SENSOR pid_ns = cg.esphome_ns.namespace("pid") PIDClimate = pid_ns.class_("PIDClimate", climate.Climate, cg.Component) @@ -45,6 +45,7 @@ CONFIG_SCHEMA = cv.All( { cv.GenerateID(): cv.declare_id(PIDClimate), cv.Required(CONF_SENSOR): cv.use_id(sensor.Sensor), + cv.Optional(CONF_HUMIDITY_SENSOR): cv.use_id(sensor.Sensor), cv.Required(CONF_DEFAULT_TARGET_TEMPERATURE): cv.temperature, cv.Optional(CONF_COOL_OUTPUT): cv.use_id(output.FloatOutput), cv.Optional(CONF_HEAT_OUTPUT): cv.use_id(output.FloatOutput), @@ -86,6 +87,10 @@ async def to_code(config): sens = await cg.get_variable(config[CONF_SENSOR]) cg.add(var.set_sensor(sens)) + if CONF_HUMIDITY_SENSOR in config: + sens = await cg.get_variable(config[CONF_HUMIDITY_SENSOR]) + cg.add(var.set_humidity_sensor(sens)) + if CONF_COOL_OUTPUT in config: out = await cg.get_variable(config[CONF_COOL_OUTPUT]) cg.add(var.set_cool_output(out)) diff --git a/esphome/components/pid/pid_climate.cpp b/esphome/components/pid/pid_climate.cpp index dab4502d40..93b6999a00 100644 --- a/esphome/components/pid/pid_climate.cpp +++ b/esphome/components/pid/pid_climate.cpp @@ -14,6 +14,16 @@ void PIDClimate::setup() { this->update_pid_(); }); this->current_temperature = this->sensor_->state; + + // register for humidity values and get initial state + if (this->humidity_sensor_ != nullptr) { + this->humidity_sensor_->add_on_state_callback([this](float state) { + this->current_humidity = state; + this->publish_state(); + }); + this->current_humidity = this->humidity_sensor_->state; + } + // restore set points auto restore = this->restore_state_(); if (restore.has_value()) { @@ -47,6 +57,9 @@ climate::ClimateTraits PIDClimate::traits() { traits.set_supports_current_temperature(true); traits.set_supports_two_point_target_temperature(false); + if (this->humidity_sensor_ != nullptr) + traits.set_supports_current_humidity(true); + traits.set_supported_modes({climate::CLIMATE_MODE_OFF}); if (supports_cool_()) traits.add_supported_mode(climate::CLIMATE_MODE_COOL); diff --git a/esphome/components/pid/pid_climate.h b/esphome/components/pid/pid_climate.h index da57209a7e..5ae97ee10b 100644 --- a/esphome/components/pid/pid_climate.h +++ b/esphome/components/pid/pid_climate.h @@ -19,6 +19,7 @@ class PIDClimate : public climate::Climate, public Component { void dump_config() override; void set_sensor(sensor::Sensor *sensor) { sensor_ = sensor; } + void set_humidity_sensor(sensor::Sensor *sensor) { humidity_sensor_ = sensor; } void set_cool_output(output::FloatOutput *cool_output) { cool_output_ = cool_output; } void set_heat_output(output::FloatOutput *heat_output) { heat_output_ = heat_output; } void set_kp(float kp) { controller_.kp_ = kp; } @@ -85,6 +86,8 @@ class PIDClimate : public climate::Climate, public Component { /// The sensor used for getting the current temperature sensor::Sensor *sensor_; + /// The sensor used for getting the current humidity + sensor::Sensor *humidity_sensor_{nullptr}; output::FloatOutput *cool_output_{nullptr}; output::FloatOutput *heat_output_{nullptr}; PIDController controller_; diff --git a/esphome/components/thermostat/climate.py b/esphome/components/thermostat/climate.py index cca46609db..89d6b13376 100644 --- a/esphome/components/thermostat/climate.py +++ b/esphome/components/thermostat/climate.py @@ -35,6 +35,7 @@ from esphome.const import ( CONF_HEAT_DEADBAND, CONF_HEAT_MODE, CONF_HEAT_OVERRUN, + CONF_HUMIDITY_SENSOR, CONF_ID, CONF_IDLE_ACTION, CONF_MAX_COOLING_RUN_TIME, @@ -519,6 +520,7 @@ CONFIG_SCHEMA = cv.All( { cv.GenerateID(): cv.declare_id(ThermostatClimate), cv.Required(CONF_SENSOR): cv.use_id(sensor.Sensor), + cv.Optional(CONF_HUMIDITY_SENSOR): cv.use_id(sensor.Sensor), cv.Required(CONF_IDLE_ACTION): automation.validate_automation(single=True), cv.Optional(CONF_COOL_ACTION): automation.validate_automation(single=True), cv.Optional( @@ -658,6 +660,10 @@ async def to_code(config): ) cg.add(var.set_sensor(sens)) + if CONF_HUMIDITY_SENSOR in config: + sens = await cg.get_variable(config[CONF_HUMIDITY_SENSOR]) + cg.add(var.set_humidity_sensor(sens)) + cg.add(var.set_cool_deadband(config[CONF_COOL_DEADBAND])) cg.add(var.set_cool_overrun(config[CONF_COOL_OVERRUN])) cg.add(var.set_heat_deadband(config[CONF_HEAT_DEADBAND])) diff --git a/esphome/components/thermostat/thermostat_climate.cpp b/esphome/components/thermostat/thermostat_climate.cpp index 73b061b07c..40a29295f1 100644 --- a/esphome/components/thermostat/thermostat_climate.cpp +++ b/esphome/components/thermostat/thermostat_climate.cpp @@ -27,6 +27,15 @@ void ThermostatClimate::setup() { }); this->current_temperature = this->sensor_->state; + // register for humidity values and get initial state + if (this->humidity_sensor_ != nullptr) { + this->humidity_sensor_->add_on_state_callback([this](float state) { + this->current_humidity = state; + this->publish_state(); + }); + this->current_humidity = this->humidity_sensor_->state; + } + auto use_default_preset = true; if (this->on_boot_restore_from_ == thermostat::OnBootRestoreFrom::MEMORY) { @@ -217,6 +226,9 @@ void ThermostatClimate::control(const climate::ClimateCall &call) { climate::ClimateTraits ThermostatClimate::traits() { auto traits = climate::ClimateTraits(); traits.set_supports_current_temperature(true); + if (this->humidity_sensor_ != nullptr) + traits.set_supports_current_humidity(true); + if (supports_auto_) traits.add_supported_mode(climate::CLIMATE_MODE_AUTO); if (supports_heat_cool_) @@ -1169,6 +1181,9 @@ void ThermostatClimate::set_idle_minimum_time_in_sec(uint32_t time) { 1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); } void ThermostatClimate::set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; } +void ThermostatClimate::set_humidity_sensor(sensor::Sensor *humidity_sensor) { + this->humidity_sensor_ = humidity_sensor; +} void ThermostatClimate::set_use_startup_delay(bool use_startup_delay) { this->use_startup_delay_ = use_startup_delay; } void ThermostatClimate::set_supports_heat_cool(bool supports_heat_cool) { this->supports_heat_cool_ = supports_heat_cool; diff --git a/esphome/components/thermostat/thermostat_climate.h b/esphome/components/thermostat/thermostat_climate.h index 677b4ad324..559812a94f 100644 --- a/esphome/components/thermostat/thermostat_climate.h +++ b/esphome/components/thermostat/thermostat_climate.h @@ -81,6 +81,7 @@ class ThermostatClimate : public climate::Climate, public Component { void set_heating_minimum_run_time_in_sec(uint32_t time); void set_idle_minimum_time_in_sec(uint32_t time); void set_sensor(sensor::Sensor *sensor); + void set_humidity_sensor(sensor::Sensor *humidity_sensor); void set_use_startup_delay(bool use_startup_delay); void set_supports_auto(bool supports_auto); void set_supports_heat_cool(bool supports_heat_cool); @@ -238,6 +239,8 @@ class ThermostatClimate : public climate::Climate, public Component { /// The sensor used for getting the current temperature sensor::Sensor *sensor_{nullptr}; + /// The sensor used for getting the current humidity + sensor::Sensor *humidity_sensor_{nullptr}; /// Whether the controller supports auto/cooling/drying/fanning/heating. /// diff --git a/esphome/const.py b/esphome/const.py index 1aabe81db3..101a690db5 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -157,6 +157,7 @@ CONF_CS_PIN = "cs_pin" CONF_CSS_INCLUDE = "css_include" CONF_CSS_URL = "css_url" CONF_CURRENT = "current" +CONF_CURRENT_HUMIDITY_STATE_TOPIC = "current_humidity_state_topic" CONF_CURRENT_OPERATION = "current_operation" CONF_CURRENT_RESISTOR = "current_resistor" CONF_CURRENT_TEMPERATURE_STATE_TOPIC = "current_temperature_state_topic" @@ -324,6 +325,7 @@ CONF_HIGH_VOLTAGE_REFERENCE = "high_voltage_reference" CONF_HOUR = "hour" CONF_HOURS = "hours" CONF_HUMIDITY = "humidity" +CONF_HUMIDITY_SENSOR = "humidity_sensor" CONF_HYSTERESIS = "hysteresis" CONF_I2C = "i2c" CONF_I2C_ID = "i2c_id" @@ -758,6 +760,8 @@ CONF_SYNC = "sync" CONF_TABLET = "tablet" CONF_TAG = "tag" CONF_TARGET = "target" +CONF_TARGET_HUMIDITY_COMMAND_TOPIC = "target_humidity_command_topic" +CONF_TARGET_HUMIDITY_STATE_TOPIC = "target_humidity_state_topic" CONF_TARGET_TEMPERATURE = "target_temperature" CONF_TARGET_TEMPERATURE_CHANGE_ACTION = "target_temperature_change_action" CONF_TARGET_TEMPERATURE_COMMAND_TOPIC = "target_temperature_command_topic" diff --git a/tests/test3.yaml b/tests/test3.yaml index 6b1757c2ad..ab7f38d07f 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -899,6 +899,7 @@ climate: - platform: bang_bang name: Bang Bang Climate sensor: ha_hello_world + humidity_sensor: ha_hello_world default_target_temperature_low: 18°C default_target_temperature_high: 24°C idle_action: @@ -913,6 +914,7 @@ climate: - platform: thermostat name: Thermostat Climate sensor: ha_hello_world + humidity_sensor: ha_hello_world preset: - name: Default Preset default_target_temperature_low: 18°C @@ -1000,6 +1002,7 @@ climate: id: pid_climate name: PID Climate Controller sensor: ha_hello_world + humidity_sensor: ha_hello_world default_target_temperature: 21°C heat_output: my_slow_pwm control_parameters: From 2fcc5b3ef212c6be62a299b50274d8a48d169ae7 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 13 Dec 2023 10:46:11 +0900 Subject: [PATCH 73/89] Remove lingering note (#5916) --- CONTRIBUTING.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ec23656763..1c92d91159 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,5 +10,3 @@ Things to note when contributing: for more information. - Please also update the tests in the `tests/` folder. You can do so by just adding a line in one of the YAML files which checks if your new feature compiles correctly. - - Sometimes I will let pull requests linger because I'm not 100% sure about them. Please feel free to ping - me after some time. From f1f8689462db94fb42e1440795d9bb68b7c2ceac Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 13 Dec 2023 11:47:46 +0900 Subject: [PATCH 74/89] Fix typo added in esp32 post_build filename (#5918) --- esphome/components/esp32/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index fd5e9377dd..307a3e76e5 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -462,7 +462,7 @@ async def to_code(config): add_extra_script( "post", - "post_build2.py", + "post_build.py", os.path.join(os.path.dirname(__file__), "post_build.py.script"), ) From 058c43e95319b2dcc544c807c786d859ed8384f7 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 13 Dec 2023 11:51:07 +0900 Subject: [PATCH 75/89] Copy esp32 custom partition files to build folder (#5919) --- esphome/components/esp32/__init__.py | 33 +++++++++++++++------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 307a3e76e5..5d17633975 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -497,10 +497,11 @@ async def to_code(config): add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0", False) add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1", False) + cg.add_platformio_option("board_build.partitions", "partitions.csv") if CONF_PARTITIONS in config: - cg.add_platformio_option("board_build.partitions", config[CONF_PARTITIONS]) - else: - cg.add_platformio_option("board_build.partitions", "partitions.csv") + add_extra_build_file( + "partitions.csv", CORE.relative_config_path(config[CONF_PARTITIONS]) + ) for name, value in conf[CONF_SDKCONFIG_OPTIONS].items(): add_idf_sdkconfig_option(name, RawSdkconfigValue(value)) @@ -639,20 +640,22 @@ def _write_sdkconfig(): # Called by writer.py def copy_files(): if CORE.using_arduino: - write_file_if_changed( - CORE.relative_build_path("partitions.csv"), - get_arduino_partition_csv( - CORE.platformio_options.get("board_upload.flash_size") - ), - ) + if "partitions.csv" not in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES]: + write_file_if_changed( + CORE.relative_build_path("partitions.csv"), + get_arduino_partition_csv( + CORE.platformio_options.get("board_upload.flash_size") + ), + ) if CORE.using_esp_idf: _write_sdkconfig() - write_file_if_changed( - CORE.relative_build_path("partitions.csv"), - get_idf_partition_csv( - CORE.platformio_options.get("board_upload.flash_size") - ), - ) + if "partitions.csv" not in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES]: + write_file_if_changed( + CORE.relative_build_path("partitions.csv"), + get_idf_partition_csv( + CORE.platformio_options.get("board_upload.flash_size") + ), + ) # IDF build scripts look for version string to put in the build. # However, if the build path does not have an initialized git repo, # and no version.txt file exists, the CMake script fails for some setups. From 9daaadb3b68aaa1fabe1ecf67acd394053da883e Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Wed, 13 Dec 2023 08:29:05 +0100 Subject: [PATCH 76/89] UART change at runtime (#5909) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- .../uart/uart_component_esp32_arduino.cpp | 9 +++++++++ .../uart/uart_component_esp32_arduino.h | 14 ++++++++++++++ .../components/uart/uart_component_esp_idf.cpp | 16 ++++++++++++++-- esphome/components/uart/uart_component_esp_idf.h | 14 ++++++++++++++ 4 files changed, 51 insertions(+), 2 deletions(-) diff --git a/esphome/components/uart/uart_component_esp32_arduino.cpp b/esphome/components/uart/uart_component_esp32_arduino.cpp index 7306dd2f31..75b67bf5c2 100644 --- a/esphome/components/uart/uart_component_esp32_arduino.cpp +++ b/esphome/components/uart/uart_component_esp32_arduino.cpp @@ -109,6 +109,11 @@ void ESP32ArduinoUARTComponent::setup() { this->number_ = next_uart_num; this->hw_serial_ = new HardwareSerial(next_uart_num++); // NOLINT(cppcoreguidelines-owning-memory) } + + this->load_settings(false); +} + +void ESP32ArduinoUARTComponent::load_settings(bool dump_config) { int8_t tx = this->tx_pin_ != nullptr ? this->tx_pin_->get_pin() : -1; int8_t rx = this->rx_pin_ != nullptr ? this->rx_pin_->get_pin() : -1; bool invert = false; @@ -118,6 +123,10 @@ void ESP32ArduinoUARTComponent::setup() { invert = true; this->hw_serial_->setRxBufferSize(this->rx_buffer_size_); this->hw_serial_->begin(this->baud_rate_, get_config(), rx, tx, invert); + if (dump_config) { + ESP_LOGCONFIG(TAG, "UART %u was reloaded.", this->number_); + this->dump_config(); + } } void ESP32ArduinoUARTComponent::dump_config() { diff --git a/esphome/components/uart/uart_component_esp32_arduino.h b/esphome/components/uart/uart_component_esp32_arduino.h index 02dfd0531e..ec4953a598 100644 --- a/esphome/components/uart/uart_component_esp32_arduino.h +++ b/esphome/components/uart/uart_component_esp32_arduino.h @@ -32,6 +32,20 @@ class ESP32ArduinoUARTComponent : public UARTComponent, public Component { HardwareSerial *get_hw_serial() { return this->hw_serial_; } uint8_t get_hw_serial_number() { return this->number_; } + /** + * Load the UART with the current settings. + * @param dump_config (Optional, default `true`): True for displaying new settings or + * false to change it quitely + * + * Example: + * ```cpp + * id(uart1).load_settings(); + * ``` + * + * This will load the current UART interface with the latest settings (baud_rate, parity, etc). + */ + void load_settings(bool dump_config = true); + protected: void check_logger_conflict() override; diff --git a/esphome/components/uart/uart_component_esp_idf.cpp b/esphome/components/uart/uart_component_esp_idf.cpp index c78626fa26..2dd6ab105f 100644 --- a/esphome/components/uart/uart_component_esp_idf.cpp +++ b/esphome/components/uart/uart_component_esp_idf.cpp @@ -122,9 +122,21 @@ void IDFUARTComponent::setup() { xSemaphoreGive(this->lock_); } +void IDFUARTComponent::load_settings(bool dump_config) { + uart_config_t uart_config = this->get_config_(); + esp_err_t err = uart_param_config(this->uart_num_, &uart_config); + if (err != ESP_OK) { + ESP_LOGW(TAG, "uart_param_config failed: %s", esp_err_to_name(err)); + this->mark_failed(); + return; + } else if (dump_config) { + ESP_LOGCONFIG(TAG, "UART %u was reloaded.", this->uart_num_); + this->dump_config(); + } +} + void IDFUARTComponent::dump_config() { - ESP_LOGCONFIG(TAG, "UART Bus:"); - ESP_LOGCONFIG(TAG, " Number: %u", this->uart_num_); + ESP_LOGCONFIG(TAG, "UART Bus %u:", this->uart_num_); LOG_PIN(" TX Pin: ", tx_pin_); LOG_PIN(" RX Pin: ", rx_pin_); if (this->rx_pin_ != nullptr) { diff --git a/esphome/components/uart/uart_component_esp_idf.h b/esphome/components/uart/uart_component_esp_idf.h index fdaa4da9a7..068ebd32dc 100644 --- a/esphome/components/uart/uart_component_esp_idf.h +++ b/esphome/components/uart/uart_component_esp_idf.h @@ -26,6 +26,20 @@ class IDFUARTComponent : public UARTComponent, public Component { uint8_t get_hw_serial_number() { return this->uart_num_; } QueueHandle_t *get_uart_event_queue() { return &this->uart_event_queue_; } + /** + * Load the UART with the current settings. + * @param dump_config (Optional, default `true`): True for displaying new settings or + * false to change it quitely + * + * Example: + * ```cpp + * id(uart1).load_settings(); + * ``` + * + * This will load the current UART interface with the latest settings (baud_rate, parity, etc). + */ + void load_settings(bool dump_config = true); + protected: void check_logger_conflict() override; uart_port_t uart_num_; From f355972c9d8f43b3a64849d698da1163d41325dc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 13 Dec 2023 09:57:50 -1000 Subject: [PATCH 77/89] Bump aioesphomeapi from 20.1.0 to 21.0.0 (#5922) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 0d08da2b45..f330ecbf3e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ platformio==6.1.11 # When updating platformio, also update Dockerfile esptool==4.6.2 click==8.1.7 esphome-dashboard==20231107.0 -aioesphomeapi==20.1.0 +aioesphomeapi==21.0.0 zeroconf==0.128.4 python-magic==0.4.27 From 9a8bc9484d99c47d207d1092d43ce81121c6e107 Mon Sep 17 00:00:00 2001 From: Jacob Masen-Smith Date: Wed, 13 Dec 2023 12:33:24 -0800 Subject: [PATCH 78/89] Fix the initial run of lambda light effects (#5921) The timer used for `millis()` is a monotonic timer based on the last start time of the device. If, for some reason, you pick a long `update_interval` and try to apply it as soon as you start the device, nothing happens because the device hasn't been on for longer than the `update_interval` --- esphome/components/light/addressable_light_effect.h | 2 +- esphome/components/light/base_light_effects.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/light/addressable_light_effect.h b/esphome/components/light/addressable_light_effect.h index 0482cf53b9..c2109b2d23 100644 --- a/esphome/components/light/addressable_light_effect.h +++ b/esphome/components/light/addressable_light_effect.h @@ -57,7 +57,7 @@ class AddressableLambdaLightEffect : public AddressableLightEffect { void start() override { this->initial_run_ = true; } void apply(AddressableLight &it, const Color ¤t_color) override { const uint32_t now = millis(); - if (now - this->last_run_ >= this->update_interval_) { + if (now - this->last_run_ >= this->update_interval_ || this->initial_run_) { this->last_run_ = now; this->f_(it, current_color, this->initial_run_); this->initial_run_ = false; diff --git a/esphome/components/light/base_light_effects.h b/esphome/components/light/base_light_effects.h index d126e4960c..c62ca43ca1 100644 --- a/esphome/components/light/base_light_effects.h +++ b/esphome/components/light/base_light_effects.h @@ -118,7 +118,7 @@ class LambdaLightEffect : public LightEffect { void start() override { this->initial_run_ = true; } void apply() override { const uint32_t now = millis(); - if (now - this->last_run_ >= this->update_interval_) { + if (now - this->last_run_ >= this->update_interval_ || this->initial_run_) { this->last_run_ = now; this->f_(this->initial_run_); this->initial_run_ = false; From 81aa48a5f346f8127b6a50563e045ebd9af90eb3 Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Wed, 13 Dec 2023 23:45:08 +0100 Subject: [PATCH 79/89] Exposes `load_settings` to `UARTComponent` class (#5920) --- esphome/components/uart/uart_component.h | 4 ++++ esphome/components/uart/uart_component_esp32_arduino.h | 3 ++- esphome/components/uart/uart_component_esp_idf.h | 3 ++- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/esphome/components/uart/uart_component.h b/esphome/components/uart/uart_component.h index 42702cf5b8..34bda42bb5 100644 --- a/esphome/components/uart/uart_component.h +++ b/esphome/components/uart/uart_component.h @@ -62,6 +62,10 @@ class UARTComponent { UARTParityOptions get_parity() const { return this->parity_; } void set_baud_rate(uint32_t baud_rate) { baud_rate_ = baud_rate; } uint32_t get_baud_rate() const { return baud_rate_; } +#ifdef USE_ESP32 + virtual void load_settings() = 0; + virtual void load_settings(bool dump_config) = 0; +#endif // USE_ESP32 #ifdef USE_UART_DEBUGGER void add_debug_callback(std::function &&callback) { diff --git a/esphome/components/uart/uart_component_esp32_arduino.h b/esphome/components/uart/uart_component_esp32_arduino.h index ec4953a598..de17d9718b 100644 --- a/esphome/components/uart/uart_component_esp32_arduino.h +++ b/esphome/components/uart/uart_component_esp32_arduino.h @@ -44,7 +44,8 @@ class ESP32ArduinoUARTComponent : public UARTComponent, public Component { * * This will load the current UART interface with the latest settings (baud_rate, parity, etc). */ - void load_settings(bool dump_config = true); + void load_settings(bool dump_config) override; + void load_settings() override { this->load_settings(true); } protected: void check_logger_conflict() override; diff --git a/esphome/components/uart/uart_component_esp_idf.h b/esphome/components/uart/uart_component_esp_idf.h index 068ebd32dc..215641ebe2 100644 --- a/esphome/components/uart/uart_component_esp_idf.h +++ b/esphome/components/uart/uart_component_esp_idf.h @@ -38,7 +38,8 @@ class IDFUARTComponent : public UARTComponent, public Component { * * This will load the current UART interface with the latest settings (baud_rate, parity, etc). */ - void load_settings(bool dump_config = true); + void load_settings(bool dump_config) override; + void load_settings() override { this->load_settings(true); } protected: void check_logger_conflict() override; From 6fd239362d768e4e9ec533e59ef453bc3cab1988 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Wed, 13 Dec 2023 16:54:55 -0600 Subject: [PATCH 80/89] Add support for PN7160 (#5486) --- CODEOWNERS | 3 + esphome/components/nfc/nci_core.h | 144 ++ esphome/components/nfc/nci_message.cpp | 166 +++ esphome/components/nfc/nci_message.h | 50 + esphome/components/nfc/nfc.cpp | 2 +- esphome/components/nfc/nfc_helpers.cpp | 47 + esphome/components/nfc/nfc_helpers.h | 17 + esphome/components/pn7160/__init__.py | 227 ++++ esphome/components/pn7160/automation.h | 82 ++ esphome/components/pn7160/pn7160.cpp | 1161 +++++++++++++++++ esphome/components/pn7160/pn7160.h | 315 +++++ .../pn7160/pn7160_mifare_classic.cpp | 322 +++++ .../pn7160/pn7160_mifare_ultralight.cpp | 186 +++ esphome/components/pn7160_i2c/__init__.py | 25 + esphome/components/pn7160_i2c/pn7160_i2c.cpp | 49 + esphome/components/pn7160_i2c/pn7160_i2c.h | 22 + esphome/components/pn7160_spi/__init__.py | 26 + esphome/components/pn7160_spi/pn7160_spi.cpp | 54 + esphome/components/pn7160_spi/pn7160_spi.h | 30 + tests/test1.yaml | 37 + 20 files changed, 2964 insertions(+), 1 deletion(-) create mode 100644 esphome/components/nfc/nci_core.h create mode 100644 esphome/components/nfc/nci_message.cpp create mode 100644 esphome/components/nfc/nci_message.h create mode 100644 esphome/components/nfc/nfc_helpers.cpp create mode 100644 esphome/components/nfc/nfc_helpers.h create mode 100644 esphome/components/pn7160/__init__.py create mode 100644 esphome/components/pn7160/automation.h create mode 100644 esphome/components/pn7160/pn7160.cpp create mode 100644 esphome/components/pn7160/pn7160.h create mode 100644 esphome/components/pn7160/pn7160_mifare_classic.cpp create mode 100644 esphome/components/pn7160/pn7160_mifare_ultralight.cpp create mode 100644 esphome/components/pn7160_i2c/__init__.py create mode 100644 esphome/components/pn7160_i2c/pn7160_i2c.cpp create mode 100644 esphome/components/pn7160_i2c/pn7160_i2c.h create mode 100644 esphome/components/pn7160_spi/__init__.py create mode 100644 esphome/components/pn7160_spi/pn7160_spi.cpp create mode 100644 esphome/components/pn7160_spi/pn7160_spi.h diff --git a/CODEOWNERS b/CODEOWNERS index d509c98433..2269d580e4 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -240,6 +240,9 @@ esphome/components/pmwcs3/* @SeByDocKy esphome/components/pn532/* @OttoWinter @jesserockz esphome/components/pn532_i2c/* @OttoWinter @jesserockz esphome/components/pn532_spi/* @OttoWinter @jesserockz +esphome/components/pn7160/* @jesserockz @kbx81 +esphome/components/pn7160_i2c/* @jesserockz @kbx81 +esphome/components/pn7160_spi/* @jesserockz @kbx81 esphome/components/power_supply/* @esphome/core esphome/components/preferences/* @esphome/core esphome/components/psram/* @esphome/core diff --git a/esphome/components/nfc/nci_core.h b/esphome/components/nfc/nci_core.h new file mode 100644 index 0000000000..fdaf6d0cc5 --- /dev/null +++ b/esphome/components/nfc/nci_core.h @@ -0,0 +1,144 @@ +#pragma once + +#include "esphome/core/helpers.h" + +#include + +namespace esphome { +namespace nfc { + +// Header info +static const uint8_t NCI_PKT_HEADER_SIZE = 3; // NCI packet (pkt) headers are always three bytes +static const uint8_t NCI_PKT_MT_GID_OFFSET = 0; // NCI packet (pkt) MT and GID offsets +static const uint8_t NCI_PKT_OID_OFFSET = 1; // NCI packet (pkt) OID offset +static const uint8_t NCI_PKT_LENGTH_OFFSET = 2; // NCI packet (pkt) message length (size) offset +static const uint8_t NCI_PKT_PAYLOAD_OFFSET = 3; // NCI packet (pkt) payload offset +// Important masks +static const uint8_t NCI_PKT_MT_MASK = 0xE0; // NCI packet (pkt) message type mask +static const uint8_t NCI_PKT_PBF_MASK = 0x10; // packet boundary flag bit +static const uint8_t NCI_PKT_GID_MASK = 0x0F; +static const uint8_t NCI_PKT_OID_MASK = 0x3F; +// Message types +static const uint8_t NCI_PKT_MT_DATA = 0x00; // For sending commands to NFC endpoint (card/tag) +static const uint8_t NCI_PKT_MT_CTRL_COMMAND = 0x20; // For sending commands to NFCC +static const uint8_t NCI_PKT_MT_CTRL_RESPONSE = 0x40; // Response from NFCC to commands +static const uint8_t NCI_PKT_MT_CTRL_NOTIFICATION = 0x60; // Notification from NFCC +// GIDs +static const uint8_t NCI_CORE_GID = 0x0; +static const uint8_t RF_GID = 0x1; +static const uint8_t NFCEE_GID = 0x1; +static const uint8_t NCI_PROPRIETARY_GID = 0xF; +// OIDs +static const uint8_t NCI_CORE_RESET_OID = 0x00; +static const uint8_t NCI_CORE_INIT_OID = 0x01; +static const uint8_t NCI_CORE_SET_CONFIG_OID = 0x02; +static const uint8_t NCI_CORE_GET_CONFIG_OID = 0x03; +static const uint8_t NCI_CORE_CONN_CREATE_OID = 0x04; +static const uint8_t NCI_CORE_CONN_CLOSE_OID = 0x05; +static const uint8_t NCI_CORE_CONN_CREDITS_OID = 0x06; +static const uint8_t NCI_CORE_GENERIC_ERROR_OID = 0x07; +static const uint8_t NCI_CORE_INTERFACE_ERROR_OID = 0x08; + +static const uint8_t RF_DISCOVER_MAP_OID = 0x00; +static const uint8_t RF_SET_LISTEN_MODE_ROUTING_OID = 0x01; +static const uint8_t RF_GET_LISTEN_MODE_ROUTING_OID = 0x02; +static const uint8_t RF_DISCOVER_OID = 0x03; +static const uint8_t RF_DISCOVER_SELECT_OID = 0x04; +static const uint8_t RF_INTF_ACTIVATED_OID = 0x05; +static const uint8_t RF_DEACTIVATE_OID = 0x06; +static const uint8_t RF_FIELD_INFO_OID = 0x07; +static const uint8_t RF_T3T_POLLING_OID = 0x08; +static const uint8_t RF_NFCEE_ACTION_OID = 0x09; +static const uint8_t RF_NFCEE_DISCOVERY_REQ_OID = 0x0A; +static const uint8_t RF_PARAMETER_UPDATE_OID = 0x0B; + +static const uint8_t NFCEE_DISCOVER_OID = 0x00; +static const uint8_t NFCEE_MODE_SET_OID = 0x01; +// Interfaces +static const uint8_t INTF_NFCEE_DIRECT = 0x00; +static const uint8_t INTF_FRAME = 0x01; +static const uint8_t INTF_ISODEP = 0x02; +static const uint8_t INTF_NFCDEP = 0x03; +static const uint8_t INTF_TAGCMD = 0x80; // NXP proprietary +// Bit rates +static const uint8_t NFC_BIT_RATE_106 = 0x00; +static const uint8_t NFC_BIT_RATE_212 = 0x01; +static const uint8_t NFC_BIT_RATE_424 = 0x02; +static const uint8_t NFC_BIT_RATE_848 = 0x03; +static const uint8_t NFC_BIT_RATE_1695 = 0x04; +static const uint8_t NFC_BIT_RATE_3390 = 0x05; +static const uint8_t NFC_BIT_RATE_6780 = 0x06; +// Protocols +static const uint8_t PROT_UNDETERMINED = 0x00; +static const uint8_t PROT_T1T = 0x01; +static const uint8_t PROT_T2T = 0x02; +static const uint8_t PROT_T3T = 0x03; +static const uint8_t PROT_ISODEP = 0x04; +static const uint8_t PROT_NFCDEP = 0x05; +static const uint8_t PROT_T5T = 0x06; +static const uint8_t PROT_MIFARE = 0x80; +// RF Technologies +static const uint8_t NFC_RF_TECH_A = 0x00; +static const uint8_t NFC_RF_TECH_B = 0x01; +static const uint8_t NFC_RF_TECH_F = 0x02; +static const uint8_t NFC_RF_TECH_15693 = 0x03; +// RF Technology & Modes +static const uint8_t MODE_MASK = 0xF0; +static const uint8_t MODE_LISTEN_MASK = 0x80; +static const uint8_t MODE_POLL = 0x00; + +static const uint8_t TECH_PASSIVE_NFCA = 0x00; +static const uint8_t TECH_PASSIVE_NFCB = 0x01; +static const uint8_t TECH_PASSIVE_NFCF = 0x02; +static const uint8_t TECH_ACTIVE_NFCA = 0x03; +static const uint8_t TECH_ACTIVE_NFCF = 0x05; +static const uint8_t TECH_PASSIVE_15693 = 0x06; +// Status codes +static const uint8_t STATUS_OK = 0x00; +static const uint8_t STATUS_REJECTED = 0x01; +static const uint8_t STATUS_RF_FRAME_CORRUPTED = 0x02; +static const uint8_t STATUS_FAILED = 0x03; +static const uint8_t STATUS_NOT_INITIALIZED = 0x04; +static const uint8_t STATUS_SYNTAX_ERROR = 0x05; +static const uint8_t STATUS_SEMANTIC_ERROR = 0x06; +static const uint8_t STATUS_INVALID_PARAM = 0x09; +static const uint8_t STATUS_MESSAGE_SIZE_EXCEEDED = 0x0A; +static const uint8_t DISCOVERY_ALREADY_STARTED = 0xA0; +static const uint8_t DISCOVERY_TARGET_ACTIVATION_FAILED = 0xA1; +static const uint8_t DISCOVERY_TEAR_DOWN = 0xA2; +static const uint8_t RF_TRANSMISSION_ERROR = 0xB0; +static const uint8_t RF_PROTOCOL_ERROR = 0xB1; +static const uint8_t RF_TIMEOUT_ERROR = 0xB2; +static const uint8_t NFCEE_INTERFACE_ACTIVATION_FAILED = 0xC0; +static const uint8_t NFCEE_TRANSMISSION_ERROR = 0xC1; +static const uint8_t NFCEE_PROTOCOL_ERROR = 0xC2; +static const uint8_t NFCEE_TIMEOUT_ERROR = 0xC3; +// Deactivation types/reasons +static const uint8_t DEACTIVATION_TYPE_IDLE = 0x00; +static const uint8_t DEACTIVATION_TYPE_SLEEP = 0x01; +static const uint8_t DEACTIVATION_TYPE_SLEEP_AF = 0x02; +static const uint8_t DEACTIVATION_TYPE_DISCOVERY = 0x03; +// RF discover map modes +static const uint8_t RF_DISCOVER_MAP_MODE_POLL = 0x1; +static const uint8_t RF_DISCOVER_MAP_MODE_LISTEN = 0x2; +// RF discover notification types +static const uint8_t RF_DISCOVER_NTF_NT_LAST = 0x00; +static const uint8_t RF_DISCOVER_NTF_NT_LAST_RL = 0x01; +static const uint8_t RF_DISCOVER_NTF_NT_MORE = 0x02; +// Important message offsets +static const uint8_t RF_DISCOVER_NTF_DISCOVERY_ID = 0 + NCI_PKT_HEADER_SIZE; +static const uint8_t RF_DISCOVER_NTF_PROTOCOL = 1 + NCI_PKT_HEADER_SIZE; +static const uint8_t RF_DISCOVER_NTF_MODE_TECH = 2 + NCI_PKT_HEADER_SIZE; +static const uint8_t RF_DISCOVER_NTF_RF_TECH_LENGTH = 3 + NCI_PKT_HEADER_SIZE; +static const uint8_t RF_DISCOVER_NTF_RF_TECH_PARAMS = 4 + NCI_PKT_HEADER_SIZE; +static const uint8_t RF_INTF_ACTIVATED_NTF_DISCOVERY_ID = 0 + NCI_PKT_HEADER_SIZE; +static const uint8_t RF_INTF_ACTIVATED_NTF_INTERFACE = 1 + NCI_PKT_HEADER_SIZE; +static const uint8_t RF_INTF_ACTIVATED_NTF_PROTOCOL = 2 + NCI_PKT_HEADER_SIZE; +static const uint8_t RF_INTF_ACTIVATED_NTF_MODE_TECH = 3 + NCI_PKT_HEADER_SIZE; +static const uint8_t RF_INTF_ACTIVATED_NTF_MAX_SIZE = 4 + NCI_PKT_HEADER_SIZE; +static const uint8_t RF_INTF_ACTIVATED_NTF_INIT_CRED = 5 + NCI_PKT_HEADER_SIZE; +static const uint8_t RF_INTF_ACTIVATED_NTF_RF_TECH_LENGTH = 6 + NCI_PKT_HEADER_SIZE; +static const uint8_t RF_INTF_ACTIVATED_NTF_RF_TECH_PARAMS = 7 + NCI_PKT_HEADER_SIZE; + +} // namespace nfc +} // namespace esphome diff --git a/esphome/components/nfc/nci_message.cpp b/esphome/components/nfc/nci_message.cpp new file mode 100644 index 0000000000..c6b21f6ae0 --- /dev/null +++ b/esphome/components/nfc/nci_message.cpp @@ -0,0 +1,166 @@ +#include "nci_core.h" +#include "nci_message.h" +#include "esphome/core/log.h" + +#include + +namespace esphome { +namespace nfc { + +static const char *const TAG = "NciMessage"; + +NciMessage::NciMessage(const uint8_t message_type, const std::vector &payload) { + this->set_message(message_type, payload); +} + +NciMessage::NciMessage(const uint8_t message_type, const uint8_t gid, const uint8_t oid) { + this->set_header(message_type, gid, oid); +} + +NciMessage::NciMessage(const uint8_t message_type, const uint8_t gid, const uint8_t oid, + const std::vector &payload) { + this->set_message(message_type, gid, oid, payload); +} + +NciMessage::NciMessage(const std::vector &raw_packet) { this->nci_message_ = raw_packet; }; + +std::vector NciMessage::encode() { + this->nci_message_[nfc::NCI_PKT_LENGTH_OFFSET] = this->nci_message_.size() - nfc::NCI_PKT_HEADER_SIZE; + std::vector message = this->nci_message_; + return message; +} + +void NciMessage::reset() { this->nci_message_ = {0, 0, 0}; } + +uint8_t NciMessage::get_message_type() const { + return this->nci_message_[nfc::NCI_PKT_MT_GID_OFFSET] & nfc::NCI_PKT_MT_MASK; +} + +uint8_t NciMessage::get_gid() const { return this->nci_message_[nfc::NCI_PKT_MT_GID_OFFSET] & nfc::NCI_PKT_GID_MASK; } + +uint8_t NciMessage::get_oid() const { return this->nci_message_[nfc::NCI_PKT_OID_OFFSET] & nfc::NCI_PKT_OID_MASK; } + +uint8_t NciMessage::get_payload_size(const bool recompute) { + if (!this->nci_message_.empty()) { + if (recompute) { + this->nci_message_[nfc::NCI_PKT_LENGTH_OFFSET] = this->nci_message_.size() - nfc::NCI_PKT_HEADER_SIZE; + } + return this->nci_message_[nfc::NCI_PKT_LENGTH_OFFSET]; + } + return 0; +} + +uint8_t NciMessage::get_simple_status_response() const { + if (this->nci_message_.size() > nfc::NCI_PKT_PAYLOAD_OFFSET) { + return this->nci_message_[nfc::NCI_PKT_PAYLOAD_OFFSET]; + } + return STATUS_FAILED; +} + +uint8_t NciMessage::get_message_byte(const uint8_t offset) const { + if (this->nci_message_.size() > offset) { + return this->nci_message_[offset]; + } + return 0; +} + +std::vector &NciMessage::get_message() { return this->nci_message_; } + +bool NciMessage::has_payload() const { return this->nci_message_.size() > nfc::NCI_PKT_HEADER_SIZE; } + +bool NciMessage::message_type_is(const uint8_t message_type) const { + if (!this->nci_message_.empty()) { + return message_type == (this->nci_message_[nfc::NCI_PKT_MT_GID_OFFSET] & nfc::NCI_PKT_MT_MASK); + } + return false; +} + +bool NciMessage::message_length_is(const uint8_t message_length, const bool recompute) { + if (this->nci_message_.size() > nfc::NCI_PKT_LENGTH_OFFSET) { + if (recompute) { + this->nci_message_[nfc::NCI_PKT_LENGTH_OFFSET] = this->nci_message_.size() - nfc::NCI_PKT_HEADER_SIZE; + } + return message_length == this->nci_message_[nfc::NCI_PKT_LENGTH_OFFSET]; + } + return false; +} + +bool NciMessage::gid_is(const uint8_t gid) const { + if (this->nci_message_.size() > nfc::NCI_PKT_MT_GID_OFFSET) { + return gid == (this->nci_message_[nfc::NCI_PKT_MT_GID_OFFSET] & nfc::NCI_PKT_GID_MASK); + } + return false; +} + +bool NciMessage::oid_is(const uint8_t oid) const { + if (this->nci_message_.size() > nfc::NCI_PKT_OID_OFFSET) { + return oid == (this->nci_message_[nfc::NCI_PKT_OID_OFFSET] & nfc::NCI_PKT_OID_MASK); + } + return false; +} + +bool NciMessage::simple_status_response_is(const uint8_t response) const { + if (this->nci_message_.size() > nfc::NCI_PKT_PAYLOAD_OFFSET) { + return response == this->nci_message_[nfc::NCI_PKT_PAYLOAD_OFFSET]; + } + return false; +} + +void NciMessage::set_header(const uint8_t message_type, const uint8_t gid, const uint8_t oid) { + if (this->nci_message_.size() < nfc::NCI_PKT_HEADER_SIZE) { + this->nci_message_.resize(nfc::NCI_PKT_HEADER_SIZE); + } + this->nci_message_[nfc::NCI_PKT_MT_GID_OFFSET] = + (message_type & nfc::NCI_PKT_MT_MASK) | (gid & nfc::NCI_PKT_GID_MASK); + this->nci_message_[nfc::NCI_PKT_OID_OFFSET] = oid & nfc::NCI_PKT_OID_MASK; +} + +void NciMessage::set_message(const uint8_t message_type, const std::vector &payload) { + this->nci_message_.resize(nfc::NCI_PKT_HEADER_SIZE); + this->nci_message_[nfc::NCI_PKT_LENGTH_OFFSET] = payload.size(); + this->nci_message_.insert(this->nci_message_.end(), payload.begin(), payload.end()); +} + +void NciMessage::set_message(const uint8_t message_type, const uint8_t gid, const uint8_t oid, + const std::vector &payload) { + this->nci_message_.resize(nfc::NCI_PKT_HEADER_SIZE); + this->nci_message_[nfc::NCI_PKT_MT_GID_OFFSET] = + (message_type & nfc::NCI_PKT_MT_MASK) | (gid & nfc::NCI_PKT_GID_MASK); + this->nci_message_[nfc::NCI_PKT_OID_OFFSET] = oid & nfc::NCI_PKT_OID_MASK; + this->nci_message_[nfc::NCI_PKT_LENGTH_OFFSET] = payload.size(); + this->nci_message_.insert(this->nci_message_.end(), payload.begin(), payload.end()); +} + +void NciMessage::set_message_type(const uint8_t message_type) { + if (this->nci_message_.size() < nfc::NCI_PKT_HEADER_SIZE) { + this->nci_message_.resize(nfc::NCI_PKT_HEADER_SIZE); + } + auto mt_masked = this->nci_message_[nfc::NCI_PKT_MT_GID_OFFSET] & ~nfc::NCI_PKT_MT_MASK; + this->nci_message_[nfc::NCI_PKT_MT_GID_OFFSET] = mt_masked | (message_type & nfc::NCI_PKT_MT_MASK); +} + +void NciMessage::set_gid(const uint8_t gid) { + if (this->nci_message_.size() < nfc::NCI_PKT_HEADER_SIZE) { + this->nci_message_.resize(nfc::NCI_PKT_HEADER_SIZE); + } + auto gid_masked = this->nci_message_[nfc::NCI_PKT_MT_GID_OFFSET] & ~nfc::NCI_PKT_GID_MASK; + this->nci_message_[nfc::NCI_PKT_MT_GID_OFFSET] = gid_masked | (gid & nfc::NCI_PKT_GID_MASK); +} + +void NciMessage::set_oid(const uint8_t oid) { + if (this->nci_message_.size() < nfc::NCI_PKT_HEADER_SIZE) { + this->nci_message_.resize(nfc::NCI_PKT_HEADER_SIZE); + } + this->nci_message_[nfc::NCI_PKT_OID_OFFSET] = oid & nfc::NCI_PKT_OID_MASK; +} + +void NciMessage::set_payload(const std::vector &payload) { + std::vector message(this->nci_message_.begin(), this->nci_message_.begin() + nfc::NCI_PKT_HEADER_SIZE); + + message.insert(message.end(), payload.begin(), payload.end()); + message[nfc::NCI_PKT_LENGTH_OFFSET] = payload.size(); + this->nci_message_ = message; +} + +} // namespace nfc +} // namespace esphome diff --git a/esphome/components/nfc/nci_message.h b/esphome/components/nfc/nci_message.h new file mode 100644 index 0000000000..c6b8537402 --- /dev/null +++ b/esphome/components/nfc/nci_message.h @@ -0,0 +1,50 @@ +#pragma once + +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" + +#include + +namespace esphome { +namespace nfc { + +class NciMessage { + public: + NciMessage() {} + NciMessage(uint8_t message_type, const std::vector &payload); + NciMessage(uint8_t message_type, uint8_t gid, uint8_t oid); + NciMessage(uint8_t message_type, uint8_t gid, uint8_t oid, const std::vector &payload); + NciMessage(const std::vector &raw_packet); + + std::vector encode(); + void reset(); + + uint8_t get_message_type() const; + uint8_t get_gid() const; + uint8_t get_oid() const; + uint8_t get_payload_size(bool recompute = false); + uint8_t get_simple_status_response() const; + uint8_t get_message_byte(uint8_t offset) const; + std::vector &get_message(); + + bool has_payload() const; + bool message_type_is(uint8_t message_type) const; + bool message_length_is(uint8_t message_length, bool recompute = false); + bool gid_is(uint8_t gid) const; + bool oid_is(uint8_t oid) const; + bool simple_status_response_is(uint8_t response) const; + + void set_header(uint8_t message_type, uint8_t gid, uint8_t oid); + void set_message(uint8_t message_type, const std::vector &payload); + void set_message(uint8_t message_type, uint8_t gid, uint8_t oid, const std::vector &payload); + void set_message_type(uint8_t message_type); + void set_gid(uint8_t gid); + void set_oid(uint8_t oid); + void set_payload(const std::vector &payload); + + protected: + std::vector nci_message_{0, 0, 0}; // three bytes, MT/PBF/GID, OID, payload length/size +}; + +} // namespace nfc +} // namespace esphome diff --git a/esphome/components/nfc/nfc.cpp b/esphome/components/nfc/nfc.cpp index 7225e373b3..cf5a7f5ef1 100644 --- a/esphome/components/nfc/nfc.cpp +++ b/esphome/components/nfc/nfc.cpp @@ -53,7 +53,7 @@ uint8_t get_mifare_classic_ndef_start_index(std::vector &data) { } bool decode_mifare_classic_tlv(std::vector &data, uint32_t &message_length, uint8_t &message_start_index) { - uint8_t i = get_mifare_classic_ndef_start_index(data); + auto i = get_mifare_classic_ndef_start_index(data); if (data[i] != 0x03) { ESP_LOGE(TAG, "Error, Can't decode message length."); return false; diff --git a/esphome/components/nfc/nfc_helpers.cpp b/esphome/components/nfc/nfc_helpers.cpp new file mode 100644 index 0000000000..bfaed6e486 --- /dev/null +++ b/esphome/components/nfc/nfc_helpers.cpp @@ -0,0 +1,47 @@ +#include "nfc_helpers.h" + +namespace esphome { +namespace nfc { + +static const char *const TAG = "nfc.helpers"; + +bool has_ha_tag_ndef(NfcTag &tag) { return !get_ha_tag_ndef(tag).empty(); } + +std::string get_ha_tag_ndef(NfcTag &tag) { + if (!tag.has_ndef_message()) { + return std::string(); + } + auto message = tag.get_ndef_message(); + auto records = message->get_records(); + for (const auto &record : records) { + std::string payload = record->get_payload(); + size_t pos = payload.find(HA_TAG_ID_PREFIX); + if (pos != std::string::npos) { + return payload.substr(pos + sizeof(HA_TAG_ID_PREFIX) - 1); + } + } + return std::string(); +} + +std::string get_random_ha_tag_ndef() { + static const char ALPHANUM[] = "0123456789abcdef"; + std::string uri = HA_TAG_ID_PREFIX; + for (int i = 0; i < 8; i++) { + uri += ALPHANUM[random_uint32() % (sizeof(ALPHANUM) - 1)]; + } + uri += "-"; + for (int j = 0; j < 3; j++) { + for (int i = 0; i < 4; i++) { + uri += ALPHANUM[random_uint32() % (sizeof(ALPHANUM) - 1)]; + } + uri += "-"; + } + for (int i = 0; i < 12; i++) { + uri += ALPHANUM[random_uint32() % (sizeof(ALPHANUM) - 1)]; + } + ESP_LOGD("pn7160", "Payload to be written: %s", uri.c_str()); + return uri; +} + +} // namespace nfc +} // namespace esphome diff --git a/esphome/components/nfc/nfc_helpers.h b/esphome/components/nfc/nfc_helpers.h new file mode 100644 index 0000000000..74f5beba13 --- /dev/null +++ b/esphome/components/nfc/nfc_helpers.h @@ -0,0 +1,17 @@ +#pragma once + +#include "nfc_tag.h" + +namespace esphome { +namespace nfc { + +static const char HA_TAG_ID_EXT_RECORD_TYPE[] = "android.com:pkg"; +static const char HA_TAG_ID_EXT_RECORD_PAYLOAD[] = "io.homeassistant.companion.android"; +static const char HA_TAG_ID_PREFIX[] = "https://www.home-assistant.io/tag/"; + +std::string get_ha_tag_ndef(NfcTag &tag); +std::string get_random_ha_tag_ndef(); +bool has_ha_tag_ndef(NfcTag &tag); + +} // namespace nfc +} // namespace esphome diff --git a/esphome/components/pn7160/__init__.py b/esphome/components/pn7160/__init__.py new file mode 100644 index 0000000000..c91ca78b03 --- /dev/null +++ b/esphome/components/pn7160/__init__.py @@ -0,0 +1,227 @@ +from esphome import automation, pins +from esphome.automation import maybe_simple_id +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import nfc +from esphome.const import ( + CONF_ID, + CONF_IRQ_PIN, + CONF_ON_TAG_REMOVED, + CONF_ON_TAG, + CONF_TRIGGER_ID, +) + +AUTO_LOAD = ["binary_sensor", "nfc"] +CODEOWNERS = ["@kbx81", "@jesserockz"] + +CONF_DWL_REQ_PIN = "dwl_req_pin" +CONF_EMULATION_MESSAGE = "emulation_message" +CONF_EMULATION_OFF = "emulation_off" +CONF_EMULATION_ON = "emulation_on" +CONF_INCLUDE_ANDROID_APP_RECORD = "include_android_app_record" +CONF_MESSAGE = "message" +CONF_ON_FINISHED_WRITE = "on_finished_write" +CONF_ON_EMULATED_TAG_SCAN = "on_emulated_tag_scan" +CONF_PN7160_ID = "pn7160_id" +CONF_POLLING_OFF = "polling_off" +CONF_POLLING_ON = "polling_on" +CONF_SET_CLEAN_MODE = "set_clean_mode" +CONF_SET_EMULATION_MESSAGE = "set_emulation_message" +CONF_SET_FORMAT_MODE = "set_format_mode" +CONF_SET_READ_MODE = "set_read_mode" +CONF_SET_WRITE_MESSAGE = "set_write_message" +CONF_SET_WRITE_MODE = "set_write_mode" +CONF_TAG_TTL = "tag_ttl" +CONF_VEN_PIN = "ven_pin" +CONF_WKUP_REQ_PIN = "wkup_req_pin" + +pn7160_ns = cg.esphome_ns.namespace("pn7160") +PN7160 = pn7160_ns.class_("PN7160", cg.Component) + +EmulationOffAction = pn7160_ns.class_("EmulationOffAction", automation.Action) +EmulationOnAction = pn7160_ns.class_("EmulationOnAction", automation.Action) +PollingOffAction = pn7160_ns.class_("PollingOffAction", automation.Action) +PollingOnAction = pn7160_ns.class_("PollingOnAction", automation.Action) +SetCleanModeAction = pn7160_ns.class_("SetCleanModeAction", automation.Action) +SetEmulationMessageAction = pn7160_ns.class_( + "SetEmulationMessageAction", automation.Action +) +SetFormatModeAction = pn7160_ns.class_("SetFormatModeAction", automation.Action) +SetReadModeAction = pn7160_ns.class_("SetReadModeAction", automation.Action) +SetWriteMessageAction = pn7160_ns.class_("SetWriteMessageAction", automation.Action) +SetWriteModeAction = pn7160_ns.class_("SetWriteModeAction", automation.Action) + + +PN7160OnEmulatedTagScanTrigger = pn7160_ns.class_( + "PN7160OnEmulatedTagScanTrigger", automation.Trigger.template() +) + +PN7160OnFinishedWriteTrigger = pn7160_ns.class_( + "PN7160OnFinishedWriteTrigger", automation.Trigger.template() +) + +PN7160IsWritingCondition = pn7160_ns.class_( + "PN7160IsWritingCondition", automation.Condition +) + + +IsWritingCondition = nfc.nfc_ns.class_("IsWritingCondition", automation.Condition) + + +SIMPLE_ACTION_SCHEMA = maybe_simple_id( + { + cv.Required(CONF_ID): cv.use_id(PN7160), + } +) + +SET_MESSAGE_ACTION_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.use_id(PN7160), + cv.Required(CONF_MESSAGE): cv.templatable(cv.string), + cv.Optional(CONF_INCLUDE_ANDROID_APP_RECORD, default=True): cv.boolean, + } +) + +PN7160_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(PN7160), + cv.Optional(CONF_ON_EMULATED_TAG_SCAN): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + PN7160OnEmulatedTagScanTrigger + ), + } + ), + cv.Optional(CONF_ON_FINISHED_WRITE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + PN7160OnFinishedWriteTrigger + ), + } + ), + cv.Optional(CONF_ON_TAG): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(nfc.NfcOnTagTrigger), + } + ), + cv.Optional(CONF_ON_TAG_REMOVED): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(nfc.NfcOnTagTrigger), + } + ), + cv.Optional(CONF_DWL_REQ_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_IRQ_PIN): pins.gpio_input_pin_schema, + cv.Required(CONF_VEN_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_WKUP_REQ_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_EMULATION_MESSAGE): cv.string, + cv.Optional(CONF_TAG_TTL): cv.positive_time_period_milliseconds, + } +).extend(cv.COMPONENT_SCHEMA) + + +@automation.register_action( + "tag.set_emulation_message", + SetEmulationMessageAction, + SET_MESSAGE_ACTION_SCHEMA, +) +@automation.register_action( + "tag.set_write_message", + SetWriteMessageAction, + SET_MESSAGE_ACTION_SCHEMA, +) +async def pn7160_set_message_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + template_ = await cg.templatable(config[CONF_MESSAGE], args, cg.std_string) + cg.add(var.set_message(template_)) + template_ = await cg.templatable( + config[CONF_INCLUDE_ANDROID_APP_RECORD], args, cg.bool_ + ) + cg.add(var.set_include_android_app_record(template_)) + return var + + +@automation.register_action( + "tag.emulation_off", EmulationOffAction, SIMPLE_ACTION_SCHEMA +) +@automation.register_action("tag.emulation_on", EmulationOnAction, SIMPLE_ACTION_SCHEMA) +@automation.register_action("tag.polling_off", PollingOffAction, SIMPLE_ACTION_SCHEMA) +@automation.register_action("tag.polling_on", PollingOnAction, SIMPLE_ACTION_SCHEMA) +@automation.register_action( + "tag.set_clean_mode", SetCleanModeAction, SIMPLE_ACTION_SCHEMA +) +@automation.register_action( + "tag.set_format_mode", SetFormatModeAction, SIMPLE_ACTION_SCHEMA +) +@automation.register_action( + "tag.set_read_mode", SetReadModeAction, SIMPLE_ACTION_SCHEMA +) +@automation.register_action( + "tag.set_write_mode", SetWriteModeAction, SIMPLE_ACTION_SCHEMA +) +async def pn7160_simple_action_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var + + +async def setup_pn7160(var, config): + await cg.register_component(var, config) + + if dwl_req_pin_config := config.get(CONF_DWL_REQ_PIN): + pin = await cg.gpio_pin_expression(dwl_req_pin_config) + cg.add(var.set_dwl_req_pin(pin)) + + pin = await cg.gpio_pin_expression(config[CONF_IRQ_PIN]) + cg.add(var.set_irq_pin(pin)) + + pin = await cg.gpio_pin_expression(config[CONF_VEN_PIN]) + cg.add(var.set_ven_pin(pin)) + + if wakeup_req_pin_config := config.get(CONF_WKUP_REQ_PIN): + pin = await cg.gpio_pin_expression(wakeup_req_pin_config) + cg.add(var.set_wkup_req_pin(pin)) + + if emulation_message_config := config.get(CONF_EMULATION_MESSAGE): + cg.add(var.set_tag_emulation_message(emulation_message_config)) + cg.add(var.set_tag_emulation_on()) + + if CONF_TAG_TTL in config: + cg.add(var.set_tag_ttl(config[CONF_TAG_TTL])) + + for conf in config.get(CONF_ON_TAG, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) + cg.add(var.register_ontag_trigger(trigger)) + await automation.build_automation( + trigger, [(cg.std_string, "x"), (nfc.NfcTag, "tag")], conf + ) + + for conf in config.get(CONF_ON_TAG_REMOVED, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) + cg.add(var.register_ontagremoved_trigger(trigger)) + await automation.build_automation( + trigger, [(cg.std_string, "x"), (nfc.NfcTag, "tag")], conf + ) + + for conf in config.get(CONF_ON_EMULATED_TAG_SCAN, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + + for conf in config.get(CONF_ON_FINISHED_WRITE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + + +@automation.register_condition( + "pn7160.is_writing", + PN7160IsWritingCondition, + cv.Schema( + { + cv.GenerateID(): cv.use_id(PN7160), + } + ), +) +async def pn7160_is_writing_to_code(config, condition_id, template_arg, args): + var = cg.new_Pvariable(condition_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var diff --git a/esphome/components/pn7160/automation.h b/esphome/components/pn7160/automation.h new file mode 100644 index 0000000000..854fb11684 --- /dev/null +++ b/esphome/components/pn7160/automation.h @@ -0,0 +1,82 @@ +#pragma once + +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/components/pn7160/pn7160.h" + +namespace esphome { +namespace pn7160 { + +class PN7160OnEmulatedTagScanTrigger : public Trigger<> { + public: + explicit PN7160OnEmulatedTagScanTrigger(PN7160 *parent) { + parent->add_on_emulated_tag_scan_callback([this]() { this->trigger(); }); + } +}; + +class PN7160OnFinishedWriteTrigger : public Trigger<> { + public: + explicit PN7160OnFinishedWriteTrigger(PN7160 *parent) { + parent->add_on_finished_write_callback([this]() { this->trigger(); }); + } +}; + +template class PN7160IsWritingCondition : public Condition, public Parented { + public: + bool check(Ts... x) override { return this->parent_->is_writing(); } +}; + +template class EmulationOffAction : public Action, public Parented { + void play(Ts... x) override { this->parent_->set_tag_emulation_off(); } +}; + +template class EmulationOnAction : public Action, public Parented { + void play(Ts... x) override { this->parent_->set_tag_emulation_on(); } +}; + +template class PollingOffAction : public Action, public Parented { + void play(Ts... x) override { this->parent_->set_polling_off(); } +}; + +template class PollingOnAction : public Action, public Parented { + void play(Ts... x) override { this->parent_->set_polling_on(); } +}; + +template class SetCleanModeAction : public Action, public Parented { + void play(Ts... x) override { this->parent_->clean_mode(); } +}; + +template class SetFormatModeAction : public Action, public Parented { + void play(Ts... x) override { this->parent_->format_mode(); } +}; + +template class SetReadModeAction : public Action, public Parented { + void play(Ts... x) override { this->parent_->read_mode(); } +}; + +template class SetEmulationMessageAction : public Action, public Parented { + TEMPLATABLE_VALUE(std::string, message) + TEMPLATABLE_VALUE(bool, include_android_app_record) + + void play(Ts... x) override { + this->parent_->set_tag_emulation_message(this->message_.optional_value(x...), + this->include_android_app_record_.optional_value(x...)); + } +}; + +template class SetWriteMessageAction : public Action, public Parented { + TEMPLATABLE_VALUE(std::string, message) + TEMPLATABLE_VALUE(bool, include_android_app_record) + + void play(Ts... x) override { + this->parent_->set_tag_write_message(this->message_.optional_value(x...), + this->include_android_app_record_.optional_value(x...)); + } +}; + +template class SetWriteModeAction : public Action, public Parented { + void play(Ts... x) override { this->parent_->write_mode(); } +}; + +} // namespace pn7160 +} // namespace esphome diff --git a/esphome/components/pn7160/pn7160.cpp b/esphome/components/pn7160/pn7160.cpp new file mode 100644 index 0000000000..ce5374d1d1 --- /dev/null +++ b/esphome/components/pn7160/pn7160.cpp @@ -0,0 +1,1161 @@ +#include + +#include "automation.h" +#include "pn7160.h" + +#include "esphome/core/hal.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace pn7160 { + +static const char *const TAG = "pn7160"; + +void PN7160::setup() { + this->irq_pin_->setup(); + this->ven_pin_->setup(); + if (this->dwl_req_pin_ != nullptr) { + this->dwl_req_pin_->setup(); + } + if (this->wkup_req_pin_ != nullptr) { + this->wkup_req_pin_->setup(); + } + + this->nci_fsm_transition_(); // kick off reset & init processes +} + +void PN7160::dump_config() { + ESP_LOGCONFIG(TAG, "PN7160:"); + if (this->dwl_req_pin_ != nullptr) { + LOG_PIN(" DWL_REQ pin: ", this->dwl_req_pin_); + } + LOG_PIN(" IRQ pin: ", this->irq_pin_); + LOG_PIN(" VEN pin: ", this->ven_pin_); + if (this->wkup_req_pin_ != nullptr) { + LOG_PIN(" WKUP_REQ pin: ", this->wkup_req_pin_); + } +} + +void PN7160::loop() { + this->nci_fsm_transition_(); + this->purge_old_tags_(); +} + +void PN7160::set_tag_emulation_message(std::shared_ptr message) { + this->card_emulation_message_ = std::move(message); + ESP_LOGD(TAG, "Tag emulation message set"); +} + +void PN7160::set_tag_emulation_message(const optional &message, + const optional include_android_app_record) { + if (!message.has_value()) { + return; + } + + auto ndef_message = make_unique(); + + ndef_message->add_uri_record(message.value()); + + if (!include_android_app_record.has_value() || include_android_app_record.value()) { + auto ext_record = make_unique(); + ext_record->set_tnf(nfc::TNF_EXTERNAL_TYPE); + ext_record->set_type(nfc::HA_TAG_ID_EXT_RECORD_TYPE); + ext_record->set_payload(nfc::HA_TAG_ID_EXT_RECORD_PAYLOAD); + ndef_message->add_record(std::move(ext_record)); + } + + this->card_emulation_message_ = std::move(ndef_message); + ESP_LOGD(TAG, "Tag emulation message set"); +} + +void PN7160::set_tag_emulation_message(const char *message, const bool include_android_app_record) { + this->set_tag_emulation_message(std::string(message), include_android_app_record); +} + +void PN7160::set_tag_emulation_off() { + if (this->listening_enabled_) { + this->listening_enabled_ = false; + this->config_refresh_pending_ = true; + } + ESP_LOGD(TAG, "Tag emulation disabled"); +} + +void PN7160::set_tag_emulation_on() { + if (this->card_emulation_message_ == nullptr) { + ESP_LOGE(TAG, "No NDEF message is set; tag emulation cannot be enabled"); + return; + } + if (!this->listening_enabled_) { + this->listening_enabled_ = true; + this->config_refresh_pending_ = true; + } + ESP_LOGD(TAG, "Tag emulation enabled"); +} + +void PN7160::set_polling_off() { + if (this->polling_enabled_) { + this->polling_enabled_ = false; + this->config_refresh_pending_ = true; + } + ESP_LOGD(TAG, "Tag polling disabled"); +} + +void PN7160::set_polling_on() { + if (!this->polling_enabled_) { + this->polling_enabled_ = true; + this->config_refresh_pending_ = true; + } + ESP_LOGD(TAG, "Tag polling enabled"); +} + +void PN7160::read_mode() { + this->next_task_ = EP_READ; + ESP_LOGD(TAG, "Waiting to read next tag"); +} + +void PN7160::clean_mode() { + this->next_task_ = EP_CLEAN; + ESP_LOGD(TAG, "Waiting to clean next tag"); +} + +void PN7160::format_mode() { + this->next_task_ = EP_FORMAT; + ESP_LOGD(TAG, "Waiting to format next tag"); +} + +void PN7160::write_mode() { + if (this->next_task_message_to_write_ == nullptr) { + ESP_LOGW(TAG, "Message to write must be set before setting write mode"); + return; + } + + this->next_task_ = EP_WRITE; + ESP_LOGD(TAG, "Waiting to write next tag"); +} + +void PN7160::set_tag_write_message(std::shared_ptr message) { + this->next_task_message_to_write_ = std::move(message); + ESP_LOGD(TAG, "Message to write has been set"); +} + +void PN7160::set_tag_write_message(optional message, optional include_android_app_record) { + if (!message.has_value()) { + return; + } + + auto ndef_message = make_unique(); + + ndef_message->add_uri_record(message.value()); + + if (!include_android_app_record.has_value() || include_android_app_record.value()) { + auto ext_record = make_unique(); + ext_record->set_tnf(nfc::TNF_EXTERNAL_TYPE); + ext_record->set_type(nfc::HA_TAG_ID_EXT_RECORD_TYPE); + ext_record->set_payload(nfc::HA_TAG_ID_EXT_RECORD_PAYLOAD); + ndef_message->add_record(std::move(ext_record)); + } + + this->next_task_message_to_write_ = std::move(ndef_message); + ESP_LOGD(TAG, "Message to write has been set"); +} + +uint8_t PN7160::set_test_mode(const TestMode test_mode, const std::vector &data, + std::vector &result) { + auto test_oid = TEST_PRBS_OID; + + switch (test_mode) { + case TestMode::TEST_PRBS: + // test_oid = TEST_PRBS_OID; + break; + + case TestMode::TEST_ANTENNA: + test_oid = TEST_ANTENNA_OID; + break; + + case TestMode::TEST_GET_REGISTER: + test_oid = TEST_GET_REGISTER_OID; + break; + + case TestMode::TEST_NONE: + default: + ESP_LOGD(TAG, "Exiting test mode"); + this->nci_fsm_set_state_(NCIState::NFCC_RESET); + return nfc::STATUS_OK; + } + + if (this->reset_core_(true, true) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Failed to reset NCI core"); + this->nci_fsm_set_error_state_(NCIState::NFCC_RESET); + result.clear(); + return nfc::STATUS_FAILED; + } else { + this->nci_fsm_set_state_(NCIState::NFCC_INIT); + } + if (this->init_core_() != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Failed to initialise NCI core"); + this->nci_fsm_set_error_state_(NCIState::NFCC_INIT); + result.clear(); + return nfc::STATUS_FAILED; + } else { + this->nci_fsm_set_state_(NCIState::TEST); + } + + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::NCI_PROPRIETARY_GID, test_oid, data); + + ESP_LOGW(TAG, "Starting test mode, OID 0x%02X", test_oid); + auto status = this->transceive_(tx, rx, NFCC_INIT_TIMEOUT); + + if (status != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Failed to start test mode, OID 0x%02X", test_oid); + this->nci_fsm_set_state_(NCIState::NFCC_RESET); + result.clear(); + } else { + result = rx.get_message(); + result.erase(result.begin(), result.begin() + 4); // remove NCI header + if (!result.empty()) { + ESP_LOGW(TAG, "Test results: %s", nfc::format_bytes(result).c_str()); + } + } + return status; +} + +uint8_t PN7160::reset_core_(const bool reset_config, const bool power) { + if (this->dwl_req_pin_ != nullptr) { + this->dwl_req_pin_->digital_write(false); + delay(NFCC_DEFAULT_TIMEOUT); + } + + if (power) { + this->ven_pin_->digital_write(true); + delay(NFCC_DEFAULT_TIMEOUT); + this->ven_pin_->digital_write(false); + delay(NFCC_DEFAULT_TIMEOUT); + this->ven_pin_->digital_write(true); + delay(NFCC_INIT_TIMEOUT); + } + + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::NCI_CORE_GID, nfc::NCI_CORE_RESET_OID, + {(uint8_t) reset_config}); + + if (this->transceive_(tx, rx, NFCC_INIT_TIMEOUT) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error sending reset command"); + return nfc::STATUS_FAILED; + } + + if (!rx.simple_status_response_is(nfc::STATUS_OK)) { + ESP_LOGE(TAG, "Invalid reset response: %s", nfc::format_bytes(rx.get_message()).c_str()); + return rx.get_simple_status_response(); + } + // read reset notification + if (this->read_nfcc(rx, NFCC_INIT_TIMEOUT) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Reset notification was not received"); + return nfc::STATUS_FAILED; + } + // verify reset notification + if ((!rx.message_type_is(nfc::NCI_PKT_MT_CTRL_NOTIFICATION)) || (!rx.message_length_is(9)) || + (rx.get_message()[nfc::NCI_PKT_PAYLOAD_OFFSET] != 0x02) || + (rx.get_message()[nfc::NCI_PKT_PAYLOAD_OFFSET + 1] != (uint8_t) reset_config)) { + ESP_LOGE(TAG, "Reset notification was malformed: %s", nfc::format_bytes(rx.get_message()).c_str()); + return nfc::STATUS_FAILED; + } + + ESP_LOGD(TAG, "Configuration %s", rx.get_message()[4] ? "reset" : "retained"); + ESP_LOGD(TAG, "NCI version: %s", rx.get_message()[5] == 0x20 ? "2.0" : "1.0"); + ESP_LOGD(TAG, "Manufacturer ID: 0x%02X", rx.get_message()[6]); + rx.get_message().erase(rx.get_message().begin(), rx.get_message().begin() + 8); + ESP_LOGD(TAG, "Manufacturer info: %s", nfc::format_bytes(rx.get_message()).c_str()); + + return nfc::STATUS_OK; +} + +uint8_t PN7160::init_core_() { + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::NCI_CORE_GID, nfc::NCI_CORE_INIT_OID); + + if (this->transceive_(tx, rx) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error sending initialise command"); + return nfc::STATUS_FAILED; + } + + if (!rx.simple_status_response_is(nfc::STATUS_OK)) { + ESP_LOGE(TAG, "Invalid initialise response: %s", nfc::format_bytes(rx.get_message()).c_str()); + return nfc::STATUS_FAILED; + } + + uint8_t hw_version = rx.get_message()[17 + rx.get_message()[8]]; + uint8_t rom_code_version = rx.get_message()[18 + rx.get_message()[8]]; + uint8_t flash_major_version = rx.get_message()[19 + rx.get_message()[8]]; + uint8_t flash_minor_version = rx.get_message()[20 + rx.get_message()[8]]; + std::vector features(rx.get_message().begin() + 4, rx.get_message().begin() + 8); + + ESP_LOGD(TAG, "Hardware version: %u", hw_version); + ESP_LOGD(TAG, "ROM code version: %u", rom_code_version); + ESP_LOGD(TAG, "FLASH major version: %u", flash_major_version); + ESP_LOGD(TAG, "FLASH minor version: %u", flash_minor_version); + ESP_LOGD(TAG, "Features: %s", nfc::format_bytes(features).c_str()); + + return rx.get_simple_status_response(); +} + +uint8_t PN7160::send_init_config_() { + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::NCI_PROPRIETARY_GID, nfc::NCI_CORE_SET_CONFIG_OID); + + if (this->transceive_(tx, rx) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error enabling proprietary extensions"); + return nfc::STATUS_FAILED; + } + + tx.set_message(nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::NCI_CORE_GID, nfc::NCI_CORE_SET_CONFIG_OID, + std::vector(std::begin(PMU_CFG), std::end(PMU_CFG))); + + if (this->transceive_(tx, rx) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error sending PMU config"); + return nfc::STATUS_FAILED; + } + + return this->send_core_config_(); +} + +uint8_t PN7160::send_core_config_() { + const auto *core_config_begin = std::begin(CORE_CONFIG_SOLO); + const auto *core_config_end = std::end(CORE_CONFIG_SOLO); + this->core_config_is_solo_ = true; + + if (this->listening_enabled_ && this->polling_enabled_) { + core_config_begin = std::begin(CORE_CONFIG_RW_CE); + core_config_end = std::end(CORE_CONFIG_RW_CE); + this->core_config_is_solo_ = false; + } + + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::NCI_CORE_GID, nfc::NCI_CORE_SET_CONFIG_OID, + std::vector(core_config_begin, core_config_end)); + + if (this->transceive_(tx, rx) != nfc::STATUS_OK) { + ESP_LOGW(TAG, "Error sending core config"); + return nfc::STATUS_FAILED; + } + + return nfc::STATUS_OK; +} + +uint8_t PN7160::refresh_core_config_() { + bool core_config_should_be_solo = !(this->listening_enabled_ && this->polling_enabled_); + + if (this->nci_state_ == NCIState::RFST_DISCOVERY) { + if (this->stop_discovery_() != nfc::STATUS_OK) { + this->nci_fsm_set_state_(NCIState::NFCC_RESET); + return nfc::STATUS_FAILED; + } + this->nci_fsm_set_state_(NCIState::RFST_IDLE); + } + + if (this->core_config_is_solo_ != core_config_should_be_solo) { + if (this->send_core_config_() != nfc::STATUS_OK) { + ESP_LOGV(TAG, "Failed to refresh core config"); + return nfc::STATUS_FAILED; + } + } + this->config_refresh_pending_ = false; + return nfc::STATUS_OK; +} + +uint8_t PN7160::set_discover_map_() { + std::vector discover_map = {sizeof(RF_DISCOVER_MAP_CONFIG) / 3}; + discover_map.insert(discover_map.end(), std::begin(RF_DISCOVER_MAP_CONFIG), std::end(RF_DISCOVER_MAP_CONFIG)); + + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::RF_GID, nfc::RF_DISCOVER_MAP_OID, discover_map); + + if (this->transceive_(tx, rx) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error sending discover map poll config"); + return nfc::STATUS_FAILED; + } + return nfc::STATUS_OK; +} + +uint8_t PN7160::set_listen_mode_routing_() { + nfc::NciMessage rx; + nfc::NciMessage tx( + nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::RF_GID, nfc::RF_SET_LISTEN_MODE_ROUTING_OID, + std::vector(std::begin(RF_LISTEN_MODE_ROUTING_CONFIG), std::end(RF_LISTEN_MODE_ROUTING_CONFIG))); + + if (this->transceive_(tx, rx) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error setting listen mode routing config"); + return nfc::STATUS_FAILED; + } + return nfc::STATUS_OK; +} + +uint8_t PN7160::start_discovery_() { + const uint8_t *rf_discovery_config = RF_DISCOVERY_CONFIG; + uint8_t length = sizeof(RF_DISCOVERY_CONFIG); + + if (!this->listening_enabled_) { + length = sizeof(RF_DISCOVERY_POLL_CONFIG); + rf_discovery_config = RF_DISCOVERY_POLL_CONFIG; + } else if (!this->polling_enabled_) { + length = sizeof(RF_DISCOVERY_LISTEN_CONFIG); + rf_discovery_config = RF_DISCOVERY_LISTEN_CONFIG; + } + + std::vector discover_config = std::vector((length * 2) + 1); + + discover_config[0] = length; + for (uint8_t i = 0; i < length; i++) { + discover_config[(i * 2) + 1] = rf_discovery_config[i]; + discover_config[(i * 2) + 2] = 0x01; // RF Technology and Mode will be executed in every discovery period + } + + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::RF_GID, nfc::RF_DISCOVER_OID, discover_config); + + if (this->transceive_(tx, rx) != nfc::STATUS_OK) { + switch (rx.get_simple_status_response()) { + // in any of these cases, we are either already in or will remain in discovery, which satisfies the function call + case nfc::STATUS_OK: + case nfc::DISCOVERY_ALREADY_STARTED: + case nfc::DISCOVERY_TARGET_ACTIVATION_FAILED: + case nfc::DISCOVERY_TEAR_DOWN: + return nfc::STATUS_OK; + + default: + ESP_LOGE(TAG, "Error starting discovery"); + return nfc::STATUS_FAILED; + } + } + + return nfc::STATUS_OK; +} + +uint8_t PN7160::stop_discovery_() { return this->deactivate_(nfc::DEACTIVATION_TYPE_IDLE, NFCC_TAG_WRITE_TIMEOUT); } + +uint8_t PN7160::deactivate_(const uint8_t type, const uint16_t timeout) { + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::RF_GID, nfc::RF_DEACTIVATE_OID, {type}); + + auto status = this->transceive_(tx, rx, timeout); + // if (status != nfc::STATUS_OK) { + // ESP_LOGE(TAG, "Error sending deactivate type %u", type); + // return nfc::STATUS_FAILED; + // } + return status; +} + +void PN7160::select_endpoint_() { + if (this->discovered_endpoint_.empty()) { + ESP_LOGW(TAG, "No cached tags to select"); + this->stop_discovery_(); + this->nci_fsm_set_state_(NCIState::RFST_IDLE); + return; + } + std::vector endpoint_data = {this->discovered_endpoint_[0].id, this->discovered_endpoint_[0].protocol, + 0x01}; // that last byte is the interface ID + for (size_t i = 0; i < this->discovered_endpoint_.size(); i++) { + if (!this->discovered_endpoint_[i].trig_called) { + endpoint_data = {this->discovered_endpoint_[i].id, this->discovered_endpoint_[i].protocol, + 0x01}; // that last byte is the interface ID + this->selecting_endpoint_ = i; + break; + } + } + + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::RF_GID, nfc::RF_DISCOVER_SELECT_OID, endpoint_data); + + if (this->transceive_(tx, rx) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error selecting endpoint"); + } else { + this->nci_fsm_set_state_(NCIState::EP_SELECTING); + } +} + +uint8_t PN7160::read_endpoint_data_(nfc::NfcTag &tag) { + uint8_t type = nfc::guess_tag_type(tag.get_uid().size()); + + switch (type) { + case nfc::TAG_TYPE_MIFARE_CLASSIC: + ESP_LOGV(TAG, "Reading Mifare classic"); + return this->read_mifare_classic_tag_(tag); + + case nfc::TAG_TYPE_2: + ESP_LOGV(TAG, "Reading Mifare ultralight"); + return this->read_mifare_ultralight_tag_(tag); + + case nfc::TAG_TYPE_UNKNOWN: + default: + ESP_LOGV(TAG, "Cannot determine tag type"); + break; + } + return nfc::STATUS_FAILED; +} + +uint8_t PN7160::clean_endpoint_(std::vector &uid) { + uint8_t type = nfc::guess_tag_type(uid.size()); + switch (type) { + case nfc::TAG_TYPE_MIFARE_CLASSIC: + return this->format_mifare_classic_mifare_(); + + case nfc::TAG_TYPE_2: + return this->clean_mifare_ultralight_(); + + default: + ESP_LOGE(TAG, "Unsupported tag for cleaning"); + break; + } + return nfc::STATUS_FAILED; +} + +uint8_t PN7160::format_endpoint_(std::vector &uid) { + uint8_t type = nfc::guess_tag_type(uid.size()); + switch (type) { + case nfc::TAG_TYPE_MIFARE_CLASSIC: + return this->format_mifare_classic_ndef_(); + + case nfc::TAG_TYPE_2: + return this->clean_mifare_ultralight_(); + + default: + ESP_LOGE(TAG, "Unsupported tag for formatting"); + break; + } + return nfc::STATUS_FAILED; +} + +uint8_t PN7160::write_endpoint_(std::vector &uid, std::shared_ptr &message) { + uint8_t type = nfc::guess_tag_type(uid.size()); + switch (type) { + case nfc::TAG_TYPE_MIFARE_CLASSIC: + return this->write_mifare_classic_tag_(message); + + case nfc::TAG_TYPE_2: + return this->write_mifare_ultralight_tag_(uid, message); + + default: + ESP_LOGE(TAG, "Unsupported tag for writing"); + break; + } + return nfc::STATUS_FAILED; +} + +std::unique_ptr PN7160::build_tag_(const uint8_t mode_tech, const std::vector &data) { + switch (mode_tech) { + case (nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCA): { + uint8_t uid_length = data[2]; + if (!uid_length) { + ESP_LOGE(TAG, "UID length cannot be zero"); + return nullptr; + } + std::vector uid(data.begin() + 3, data.begin() + 3 + uid_length); + const auto *tag_type_str = + nfc::guess_tag_type(uid_length) == nfc::TAG_TYPE_MIFARE_CLASSIC ? nfc::MIFARE_CLASSIC : nfc::NFC_FORUM_TYPE_2; + return make_unique(uid, tag_type_str); + } + } + return nullptr; +} + +optional PN7160::find_tag_uid_(const std::vector &uid) { + if (!this->discovered_endpoint_.empty()) { + for (size_t i = 0; i < this->discovered_endpoint_.size(); i++) { + auto existing_tag_uid = this->discovered_endpoint_[i].tag->get_uid(); + bool uid_match = (uid.size() == existing_tag_uid.size()); + + if (uid_match) { + for (size_t i = 0; i < uid.size(); i++) { + uid_match &= (uid[i] == existing_tag_uid[i]); + } + if (uid_match) { + return i; + } + } + } + } + return nullopt; +} + +void PN7160::purge_old_tags_() { + for (size_t i = 0; i < this->discovered_endpoint_.size(); i++) { + if (millis() - this->discovered_endpoint_[i].last_seen > this->tag_ttl_) { + this->erase_tag_(i); + } + } +} + +void PN7160::erase_tag_(const uint8_t tag_index) { + if (tag_index < this->discovered_endpoint_.size()) { + for (auto *trigger : this->triggers_ontagremoved_) { + trigger->process(this->discovered_endpoint_[tag_index].tag); + } + ESP_LOGI(TAG, "Tag %s removed", nfc::format_uid(this->discovered_endpoint_[tag_index].tag->get_uid()).c_str()); + this->discovered_endpoint_.erase(this->discovered_endpoint_.begin() + tag_index); + } +} + +void PN7160::nci_fsm_transition_() { + switch (this->nci_state_) { + case NCIState::NFCC_RESET: + if (this->reset_core_(true, true) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Failed to reset NCI core"); + this->nci_fsm_set_error_state_(NCIState::NFCC_RESET); + return; + } else { + this->nci_fsm_set_state_(NCIState::NFCC_INIT); + } + // fall through + + case NCIState::NFCC_INIT: + if (this->init_core_() != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Failed to initialise NCI core"); + this->nci_fsm_set_error_state_(NCIState::NFCC_INIT); + return; + } else { + this->nci_fsm_set_state_(NCIState::NFCC_CONFIG); + } + // fall through + + case NCIState::NFCC_CONFIG: + if (this->send_init_config_() != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Failed to send initial config"); + this->nci_fsm_set_error_state_(NCIState::NFCC_CONFIG); + return; + } else { + this->config_refresh_pending_ = false; + this->nci_fsm_set_state_(NCIState::NFCC_SET_DISCOVER_MAP); + } + // fall through + + case NCIState::NFCC_SET_DISCOVER_MAP: + if (this->set_discover_map_() != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Failed to set discover map"); + this->nci_fsm_set_error_state_(NCIState::NFCC_SET_LISTEN_MODE_ROUTING); + return; + } else { + this->nci_fsm_set_state_(NCIState::NFCC_SET_LISTEN_MODE_ROUTING); + } + // fall through + + case NCIState::NFCC_SET_LISTEN_MODE_ROUTING: + if (this->set_listen_mode_routing_() != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Failed to set listen mode routing"); + this->nci_fsm_set_error_state_(NCIState::RFST_IDLE); + return; + } else { + this->nci_fsm_set_state_(NCIState::RFST_IDLE); + } + // fall through + + case NCIState::RFST_IDLE: + if (this->nci_state_error_ == NCIState::RFST_DISCOVERY) { + this->stop_discovery_(); + } + + if (this->config_refresh_pending_) { + this->refresh_core_config_(); + } + + if (!this->listening_enabled_ && !this->polling_enabled_) { + return; + } + + if (this->start_discovery_() != nfc::STATUS_OK) { + ESP_LOGV(TAG, "Failed to start discovery"); + this->nci_fsm_set_error_state_(NCIState::RFST_DISCOVERY); + } else { + this->nci_fsm_set_state_(NCIState::RFST_DISCOVERY); + } + return; + + case NCIState::RFST_W4_HOST_SELECT: + select_endpoint_(); + // fall through + + // All cases below are waiting for NOTIFICATION messages + case NCIState::RFST_DISCOVERY: + if (this->config_refresh_pending_) { + this->refresh_core_config_(); + } + // fall through + + case NCIState::RFST_LISTEN_ACTIVE: + case NCIState::RFST_LISTEN_SLEEP: + case NCIState::RFST_POLL_ACTIVE: + case NCIState::EP_SELECTING: + case NCIState::EP_DEACTIVATING: + if (this->irq_pin_->digital_read()) { + this->process_message_(); + } + break; + + case NCIState::FAILED: + case NCIState::NONE: + default: + return; + } +} + +void PN7160::nci_fsm_set_state_(NCIState new_state) { + ESP_LOGVV(TAG, "nci_fsm_set_state_(%u)", (uint8_t) new_state); + this->nci_state_ = new_state; + this->nci_state_error_ = NCIState::NONE; + this->error_count_ = 0; + this->last_nci_state_change_ = millis(); +} + +bool PN7160::nci_fsm_set_error_state_(NCIState new_state) { + ESP_LOGVV(TAG, "nci_fsm_set_error_state_(%u); error_count_ = %u", (uint8_t) new_state, this->error_count_); + this->nci_state_error_ = new_state; + if (this->error_count_++ > NFCC_MAX_ERROR_COUNT) { + if ((this->nci_state_error_ == NCIState::NFCC_RESET) || (this->nci_state_error_ == NCIState::NFCC_INIT) || + (this->nci_state_error_ == NCIState::NFCC_CONFIG)) { + ESP_LOGE(TAG, "Too many initialization failures -- check device connections"); + this->mark_failed(); + this->nci_fsm_set_state_(NCIState::FAILED); + } else { + ESP_LOGW(TAG, "Too many errors transitioning to state %u; resetting NFCC", (uint8_t) this->nci_state_error_); + this->nci_fsm_set_state_(NCIState::NFCC_RESET); + } + } + return this->error_count_ > NFCC_MAX_ERROR_COUNT; +} + +void PN7160::process_message_() { + nfc::NciMessage rx; + if (this->read_nfcc(rx, NFCC_DEFAULT_TIMEOUT) != nfc::STATUS_OK) { + return; // No data + } + + switch (rx.get_message_type()) { + case nfc::NCI_PKT_MT_CTRL_NOTIFICATION: + if (rx.get_gid() == nfc::RF_GID) { + switch (rx.get_oid()) { + case nfc::RF_INTF_ACTIVATED_OID: + ESP_LOGVV(TAG, "RF_INTF_ACTIVATED_OID"); + this->process_rf_intf_activated_oid_(rx); + return; + + case nfc::RF_DISCOVER_OID: + ESP_LOGVV(TAG, "RF_DISCOVER_OID"); + this->process_rf_discover_oid_(rx); + return; + + case nfc::RF_DEACTIVATE_OID: + ESP_LOGVV(TAG, "RF_DEACTIVATE_OID: type: 0x%02X, reason: 0x%02X", rx.get_message()[3], rx.get_message()[4]); + this->process_rf_deactivate_oid_(rx); + return; + + default: + ESP_LOGV(TAG, "Unimplemented RF OID received: 0x%02X", rx.get_oid()); + } + } else if (rx.get_gid() == nfc::NCI_CORE_GID) { + switch (rx.get_oid()) { + case nfc::NCI_CORE_GENERIC_ERROR_OID: + ESP_LOGV(TAG, "NCI_CORE_GENERIC_ERROR_OID:"); + switch (rx.get_simple_status_response()) { + case nfc::DISCOVERY_ALREADY_STARTED: + ESP_LOGV(TAG, " DISCOVERY_ALREADY_STARTED"); + break; + + case nfc::DISCOVERY_TARGET_ACTIVATION_FAILED: + // Tag removed too soon + ESP_LOGV(TAG, " DISCOVERY_TARGET_ACTIVATION_FAILED"); + if (this->nci_state_ == NCIState::EP_SELECTING) { + this->nci_fsm_set_state_(NCIState::RFST_W4_HOST_SELECT); + if (!this->discovered_endpoint_.empty()) { + this->erase_tag_(this->selecting_endpoint_); + } + } else { + this->stop_discovery_(); + this->nci_fsm_set_state_(NCIState::RFST_IDLE); + } + break; + + case nfc::DISCOVERY_TEAR_DOWN: + ESP_LOGV(TAG, " DISCOVERY_TEAR_DOWN"); + break; + + default: + ESP_LOGW(TAG, "Unknown error: 0x%02X", rx.get_simple_status_response()); + break; + } + break; + + default: + ESP_LOGV(TAG, "Unimplemented NCI Core OID received: 0x%02X", rx.get_oid()); + } + } else { + ESP_LOGV(TAG, "Unimplemented notification: %s", nfc::format_bytes(rx.get_message()).c_str()); + } + break; + + case nfc::NCI_PKT_MT_CTRL_RESPONSE: + ESP_LOGV(TAG, "Unimplemented GID: 0x%02X OID: 0x%02X Full response: %s", rx.get_gid(), rx.get_oid(), + nfc::format_bytes(rx.get_message()).c_str()); + break; + + case nfc::NCI_PKT_MT_CTRL_COMMAND: + ESP_LOGV(TAG, "Unimplemented command: %s", nfc::format_bytes(rx.get_message()).c_str()); + break; + + case nfc::NCI_PKT_MT_DATA: + this->process_data_message_(rx); + break; + + default: + ESP_LOGV(TAG, "Unimplemented message type: %s", nfc::format_bytes(rx.get_message()).c_str()); + break; + } +} + +void PN7160::process_rf_intf_activated_oid_(nfc::NciMessage &rx) { // an endpoint was activated + uint8_t discovery_id = rx.get_message_byte(nfc::RF_INTF_ACTIVATED_NTF_DISCOVERY_ID); + uint8_t interface = rx.get_message_byte(nfc::RF_INTF_ACTIVATED_NTF_INTERFACE); + uint8_t protocol = rx.get_message_byte(nfc::RF_INTF_ACTIVATED_NTF_PROTOCOL); + uint8_t mode_tech = rx.get_message_byte(nfc::RF_INTF_ACTIVATED_NTF_MODE_TECH); + uint8_t max_size = rx.get_message_byte(nfc::RF_INTF_ACTIVATED_NTF_MAX_SIZE); + + ESP_LOGVV(TAG, "Endpoint activated -- interface: 0x%02X, protocol: 0x%02X, mode&tech: 0x%02X, max payload: %u", + interface, protocol, mode_tech, max_size); + + if (mode_tech & nfc::MODE_LISTEN_MASK) { + ESP_LOGVV(TAG, "Tag activated in listen mode"); + this->nci_fsm_set_state_(NCIState::RFST_LISTEN_ACTIVE); + return; + } + + this->nci_fsm_set_state_(NCIState::RFST_POLL_ACTIVE); + auto incoming_tag = + this->build_tag_(mode_tech, std::vector(rx.get_message().begin() + 10, rx.get_message().end())); + + if (incoming_tag == nullptr) { + ESP_LOGE(TAG, "Could not build tag"); + } else { + auto tag_loc = this->find_tag_uid_(incoming_tag->get_uid()); + if (tag_loc.has_value()) { + this->discovered_endpoint_[tag_loc.value()].id = discovery_id; + this->discovered_endpoint_[tag_loc.value()].protocol = protocol; + this->discovered_endpoint_[tag_loc.value()].last_seen = millis(); + ESP_LOGVV(TAG, "Tag cache updated"); + } else { + this->discovered_endpoint_.emplace_back( + DiscoveredEndpoint{discovery_id, protocol, millis(), std::move(incoming_tag), false}); + tag_loc = this->discovered_endpoint_.size() - 1; + ESP_LOGVV(TAG, "Tag added to cache"); + } + + auto &working_endpoint = this->discovered_endpoint_[tag_loc.value()]; + + switch (this->next_task_) { + case EP_CLEAN: + ESP_LOGD(TAG, " Tag cleaning..."); + if (this->clean_endpoint_(working_endpoint.tag->get_uid()) != nfc::STATUS_OK) { + ESP_LOGE(TAG, " Tag cleaning incomplete"); + } + ESP_LOGD(TAG, " Tag cleaned!"); + break; + + case EP_FORMAT: + ESP_LOGD(TAG, " Tag formatting..."); + if (this->format_endpoint_(working_endpoint.tag->get_uid()) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error formatting tag as NDEF"); + } + ESP_LOGD(TAG, " Tag formatted!"); + break; + + case EP_WRITE: + if (this->next_task_message_to_write_ != nullptr) { + ESP_LOGD(TAG, " Tag writing..."); + ESP_LOGD(TAG, " Tag formatting..."); + if (this->format_endpoint_(working_endpoint.tag->get_uid()) != nfc::STATUS_OK) { + ESP_LOGE(TAG, " Tag could not be formatted for writing"); + } else { + ESP_LOGD(TAG, " Writing NDEF data"); + if (this->write_endpoint_(working_endpoint.tag->get_uid(), this->next_task_message_to_write_) != + nfc::STATUS_OK) { + ESP_LOGE(TAG, " Failed to write message to tag"); + } + ESP_LOGD(TAG, " Finished writing NDEF data"); + this->next_task_message_to_write_ = nullptr; + this->on_finished_write_callback_.call(); + } + } + break; + + case EP_READ: + default: + if (!working_endpoint.trig_called) { + ESP_LOGI(TAG, "Read tag type %s with UID %s", working_endpoint.tag->get_tag_type().c_str(), + nfc::format_uid(working_endpoint.tag->get_uid()).c_str()); + if (this->read_endpoint_data_(*working_endpoint.tag) != nfc::STATUS_OK) { + ESP_LOGW(TAG, " Unable to read NDEF record(s)"); + } else if (working_endpoint.tag->has_ndef_message()) { + const auto message = working_endpoint.tag->get_ndef_message(); + const auto records = message->get_records(); + ESP_LOGD(TAG, " NDEF record(s):"); + for (const auto &record : records) { + ESP_LOGD(TAG, " %s - %s", record->get_type().c_str(), record->get_payload().c_str()); + } + } else { + ESP_LOGW(TAG, " No NDEF records found"); + } + for (auto *trigger : this->triggers_ontag_) { + trigger->process(working_endpoint.tag); + } + working_endpoint.trig_called = true; + break; + } + } + if (working_endpoint.tag->get_tag_type() == nfc::MIFARE_CLASSIC) { + this->halt_mifare_classic_tag_(); + } + } + if (this->next_task_ != EP_READ) { + this->read_mode(); + } + + this->stop_discovery_(); + this->nci_fsm_set_state_(NCIState::EP_DEACTIVATING); +} + +void PN7160::process_rf_discover_oid_(nfc::NciMessage &rx) { + auto incoming_tag = this->build_tag_(rx.get_message_byte(nfc::RF_DISCOVER_NTF_MODE_TECH), + std::vector(rx.get_message().begin() + 7, rx.get_message().end())); + + if (incoming_tag == nullptr) { + ESP_LOGE(TAG, "Could not build tag!"); + } else { + auto tag_loc = this->find_tag_uid_(incoming_tag->get_uid()); + if (tag_loc.has_value()) { + this->discovered_endpoint_[tag_loc.value()].id = rx.get_message_byte(nfc::RF_DISCOVER_NTF_DISCOVERY_ID); + this->discovered_endpoint_[tag_loc.value()].protocol = rx.get_message_byte(nfc::RF_DISCOVER_NTF_PROTOCOL); + this->discovered_endpoint_[tag_loc.value()].last_seen = millis(); + ESP_LOGVV(TAG, "Tag found & updated"); + } else { + this->discovered_endpoint_.emplace_back(DiscoveredEndpoint{rx.get_message_byte(nfc::RF_DISCOVER_NTF_DISCOVERY_ID), + rx.get_message_byte(nfc::RF_DISCOVER_NTF_PROTOCOL), + millis(), std::move(incoming_tag), false}); + ESP_LOGVV(TAG, "Tag saved"); + } + } + + if (rx.get_message().back() != nfc::RF_DISCOVER_NTF_NT_MORE) { + this->nci_fsm_set_state_(NCIState::RFST_W4_HOST_SELECT); + ESP_LOGVV(TAG, "Discovered %u endpoints", this->discovered_endpoint_.size()); + } +} + +void PN7160::process_rf_deactivate_oid_(nfc::NciMessage &rx) { + this->ce_state_ = CardEmulationState::CARD_EMU_IDLE; + + switch (rx.get_simple_status_response()) { + case nfc::DEACTIVATION_TYPE_DISCOVERY: + this->nci_fsm_set_state_(NCIState::RFST_DISCOVERY); + break; + + case nfc::DEACTIVATION_TYPE_IDLE: + this->nci_fsm_set_state_(NCIState::RFST_IDLE); + break; + + case nfc::DEACTIVATION_TYPE_SLEEP: + case nfc::DEACTIVATION_TYPE_SLEEP_AF: + if (this->nci_state_ == NCIState::RFST_LISTEN_ACTIVE) { + this->nci_fsm_set_state_(NCIState::RFST_LISTEN_SLEEP); + } else if (this->nci_state_ == NCIState::RFST_POLL_ACTIVE) { + this->nci_fsm_set_state_(NCIState::RFST_W4_HOST_SELECT); + } else { + this->nci_fsm_set_state_(NCIState::RFST_IDLE); + } + break; + + default: + break; + } +} + +void PN7160::process_data_message_(nfc::NciMessage &rx) { + ESP_LOGVV(TAG, "Received data message: %s", nfc::format_bytes(rx.get_message()).c_str()); + + std::vector ndef_response; + this->card_emu_t4t_get_response_(rx.get_message(), ndef_response); + + uint16_t ndef_response_size = ndef_response.size(); + if (!ndef_response_size) { + return; // no message returned, we cannot respond + } + + std::vector tx_msg = {nfc::NCI_PKT_MT_DATA, uint8_t((ndef_response_size & 0xFF00) >> 8), + uint8_t(ndef_response_size & 0x00FF)}; + tx_msg.insert(tx_msg.end(), ndef_response.begin(), ndef_response.end()); + nfc::NciMessage tx(tx_msg); + ESP_LOGVV(TAG, "Sending data message: %s", nfc::format_bytes(tx.get_message()).c_str()); + if (this->transceive_(tx, rx, NFCC_DEFAULT_TIMEOUT, false) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Sending reply for card emulation failed"); + } +} + +void PN7160::card_emu_t4t_get_response_(std::vector &response, std::vector &ndef_response) { + if (this->card_emulation_message_ == nullptr) { + ESP_LOGE(TAG, "No NDEF message is set; tag emulation not possible"); + ndef_response.clear(); + return; + } + + if (equal(response.begin() + nfc::NCI_PKT_HEADER_SIZE, response.end(), std::begin(CARD_EMU_T4T_APP_SELECT))) { + // CARD_EMU_T4T_APP_SELECT + ESP_LOGVV(TAG, "CARD_EMU_NDEF_APP_SELECTED"); + this->ce_state_ = CardEmulationState::CARD_EMU_NDEF_APP_SELECTED; + ndef_response.insert(ndef_response.begin(), std::begin(CARD_EMU_T4T_OK), std::end(CARD_EMU_T4T_OK)); + } else if (equal(response.begin() + nfc::NCI_PKT_HEADER_SIZE, response.end(), std::begin(CARD_EMU_T4T_CC_SELECT))) { + // CARD_EMU_T4T_CC_SELECT + if (this->ce_state_ == CardEmulationState::CARD_EMU_NDEF_APP_SELECTED) { + ESP_LOGVV(TAG, "CARD_EMU_CC_SELECTED"); + this->ce_state_ = CardEmulationState::CARD_EMU_CC_SELECTED; + ndef_response.insert(ndef_response.begin(), std::begin(CARD_EMU_T4T_OK), std::end(CARD_EMU_T4T_OK)); + } + } else if (equal(response.begin() + nfc::NCI_PKT_HEADER_SIZE, response.end(), std::begin(CARD_EMU_T4T_NDEF_SELECT))) { + // CARD_EMU_T4T_NDEF_SELECT + ESP_LOGVV(TAG, "CARD_EMU_NDEF_SELECTED"); + this->ce_state_ = CardEmulationState::CARD_EMU_NDEF_SELECTED; + ndef_response.insert(ndef_response.begin(), std::begin(CARD_EMU_T4T_OK), std::end(CARD_EMU_T4T_OK)); + } else if (equal(response.begin() + nfc::NCI_PKT_HEADER_SIZE, + response.begin() + nfc::NCI_PKT_HEADER_SIZE + sizeof(CARD_EMU_T4T_READ), + std::begin(CARD_EMU_T4T_READ))) { + // CARD_EMU_T4T_READ + if (this->ce_state_ == CardEmulationState::CARD_EMU_CC_SELECTED) { + // CARD_EMU_T4T_READ with CARD_EMU_CC_SELECTED + ESP_LOGVV(TAG, "CARD_EMU_T4T_READ with CARD_EMU_CC_SELECTED"); + uint16_t offset = (response[nfc::NCI_PKT_HEADER_SIZE + 2] << 8) + response[nfc::NCI_PKT_HEADER_SIZE + 3]; + uint8_t length = response[nfc::NCI_PKT_HEADER_SIZE + 4]; + + if (length <= (sizeof(CARD_EMU_T4T_CC) + offset + 2)) { + ndef_response.insert(ndef_response.begin(), std::begin(CARD_EMU_T4T_CC) + offset, + std::begin(CARD_EMU_T4T_CC) + offset + length); + ndef_response.insert(ndef_response.end(), std::begin(CARD_EMU_T4T_OK), std::end(CARD_EMU_T4T_OK)); + } + } else if (this->ce_state_ == CardEmulationState::CARD_EMU_NDEF_SELECTED) { + // CARD_EMU_T4T_READ with CARD_EMU_NDEF_SELECTED + ESP_LOGVV(TAG, "CARD_EMU_T4T_READ with CARD_EMU_NDEF_SELECTED"); + auto ndef_message = this->card_emulation_message_->encode(); + uint16_t ndef_msg_size = ndef_message.size(); + uint16_t offset = (response[nfc::NCI_PKT_HEADER_SIZE + 2] << 8) + response[nfc::NCI_PKT_HEADER_SIZE + 3]; + uint8_t length = response[nfc::NCI_PKT_HEADER_SIZE + 4]; + + ESP_LOGVV(TAG, "Encoded NDEF message: %s", nfc::format_bytes(ndef_message).c_str()); + + if (length <= (ndef_msg_size + offset + 2)) { + if (offset == 0) { + ndef_response.resize(2); + ndef_response[0] = (ndef_msg_size & 0xFF00) >> 8; + ndef_response[1] = (ndef_msg_size & 0x00FF); + if (length > 2) { + ndef_response.insert(ndef_response.end(), ndef_message.begin(), ndef_message.begin() + length - 2); + } + } else if (offset == 1) { + ndef_response.resize(1); + ndef_response[0] = (ndef_msg_size & 0x00FF); + if (length > 1) { + ndef_response.insert(ndef_response.end(), ndef_message.begin(), ndef_message.begin() + length - 1); + } + } else { + ndef_response.insert(ndef_response.end(), ndef_message.begin(), ndef_message.begin() + length); + } + + ndef_response.insert(ndef_response.end(), std::begin(CARD_EMU_T4T_OK), std::end(CARD_EMU_T4T_OK)); + + if ((offset + length) >= (ndef_msg_size + 2)) { + ESP_LOGD(TAG, "NDEF message sent"); + this->on_emulated_tag_scan_callback_.call(); + } + } + } + } else if (equal(response.begin() + nfc::NCI_PKT_HEADER_SIZE, + response.begin() + nfc::NCI_PKT_HEADER_SIZE + sizeof(CARD_EMU_T4T_WRITE), + std::begin(CARD_EMU_T4T_WRITE))) { + // CARD_EMU_T4T_WRITE + if (this->ce_state_ == CardEmulationState::CARD_EMU_NDEF_SELECTED) { + ESP_LOGVV(TAG, "CARD_EMU_T4T_WRITE"); + uint8_t length = response[nfc::NCI_PKT_HEADER_SIZE + 4]; + std::vector ndef_msg_written; + + ndef_msg_written.insert(ndef_msg_written.end(), response.begin() + nfc::NCI_PKT_HEADER_SIZE + 5, + response.begin() + nfc::NCI_PKT_HEADER_SIZE + 5 + length); + ESP_LOGD(TAG, "Received %u-byte NDEF message: %s", length, nfc::format_bytes(ndef_msg_written).c_str()); + ndef_response.insert(ndef_response.end(), std::begin(CARD_EMU_T4T_OK), std::end(CARD_EMU_T4T_OK)); + } + } +} + +uint8_t PN7160::transceive_(nfc::NciMessage &tx, nfc::NciMessage &rx, const uint16_t timeout, + const bool expect_notification) { + uint8_t retries = NFCC_MAX_COMM_FAILS; + + while (retries) { + // first, send the message we need to send + if (this->write_nfcc(tx) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error sending message"); + return nfc::STATUS_FAILED; + } + ESP_LOGVV(TAG, "Wrote: %s", nfc::format_bytes(tx.get_message()).c_str()); + // next, the NFCC should send back a response + if (this->read_nfcc(rx, timeout) != nfc::STATUS_OK) { + ESP_LOGW(TAG, "Error receiving message"); + if (!retries--) { + ESP_LOGE(TAG, " ...giving up"); + return nfc::STATUS_FAILED; + } + } else { + break; + } + } + ESP_LOGVV(TAG, "Read: %s", nfc::format_bytes(rx.get_message()).c_str()); + // validate the response based on the message type that was sent (command vs. data) + if (!tx.message_type_is(nfc::NCI_PKT_MT_DATA)) { + // for commands, the GID and OID should match and the status should be OK + if ((rx.get_gid() != tx.get_gid()) || (rx.get_oid()) != tx.get_oid()) { + ESP_LOGE(TAG, "Incorrect response to command: %s", nfc::format_bytes(rx.get_message()).c_str()); + return nfc::STATUS_FAILED; + } + + if (!rx.simple_status_response_is(nfc::STATUS_OK)) { + ESP_LOGE(TAG, "Error in response to command: %s", nfc::format_bytes(rx.get_message()).c_str()); + } + return rx.get_simple_status_response(); + } else { + // when requesting data from the endpoint, the first response is from the NFCC; we must validate this, first + if ((!rx.message_type_is(nfc::NCI_PKT_MT_CTRL_NOTIFICATION)) || (!rx.gid_is(nfc::NCI_CORE_GID)) || + (!rx.oid_is(nfc::NCI_CORE_CONN_CREDITS_OID)) || (!rx.message_length_is(3))) { + ESP_LOGE(TAG, "Incorrect response to data message: %s", nfc::format_bytes(rx.get_message()).c_str()); + return nfc::STATUS_FAILED; + } + + if (expect_notification) { + // if the NFCC said "OK", there will be additional data to read; this comes back in a notification message + if (this->read_nfcc(rx, timeout) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error receiving data from endpoint"); + return nfc::STATUS_FAILED; + } + ESP_LOGVV(TAG, "Read: %s", nfc::format_bytes(rx.get_message()).c_str()); + } + + return nfc::STATUS_OK; + } +} + +uint8_t PN7160::wait_for_irq_(uint16_t timeout, bool pin_state) { + auto start_time = millis(); + + while (millis() - start_time < timeout) { + if (this->irq_pin_->digital_read() == pin_state) { + return nfc::STATUS_OK; + } + } + ESP_LOGW(TAG, "Timed out waiting for IRQ state"); + return nfc::STATUS_FAILED; +} + +} // namespace pn7160 +} // namespace esphome diff --git a/esphome/components/pn7160/pn7160.h b/esphome/components/pn7160/pn7160.h new file mode 100644 index 0000000000..2b3cb99453 --- /dev/null +++ b/esphome/components/pn7160/pn7160.h @@ -0,0 +1,315 @@ +#pragma once + +#include "esphome/components/nfc/automation.h" +#include "esphome/components/nfc/nci_core.h" +#include "esphome/components/nfc/nci_message.h" +#include "esphome/components/nfc/nfc.h" +#include "esphome/components/nfc/nfc_helpers.h" +#include "esphome/core/component.h" +#include "esphome/core/gpio.h" +#include "esphome/core/helpers.h" + +#include + +namespace esphome { +namespace pn7160 { + +static const uint16_t NFCC_DEFAULT_TIMEOUT = 10; +static const uint16_t NFCC_INIT_TIMEOUT = 50; +static const uint16_t NFCC_TAG_WRITE_TIMEOUT = 15; + +static const uint8_t NFCC_MAX_COMM_FAILS = 3; +static const uint8_t NFCC_MAX_ERROR_COUNT = 10; + +static const uint8_t XCHG_DATA_OID = 0x10; +static const uint8_t MF_SECTORSEL_OID = 0x32; +static const uint8_t MFC_AUTHENTICATE_OID = 0x40; +static const uint8_t TEST_PRBS_OID = 0x30; +static const uint8_t TEST_ANTENNA_OID = 0x3D; +static const uint8_t TEST_GET_REGISTER_OID = 0x33; + +static const uint8_t MFC_AUTHENTICATE_PARAM_KS_A = 0x00; // key select A +static const uint8_t MFC_AUTHENTICATE_PARAM_KS_B = 0x80; // key select B +static const uint8_t MFC_AUTHENTICATE_PARAM_EMBED_KEY = 0x10; + +static const uint8_t CARD_EMU_T4T_APP_SELECT[] = {0x00, 0xA4, 0x04, 0x00, 0x07, 0xD2, 0x76, + 0x00, 0x00, 0x85, 0x01, 0x01, 0x00}; +static const uint8_t CARD_EMU_T4T_CC[] = {0x00, 0x0F, 0x20, 0x00, 0xFF, 0x00, 0xFF, 0x04, + 0x06, 0xE1, 0x04, 0x00, 0xFF, 0x00, 0x00}; +static const uint8_t CARD_EMU_T4T_CC_SELECT[] = {0x00, 0xA4, 0x00, 0x0C, 0x02, 0xE1, 0x03}; +static const uint8_t CARD_EMU_T4T_NDEF_SELECT[] = {0x00, 0xA4, 0x00, 0x0C, 0x02, 0xE1, 0x04}; +static const uint8_t CARD_EMU_T4T_READ[] = {0x00, 0xB0}; +static const uint8_t CARD_EMU_T4T_WRITE[] = {0x00, 0xD6}; +static const uint8_t CARD_EMU_T4T_OK[] = {0x90, 0x00}; +static const uint8_t CARD_EMU_T4T_NOK[] = {0x6A, 0x82}; + +static const uint8_t CORE_CONFIG_SOLO[] = {0x01, // Number of parameter fields + 0x00, // config param identifier (TOTAL_DURATION) + 0x02, // length of value + 0x01, // TOTAL_DURATION (low)... + 0x00}; // TOTAL_DURATION (high): 1 ms + +static const uint8_t CORE_CONFIG_RW_CE[] = {0x01, // Number of parameter fields + 0x00, // config param identifier (TOTAL_DURATION) + 0x02, // length of value + 0xF8, // TOTAL_DURATION (low)... + 0x02}; // TOTAL_DURATION (high): 760 ms + +static const uint8_t PMU_CFG[] = { + 0x01, // Number of parameters + 0xA0, 0x0E, // ext. tag + 11, // length + 0x11, // IRQ Enable: PVDD + temp sensor IRQs + 0x01, // RFU + 0x01, // Power and Clock Configuration, device on (CFG1) + 0x01, // Power and Clock Configuration, device off (CFG1) + 0x00, // RFU + 0x00, // DC-DC 0 + 0x00, // DC-DC 1 + // 0x14, // TXLDO (3.3V / 4.75V) + // 0xBB, // TXLDO (4.7V / 4.7V) + 0xFF, // TXLDO (5.0V / 5.0V) + 0x00, // RFU + 0xD0, // TXLDO check + 0x0C, // RFU +}; + +static const uint8_t RF_DISCOVER_MAP_CONFIG[] = { // poll modes + nfc::PROT_T1T, nfc::RF_DISCOVER_MAP_MODE_POLL, + nfc::INTF_FRAME, // poll mode + nfc::PROT_T2T, nfc::RF_DISCOVER_MAP_MODE_POLL, + nfc::INTF_FRAME, // poll mode + nfc::PROT_T3T, nfc::RF_DISCOVER_MAP_MODE_POLL, + nfc::INTF_FRAME, // poll mode + nfc::PROT_ISODEP, nfc::RF_DISCOVER_MAP_MODE_POLL | nfc::RF_DISCOVER_MAP_MODE_LISTEN, + nfc::INTF_ISODEP, // poll & listen mode + nfc::PROT_MIFARE, nfc::RF_DISCOVER_MAP_MODE_POLL, + nfc::INTF_TAGCMD}; // poll mode + +static const uint8_t RF_DISCOVERY_LISTEN_CONFIG[] = {nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCA, // listen mode + nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCB, // listen mode + nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCF}; // listen mode + +static const uint8_t RF_DISCOVERY_POLL_CONFIG[] = {nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCA, // poll mode + nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCB, // poll mode + nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCF}; // poll mode + +static const uint8_t RF_DISCOVERY_CONFIG[] = {nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCA, // poll mode + nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCB, // poll mode + nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCF, // poll mode + nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCA, // listen mode + nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCB, // listen mode + nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCF}; // listen mode + +static const uint8_t RF_LISTEN_MODE_ROUTING_CONFIG[] = {0x00, // "more" (another message is coming) + 2, // number of table entries + 0x01, // type = protocol-based + 3, // length + 0, // DH NFCEE ID, a static ID representing the DH-NFCEE + 0x07, // power state + nfc::PROT_ISODEP, // protocol + 0x00, // type = technology-based + 3, // length + 0, // DH NFCEE ID, a static ID representing the DH-NFCEE + 0x07, // power state + nfc::TECH_PASSIVE_NFCA}; // technology + +enum class CardEmulationState : uint8_t { + CARD_EMU_IDLE, + CARD_EMU_NDEF_APP_SELECTED, + CARD_EMU_CC_SELECTED, + CARD_EMU_NDEF_SELECTED, + CARD_EMU_DESFIRE_PROD, +}; + +enum class NCIState : uint8_t { + NONE = 0x00, + NFCC_RESET, + NFCC_INIT, + NFCC_CONFIG, + NFCC_SET_DISCOVER_MAP, + NFCC_SET_LISTEN_MODE_ROUTING, + RFST_IDLE, + RFST_DISCOVERY, + RFST_W4_ALL_DISCOVERIES, + RFST_W4_HOST_SELECT, + RFST_LISTEN_ACTIVE, + RFST_LISTEN_SLEEP, + RFST_POLL_ACTIVE, + EP_DEACTIVATING, + EP_SELECTING, + TEST = 0XFE, + FAILED = 0XFF, +}; + +enum class TestMode : uint8_t { + TEST_NONE = 0x00, + TEST_PRBS, + TEST_ANTENNA, + TEST_GET_REGISTER, +}; + +struct DiscoveredEndpoint { + uint8_t id; + uint8_t protocol; + uint32_t last_seen; + std::unique_ptr tag; + bool trig_called; +}; + +class PN7160 : public Component { + public: + void setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + void loop() override; + + void set_dwl_req_pin(GPIOPin *dwl_req_pin) { this->dwl_req_pin_ = dwl_req_pin; } + void set_irq_pin(GPIOPin *irq_pin) { this->irq_pin_ = irq_pin; } + void set_ven_pin(GPIOPin *ven_pin) { this->ven_pin_ = ven_pin; } + void set_wkup_req_pin(GPIOPin *wkup_req_pin) { this->wkup_req_pin_ = wkup_req_pin; } + + void set_tag_ttl(uint32_t ttl) { this->tag_ttl_ = ttl; } + void set_tag_emulation_message(std::shared_ptr message); + void set_tag_emulation_message(const optional &message, optional include_android_app_record); + void set_tag_emulation_message(const char *message, bool include_android_app_record = true); + void set_tag_emulation_off(); + void set_tag_emulation_on(); + bool tag_emulation_enabled() { return this->listening_enabled_; } + + void set_polling_off(); + void set_polling_on(); + bool polling_enabled() { return this->polling_enabled_; } + + void register_ontag_trigger(nfc::NfcOnTagTrigger *trig) { this->triggers_ontag_.push_back(trig); } + void register_ontagremoved_trigger(nfc::NfcOnTagTrigger *trig) { this->triggers_ontagremoved_.push_back(trig); } + + void add_on_emulated_tag_scan_callback(std::function callback) { + this->on_emulated_tag_scan_callback_.add(std::move(callback)); + } + + void add_on_finished_write_callback(std::function callback) { + this->on_finished_write_callback_.add(std::move(callback)); + } + + bool is_writing() { return this->next_task_ != EP_READ; }; + + void read_mode(); + void clean_mode(); + void format_mode(); + void write_mode(); + void set_tag_write_message(std::shared_ptr message); + void set_tag_write_message(optional message, optional include_android_app_record); + + uint8_t set_test_mode(TestMode test_mode, const std::vector &data, std::vector &result); + + protected: + uint8_t reset_core_(bool reset_config, bool power); + uint8_t init_core_(); + uint8_t send_init_config_(); + uint8_t send_core_config_(); + uint8_t refresh_core_config_(); + + uint8_t set_discover_map_(); + + uint8_t set_listen_mode_routing_(); + + uint8_t start_discovery_(); + uint8_t stop_discovery_(); + uint8_t deactivate_(uint8_t type, uint16_t timeout = NFCC_DEFAULT_TIMEOUT); + + void select_endpoint_(); + + uint8_t read_endpoint_data_(nfc::NfcTag &tag); + uint8_t clean_endpoint_(std::vector &uid); + uint8_t format_endpoint_(std::vector &uid); + uint8_t write_endpoint_(std::vector &uid, std::shared_ptr &message); + + std::unique_ptr build_tag_(uint8_t mode_tech, const std::vector &data); + optional find_tag_uid_(const std::vector &uid); + void purge_old_tags_(); + void erase_tag_(uint8_t tag_index); + + /// advance controller state as required + void nci_fsm_transition_(); + /// set new controller state + void nci_fsm_set_state_(NCIState new_state); + /// setting controller to this state caused an error; returns true if too many errors/failures + bool nci_fsm_set_error_state_(NCIState new_state); + /// parse & process incoming messages from the NFCC + void process_message_(); + void process_rf_intf_activated_oid_(nfc::NciMessage &rx); + void process_rf_discover_oid_(nfc::NciMessage &rx); + void process_rf_deactivate_oid_(nfc::NciMessage &rx); + void process_data_message_(nfc::NciMessage &rx); + + void card_emu_t4t_get_response_(std::vector &response, std::vector &ndef_response); + + uint8_t transceive_(nfc::NciMessage &tx, nfc::NciMessage &rx, uint16_t timeout = NFCC_DEFAULT_TIMEOUT, + bool expect_notification = true); + virtual uint8_t read_nfcc(nfc::NciMessage &rx, uint16_t timeout) = 0; + virtual uint8_t write_nfcc(nfc::NciMessage &tx) = 0; + + uint8_t wait_for_irq_(uint16_t timeout = NFCC_DEFAULT_TIMEOUT, bool pin_state = true); + + uint8_t read_mifare_classic_tag_(nfc::NfcTag &tag); + uint8_t read_mifare_classic_block_(uint8_t block_num, std::vector &data); + uint8_t write_mifare_classic_block_(uint8_t block_num, std::vector &data); + uint8_t auth_mifare_classic_block_(uint8_t block_num, uint8_t key_num, const uint8_t *key); + uint8_t sect_to_auth_(uint8_t block_num); + uint8_t format_mifare_classic_mifare_(); + uint8_t format_mifare_classic_ndef_(); + uint8_t write_mifare_classic_tag_(const std::shared_ptr &message); + uint8_t halt_mifare_classic_tag_(); + + uint8_t read_mifare_ultralight_tag_(nfc::NfcTag &tag); + uint8_t read_mifare_ultralight_bytes_(uint8_t start_page, uint16_t num_bytes, std::vector &data); + bool is_mifare_ultralight_formatted_(const std::vector &page_3_to_6); + uint16_t read_mifare_ultralight_capacity_(); + uint8_t find_mifare_ultralight_ndef_(const std::vector &page_3_to_6, uint8_t &message_length, + uint8_t &message_start_index); + uint8_t write_mifare_ultralight_page_(uint8_t page_num, std::vector &write_data); + uint8_t write_mifare_ultralight_tag_(std::vector &uid, const std::shared_ptr &message); + uint8_t clean_mifare_ultralight_(); + + enum NfcTask : uint8_t { + EP_READ = 0, + EP_CLEAN, + EP_FORMAT, + EP_WRITE, + } next_task_{EP_READ}; + + bool config_refresh_pending_{false}; + bool core_config_is_solo_{false}; + bool listening_enabled_{false}; + bool polling_enabled_{true}; + + uint8_t error_count_{0}; + uint8_t fail_count_{0}; + uint32_t last_nci_state_change_{0}; + uint8_t selecting_endpoint_{0}; + uint32_t tag_ttl_{250}; + + GPIOPin *dwl_req_pin_{nullptr}; + GPIOPin *irq_pin_{nullptr}; + GPIOPin *ven_pin_{nullptr}; + GPIOPin *wkup_req_pin_{nullptr}; + + CallbackManager on_emulated_tag_scan_callback_; + CallbackManager on_finished_write_callback_; + + std::vector discovered_endpoint_; + + CardEmulationState ce_state_{CardEmulationState::CARD_EMU_IDLE}; + NCIState nci_state_{NCIState::NFCC_RESET}; + NCIState nci_state_error_{NCIState::NONE}; + + std::shared_ptr card_emulation_message_; + std::shared_ptr next_task_message_to_write_; + + std::vector triggers_ontag_; + std::vector triggers_ontagremoved_; +}; + +} // namespace pn7160 +} // namespace esphome diff --git a/esphome/components/pn7160/pn7160_mifare_classic.cpp b/esphome/components/pn7160/pn7160_mifare_classic.cpp new file mode 100644 index 0000000000..fa63cc00d5 --- /dev/null +++ b/esphome/components/pn7160/pn7160_mifare_classic.cpp @@ -0,0 +1,322 @@ +#include + +#include "pn7160.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace pn7160 { + +static const char *const TAG = "pn7160.mifare_classic"; + +uint8_t PN7160::read_mifare_classic_tag_(nfc::NfcTag &tag) { + uint8_t current_block = 4; + uint8_t message_start_index = 0; + uint32_t message_length = 0; + + if (this->auth_mifare_classic_block_(current_block, nfc::MIFARE_CMD_AUTH_A, nfc::NDEF_KEY) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Tag auth failed while attempting to read tag data"); + return nfc::STATUS_FAILED; + } + std::vector data; + + if (this->read_mifare_classic_block_(current_block, data) == nfc::STATUS_OK) { + if (!nfc::decode_mifare_classic_tlv(data, message_length, message_start_index)) { + return nfc::STATUS_FAILED; + } + } else { + ESP_LOGE(TAG, "Failed to read block %u", current_block); + return nfc::STATUS_FAILED; + } + + uint32_t index = 0; + uint32_t buffer_size = nfc::get_mifare_classic_buffer_size(message_length); + std::vector buffer; + + while (index < buffer_size) { + if (nfc::mifare_classic_is_first_block(current_block)) { + if (this->auth_mifare_classic_block_(current_block, nfc::MIFARE_CMD_AUTH_A, nfc::NDEF_KEY) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Block authentication failed for %u", current_block); + return nfc::STATUS_FAILED; + } + } + std::vector block_data; + if (this->read_mifare_classic_block_(current_block, block_data) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error reading block %u", current_block); + return nfc::STATUS_FAILED; + } else { + buffer.insert(buffer.end(), block_data.begin(), block_data.end()); + } + + index += nfc::MIFARE_CLASSIC_BLOCK_SIZE; + current_block++; + + if (nfc::mifare_classic_is_trailer_block(current_block)) { + current_block++; + } + } + + if (buffer.begin() + message_start_index < buffer.end()) { + buffer.erase(buffer.begin(), buffer.begin() + message_start_index); + } else { + return nfc::STATUS_FAILED; + } + + tag.set_ndef_message(make_unique(buffer)); + + return nfc::STATUS_OK; +} + +uint8_t PN7160::read_mifare_classic_block_(uint8_t block_num, std::vector &data) { + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, {XCHG_DATA_OID, nfc::MIFARE_CMD_READ, block_num}); + + ESP_LOGVV(TAG, "Read XCHG_DATA_REQ: %s", nfc::format_bytes(tx.get_message()).c_str()); + if (this->transceive_(tx, rx) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Timeout reading tag data"); + return nfc::STATUS_FAILED; + } + + if ((!rx.message_type_is(nfc::NCI_PKT_MT_DATA)) || (!rx.simple_status_response_is(XCHG_DATA_OID)) || + (!rx.message_length_is(18))) { + ESP_LOGE(TAG, "MFC read block failed - block 0x%02x", block_num); + ESP_LOGV(TAG, "Read response: %s", nfc::format_bytes(rx.get_message()).c_str()); + return nfc::STATUS_FAILED; + } + + data.insert(data.begin(), rx.get_message().begin() + 4, rx.get_message().end() - 1); + + ESP_LOGVV(TAG, " Block %u: %s", block_num, nfc::format_bytes(data).c_str()); + return nfc::STATUS_OK; +} + +uint8_t PN7160::auth_mifare_classic_block_(uint8_t block_num, uint8_t key_num, const uint8_t *key) { + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, {MFC_AUTHENTICATE_OID, this->sect_to_auth_(block_num), key_num}); + + switch (key_num) { + case nfc::MIFARE_CMD_AUTH_A: + tx.get_message().back() = MFC_AUTHENTICATE_PARAM_KS_A; + break; + + case nfc::MIFARE_CMD_AUTH_B: + tx.get_message().back() = MFC_AUTHENTICATE_PARAM_KS_B; + break; + + default: + break; + } + + if (key != nullptr) { + tx.get_message().back() |= MFC_AUTHENTICATE_PARAM_EMBED_KEY; + tx.get_message().insert(tx.get_message().end(), key, key + 6); + } + + ESP_LOGVV(TAG, "MFC_AUTHENTICATE_REQ: %s", nfc::format_bytes(tx.get_message()).c_str()); + if (this->transceive_(tx, rx) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Sending MFC_AUTHENTICATE_REQ failed"); + return nfc::STATUS_FAILED; + } + if ((!rx.message_type_is(nfc::NCI_PKT_MT_DATA)) || (!rx.simple_status_response_is(MFC_AUTHENTICATE_OID)) || + (rx.get_message()[4] != nfc::STATUS_OK)) { + ESP_LOGE(TAG, "MFC authentication failed - block 0x%02x", block_num); + ESP_LOGVV(TAG, "MFC_AUTHENTICATE_RSP: %s", nfc::format_bytes(rx.get_message()).c_str()); + return nfc::STATUS_FAILED; + } + + ESP_LOGV(TAG, "MFC block %u authentication succeeded", block_num); + return nfc::STATUS_OK; +} + +uint8_t PN7160::sect_to_auth_(const uint8_t block_num) { + const uint8_t first_high_block = nfc::MIFARE_CLASSIC_BLOCKS_PER_SECT_LOW * nfc::MIFARE_CLASSIC_16BLOCK_SECT_START; + if (block_num >= first_high_block) { + return ((block_num - first_high_block) / nfc::MIFARE_CLASSIC_BLOCKS_PER_SECT_HIGH) + + nfc::MIFARE_CLASSIC_16BLOCK_SECT_START; + } + return block_num / nfc::MIFARE_CLASSIC_BLOCKS_PER_SECT_LOW; +} + +uint8_t PN7160::format_mifare_classic_mifare_() { + std::vector blank_buffer( + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); + std::vector trailer_buffer( + {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x07, 0x80, 0x69, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}); + + auto status = nfc::STATUS_OK; + + for (int block = 0; block < 64; block += 4) { + if (this->auth_mifare_classic_block_(block + 3, nfc::MIFARE_CMD_AUTH_B, nfc::DEFAULT_KEY) != nfc::STATUS_OK) { + continue; + } + if (block != 0) { + if (this->write_mifare_classic_block_(block, blank_buffer) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Unable to write block %u", block); + status = nfc::STATUS_FAILED; + } + } + if (this->write_mifare_classic_block_(block + 1, blank_buffer) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Unable to write block %u", block + 1); + status = nfc::STATUS_FAILED; + } + if (this->write_mifare_classic_block_(block + 2, blank_buffer) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Unable to write block %u", block + 2); + status = nfc::STATUS_FAILED; + } + if (this->write_mifare_classic_block_(block + 3, trailer_buffer) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Unable to write block %u", block + 3); + status = nfc::STATUS_FAILED; + } + } + + return status; +} + +uint8_t PN7160::format_mifare_classic_ndef_() { + std::vector empty_ndef_message( + {0x03, 0x03, 0xD0, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); + std::vector blank_block( + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); + std::vector block_1_data( + {0x14, 0x01, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1}); + std::vector block_2_data( + {0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1}); + std::vector block_3_trailer( + {0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0x78, 0x77, 0x88, 0xC1, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}); + std::vector ndef_trailer( + {0xD3, 0xF7, 0xD3, 0xF7, 0xD3, 0xF7, 0x7F, 0x07, 0x88, 0x40, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}); + + if (this->auth_mifare_classic_block_(0, nfc::MIFARE_CMD_AUTH_B, nfc::DEFAULT_KEY) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Unable to authenticate block 0 for formatting"); + return nfc::STATUS_FAILED; + } + if (this->write_mifare_classic_block_(1, block_1_data) != nfc::STATUS_OK) { + return nfc::STATUS_FAILED; + } + if (this->write_mifare_classic_block_(2, block_2_data) != nfc::STATUS_OK) { + return nfc::STATUS_FAILED; + } + if (this->write_mifare_classic_block_(3, block_3_trailer) != nfc::STATUS_OK) { + return nfc::STATUS_FAILED; + } + + ESP_LOGD(TAG, "Sector 0 formatted with NDEF"); + + auto status = nfc::STATUS_OK; + + for (int block = 4; block < 64; block += 4) { + if (this->auth_mifare_classic_block_(block + 3, nfc::MIFARE_CMD_AUTH_B, nfc::DEFAULT_KEY) != nfc::STATUS_OK) { + return nfc::STATUS_FAILED; + } + if (block == 4) { + if (this->write_mifare_classic_block_(block, empty_ndef_message) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Unable to write block %u", block); + status = nfc::STATUS_FAILED; + } + } else { + if (this->write_mifare_classic_block_(block, blank_block) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Unable to write block %u", block); + status = nfc::STATUS_FAILED; + } + } + if (this->write_mifare_classic_block_(block + 1, blank_block) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Unable to write block %u", block + 1); + status = nfc::STATUS_FAILED; + } + if (this->write_mifare_classic_block_(block + 2, blank_block) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Unable to write block %u", block + 2); + status = nfc::STATUS_FAILED; + } + if (this->write_mifare_classic_block_(block + 3, ndef_trailer) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Unable to write trailer block %u", block + 3); + status = nfc::STATUS_FAILED; + } + } + return status; +} + +uint8_t PN7160::write_mifare_classic_block_(uint8_t block_num, std::vector &write_data) { + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, {XCHG_DATA_OID, nfc::MIFARE_CMD_WRITE, block_num}); + + ESP_LOGVV(TAG, "Write XCHG_DATA_REQ 1: %s", nfc::format_bytes(tx.get_message()).c_str()); + if (this->transceive_(tx, rx) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Sending XCHG_DATA_REQ failed"); + return nfc::STATUS_FAILED; + } + // write command part two + tx.set_payload({XCHG_DATA_OID}); + tx.get_message().insert(tx.get_message().end(), write_data.begin(), write_data.end()); + + ESP_LOGVV(TAG, "Write XCHG_DATA_REQ 2: %s", nfc::format_bytes(tx.get_message()).c_str()); + if (this->transceive_(tx, rx, NFCC_TAG_WRITE_TIMEOUT) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "MFC XCHG_DATA timed out waiting for XCHG_DATA_RSP during block write"); + return nfc::STATUS_FAILED; + } + + if ((!rx.message_type_is(nfc::NCI_PKT_MT_DATA)) || (!rx.simple_status_response_is(XCHG_DATA_OID)) || + (rx.get_message()[4] != nfc::MIFARE_CMD_ACK)) { + ESP_LOGE(TAG, "MFC write block failed - block 0x%02x", block_num); + ESP_LOGV(TAG, "Write response: %s", nfc::format_bytes(rx.get_message()).c_str()); + return nfc::STATUS_FAILED; + } + + return nfc::STATUS_OK; +} + +uint8_t PN7160::write_mifare_classic_tag_(const std::shared_ptr &message) { + auto encoded = message->encode(); + + uint32_t message_length = encoded.size(); + uint32_t buffer_length = nfc::get_mifare_classic_buffer_size(message_length); + + encoded.insert(encoded.begin(), 0x03); + if (message_length < 255) { + encoded.insert(encoded.begin() + 1, message_length); + } else { + encoded.insert(encoded.begin() + 1, 0xFF); + encoded.insert(encoded.begin() + 2, (message_length >> 8) & 0xFF); + encoded.insert(encoded.begin() + 3, message_length & 0xFF); + } + encoded.push_back(0xFE); + + encoded.resize(buffer_length, 0); + + uint32_t index = 0; + uint8_t current_block = 4; + + while (index < buffer_length) { + if (nfc::mifare_classic_is_first_block(current_block)) { + if (this->auth_mifare_classic_block_(current_block, nfc::MIFARE_CMD_AUTH_A, nfc::NDEF_KEY) != nfc::STATUS_OK) { + return nfc::STATUS_FAILED; + } + } + + std::vector data(encoded.begin() + index, encoded.begin() + index + nfc::MIFARE_CLASSIC_BLOCK_SIZE); + if (this->write_mifare_classic_block_(current_block, data) != nfc::STATUS_OK) { + return nfc::STATUS_FAILED; + } + index += nfc::MIFARE_CLASSIC_BLOCK_SIZE; + current_block++; + + if (nfc::mifare_classic_is_trailer_block(current_block)) { + // Skipping as cannot write to trailer + current_block++; + } + } + return nfc::STATUS_OK; +} + +uint8_t PN7160::halt_mifare_classic_tag_() { + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, {XCHG_DATA_OID, nfc::MIFARE_CMD_HALT, 0}); + + ESP_LOGVV(TAG, "Halt XCHG_DATA_REQ: %s", nfc::format_bytes(tx.get_message()).c_str()); + if (this->transceive_(tx, rx, NFCC_TAG_WRITE_TIMEOUT) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Sending halt XCHG_DATA_REQ failed"); + return nfc::STATUS_FAILED; + } + return nfc::STATUS_OK; +} + +} // namespace pn7160 +} // namespace esphome diff --git a/esphome/components/pn7160/pn7160_mifare_ultralight.cpp b/esphome/components/pn7160/pn7160_mifare_ultralight.cpp new file mode 100644 index 0000000000..a74f23d4f2 --- /dev/null +++ b/esphome/components/pn7160/pn7160_mifare_ultralight.cpp @@ -0,0 +1,186 @@ +#include +#include + +#include "pn7160.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace pn7160 { + +static const char *const TAG = "pn7160.mifare_ultralight"; + +uint8_t PN7160::read_mifare_ultralight_tag_(nfc::NfcTag &tag) { + std::vector data; + // pages 3 to 6 contain various info we are interested in -- do one read to grab it all + if (this->read_mifare_ultralight_bytes_(3, nfc::MIFARE_ULTRALIGHT_PAGE_SIZE * nfc::MIFARE_ULTRALIGHT_READ_SIZE, + data) != nfc::STATUS_OK) { + return nfc::STATUS_FAILED; + } + + if (!this->is_mifare_ultralight_formatted_(data)) { + ESP_LOGW(TAG, "Not NDEF formatted"); + return nfc::STATUS_FAILED; + } + + uint8_t message_length; + uint8_t message_start_index; + if (this->find_mifare_ultralight_ndef_(data, message_length, message_start_index) != nfc::STATUS_OK) { + ESP_LOGW(TAG, "Couldn't find NDEF message"); + return nfc::STATUS_FAILED; + } + ESP_LOGVV(TAG, "NDEF message length: %u, start: %u", message_length, message_start_index); + + if (message_length == 0) { + return nfc::STATUS_FAILED; + } + // we already read pages 3-6 earlier -- pick up where we left off so we're not re-reading pages + const uint8_t read_length = message_length + message_start_index > 12 ? message_length + message_start_index - 12 : 0; + if (read_length) { + if (read_mifare_ultralight_bytes_(nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE + 3, read_length, data) != + nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error reading tag data"); + return nfc::STATUS_FAILED; + } + } + // we need to trim off page 3 as well as any bytes ahead of message_start_index + data.erase(data.begin(), data.begin() + message_start_index + nfc::MIFARE_ULTRALIGHT_PAGE_SIZE); + + tag.set_ndef_message(make_unique(data)); + + return nfc::STATUS_OK; +} + +uint8_t PN7160::read_mifare_ultralight_bytes_(uint8_t start_page, uint16_t num_bytes, std::vector &data) { + const uint8_t read_increment = nfc::MIFARE_ULTRALIGHT_READ_SIZE * nfc::MIFARE_ULTRALIGHT_PAGE_SIZE; + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, {nfc::MIFARE_CMD_READ, start_page}); + + for (size_t i = 0; i * read_increment < num_bytes; i++) { + tx.get_message().back() = i * nfc::MIFARE_ULTRALIGHT_READ_SIZE + start_page; + do { // loop because sometimes we struggle here...???... + if (this->transceive_(tx, rx) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error reading tag data"); + return nfc::STATUS_FAILED; + } + } while (rx.get_payload_size() < read_increment); + uint16_t bytes_offset = (i + 1) * read_increment; + auto pages_in_end_itr = bytes_offset <= num_bytes ? rx.get_message().end() - 1 + : rx.get_message().end() - (bytes_offset - num_bytes + 1); + + if ((pages_in_end_itr > rx.get_message().begin()) && (pages_in_end_itr < rx.get_message().end())) { + data.insert(data.end(), rx.get_message().begin() + nfc::NCI_PKT_HEADER_SIZE, pages_in_end_itr); + } + } + + ESP_LOGVV(TAG, "Data read: %s", nfc::format_bytes(data).c_str()); + + return nfc::STATUS_OK; +} + +bool PN7160::is_mifare_ultralight_formatted_(const std::vector &page_3_to_6) { + const uint8_t p4_offset = nfc::MIFARE_ULTRALIGHT_PAGE_SIZE; // page 4 will begin 4 bytes into the vector + + return (page_3_to_6.size() > p4_offset + 3) && + !((page_3_to_6[p4_offset + 0] == 0xFF) && (page_3_to_6[p4_offset + 1] == 0xFF) && + (page_3_to_6[p4_offset + 2] == 0xFF) && (page_3_to_6[p4_offset + 3] == 0xFF)); +} + +uint16_t PN7160::read_mifare_ultralight_capacity_() { + std::vector data; + if (this->read_mifare_ultralight_bytes_(3, nfc::MIFARE_ULTRALIGHT_PAGE_SIZE, data) == nfc::STATUS_OK) { + ESP_LOGV(TAG, "Tag capacity is %u bytes", data[2] * 8U); + return data[2] * 8U; + } + return 0; +} + +uint8_t PN7160::find_mifare_ultralight_ndef_(const std::vector &page_3_to_6, uint8_t &message_length, + uint8_t &message_start_index) { + const uint8_t p4_offset = nfc::MIFARE_ULTRALIGHT_PAGE_SIZE; // page 4 will begin 4 bytes into the vector + + if (!(page_3_to_6.size() > p4_offset + 5)) { + return nfc::STATUS_FAILED; + } + + if (page_3_to_6[p4_offset + 0] == 0x03) { + message_length = page_3_to_6[p4_offset + 1]; + message_start_index = 2; + return nfc::STATUS_OK; + } else if (page_3_to_6[p4_offset + 5] == 0x03) { + message_length = page_3_to_6[p4_offset + 6]; + message_start_index = 7; + return nfc::STATUS_OK; + } + return nfc::STATUS_FAILED; +} + +uint8_t PN7160::write_mifare_ultralight_tag_(std::vector &uid, + const std::shared_ptr &message) { + uint32_t capacity = this->read_mifare_ultralight_capacity_(); + + auto encoded = message->encode(); + + uint32_t message_length = encoded.size(); + uint32_t buffer_length = nfc::get_mifare_ultralight_buffer_size(message_length); + + if (buffer_length > capacity) { + ESP_LOGE(TAG, "Message length exceeds tag capacity %" PRIu32 " > %" PRIu32, buffer_length, capacity); + return nfc::STATUS_FAILED; + } + + encoded.insert(encoded.begin(), 0x03); + if (message_length < 255) { + encoded.insert(encoded.begin() + 1, message_length); + } else { + encoded.insert(encoded.begin() + 1, 0xFF); + encoded.insert(encoded.begin() + 2, (message_length >> 8) & 0xFF); + encoded.insert(encoded.begin() + 2, message_length & 0xFF); + } + encoded.push_back(0xFE); + + encoded.resize(buffer_length, 0); + + uint32_t index = 0; + uint8_t current_page = nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE; + + while (index < buffer_length) { + std::vector data(encoded.begin() + index, encoded.begin() + index + nfc::MIFARE_ULTRALIGHT_PAGE_SIZE); + if (this->write_mifare_ultralight_page_(current_page, data) != nfc::STATUS_OK) { + return nfc::STATUS_FAILED; + } + index += nfc::MIFARE_ULTRALIGHT_PAGE_SIZE; + current_page++; + } + return nfc::STATUS_OK; +} + +uint8_t PN7160::clean_mifare_ultralight_() { + uint32_t capacity = this->read_mifare_ultralight_capacity_(); + uint8_t pages = (capacity / nfc::MIFARE_ULTRALIGHT_PAGE_SIZE) + nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE; + + std::vector blank_data = {0x00, 0x00, 0x00, 0x00}; + + for (int i = nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE; i < pages; i++) { + if (this->write_mifare_ultralight_page_(i, blank_data) != nfc::STATUS_OK) { + return nfc::STATUS_FAILED; + } + } + return nfc::STATUS_OK; +} + +uint8_t PN7160::write_mifare_ultralight_page_(uint8_t page_num, std::vector &write_data) { + std::vector payload = {nfc::MIFARE_CMD_WRITE_ULTRALIGHT, page_num}; + payload.insert(payload.end(), write_data.begin(), write_data.end()); + + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, payload); + + if (this->transceive_(tx, rx, NFCC_TAG_WRITE_TIMEOUT) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error writing page %u", page_num); + return nfc::STATUS_FAILED; + } + return nfc::STATUS_OK; +} + +} // namespace pn7160 +} // namespace esphome diff --git a/esphome/components/pn7160_i2c/__init__.py b/esphome/components/pn7160_i2c/__init__.py new file mode 100644 index 0000000000..87c4719ca8 --- /dev/null +++ b/esphome/components/pn7160_i2c/__init__.py @@ -0,0 +1,25 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, pn7160 +from esphome.const import CONF_ID + +AUTO_LOAD = ["pn7160"] +CODEOWNERS = ["@kbx81", "@jesserockz"] +DEPENDENCIES = ["i2c"] + +pn7160_i2c_ns = cg.esphome_ns.namespace("pn7160_i2c") +PN7160I2C = pn7160_i2c_ns.class_("PN7160I2C", pn7160.PN7160, i2c.I2CDevice) + +CONFIG_SCHEMA = cv.All( + pn7160.PN7160_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(PN7160I2C), + } + ).extend(i2c.i2c_device_schema(0x28)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await pn7160.setup_pn7160(var, config) + await i2c.register_i2c_device(var, config) diff --git a/esphome/components/pn7160_i2c/pn7160_i2c.cpp b/esphome/components/pn7160_i2c/pn7160_i2c.cpp new file mode 100644 index 0000000000..7c6da9dd06 --- /dev/null +++ b/esphome/components/pn7160_i2c/pn7160_i2c.cpp @@ -0,0 +1,49 @@ +#include "pn7160_i2c.h" +#include "esphome/core/log.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace pn7160_i2c { + +static const char *const TAG = "pn7160_i2c"; + +uint8_t PN7160I2C::read_nfcc(nfc::NciMessage &rx, const uint16_t timeout) { + if (this->wait_for_irq_(timeout) != nfc::STATUS_OK) { + ESP_LOGW(TAG, "read_nfcc_() timeout waiting for IRQ"); + return nfc::STATUS_FAILED; + } + + rx.get_message().resize(nfc::NCI_PKT_HEADER_SIZE); + if (!this->read_bytes_raw(rx.get_message().data(), nfc::NCI_PKT_HEADER_SIZE)) { + return nfc::STATUS_FAILED; + } + + uint8_t length = rx.get_payload_size(); + if (length > 0) { + rx.get_message().resize(length + nfc::NCI_PKT_HEADER_SIZE); + if (!this->read_bytes_raw(rx.get_message().data() + nfc::NCI_PKT_HEADER_SIZE, length)) { + return nfc::STATUS_FAILED; + } + } + // semaphore to ensure transaction is complete before returning + if (this->wait_for_irq_(pn7160::NFCC_DEFAULT_TIMEOUT, false) != nfc::STATUS_OK) { + ESP_LOGW(TAG, "read_nfcc_() post-read timeout waiting for IRQ line to clear"); + return nfc::STATUS_FAILED; + } + return nfc::STATUS_OK; +} + +uint8_t PN7160I2C::write_nfcc(nfc::NciMessage &tx) { + if (this->write(tx.encode().data(), tx.encode().size()) == i2c::ERROR_OK) { + return nfc::STATUS_OK; + } + return nfc::STATUS_FAILED; +} + +void PN7160I2C::dump_config() { + PN7160::dump_config(); + LOG_I2C_DEVICE(this); +} + +} // namespace pn7160_i2c +} // namespace esphome diff --git a/esphome/components/pn7160_i2c/pn7160_i2c.h b/esphome/components/pn7160_i2c/pn7160_i2c.h new file mode 100644 index 0000000000..eb253085eb --- /dev/null +++ b/esphome/components/pn7160_i2c/pn7160_i2c.h @@ -0,0 +1,22 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/pn7160/pn7160.h" +#include "esphome/components/i2c/i2c.h" + +#include + +namespace esphome { +namespace pn7160_i2c { + +class PN7160I2C : public pn7160::PN7160, public i2c::I2CDevice { + public: + void dump_config() override; + + protected: + uint8_t read_nfcc(nfc::NciMessage &rx, uint16_t timeout) override; + uint8_t write_nfcc(nfc::NciMessage &tx) override; +}; + +} // namespace pn7160_i2c +} // namespace esphome diff --git a/esphome/components/pn7160_spi/__init__.py b/esphome/components/pn7160_spi/__init__.py new file mode 100644 index 0000000000..ae1235655a --- /dev/null +++ b/esphome/components/pn7160_spi/__init__.py @@ -0,0 +1,26 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import spi, pn7160 +from esphome.const import CONF_ID + +AUTO_LOAD = ["pn7160"] +CODEOWNERS = ["@kbx81", "@jesserockz"] +DEPENDENCIES = ["spi"] +MULTI_CONF = True + +pn7160_spi_ns = cg.esphome_ns.namespace("pn7160_spi") +PN7160Spi = pn7160_spi_ns.class_("PN7160Spi", pn7160.PN7160, spi.SPIDevice) + +CONFIG_SCHEMA = cv.All( + pn7160.PN7160_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(PN7160Spi), + } + ).extend(spi.spi_device_schema(cs_pin_required=True)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await pn7160.setup_pn7160(var, config) + await spi.register_spi_device(var, config) diff --git a/esphome/components/pn7160_spi/pn7160_spi.cpp b/esphome/components/pn7160_spi/pn7160_spi.cpp new file mode 100644 index 0000000000..09f673f700 --- /dev/null +++ b/esphome/components/pn7160_spi/pn7160_spi.cpp @@ -0,0 +1,54 @@ +#include "pn7160_spi.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace pn7160_spi { + +static const char *const TAG = "pn7160_spi"; + +void PN7160Spi::setup() { + this->spi_setup(); + this->cs_->digital_write(false); + PN7160::setup(); +} + +uint8_t PN7160Spi::read_nfcc(nfc::NciMessage &rx, const uint16_t timeout) { + if (this->wait_for_irq_(timeout) != nfc::STATUS_OK) { + ESP_LOGW(TAG, "read_nfcc_() timeout waiting for IRQ"); + return nfc::STATUS_FAILED; + } + + rx.get_message().resize(nfc::NCI_PKT_HEADER_SIZE); + this->enable(); + this->write_byte(TDD_SPI_READ); // send "transfer direction detector" + this->read_array(rx.get_message().data(), nfc::NCI_PKT_HEADER_SIZE); + + uint8_t length = rx.get_payload_size(); + if (length > 0) { + rx.get_message().resize(length + nfc::NCI_PKT_HEADER_SIZE); + this->read_array(rx.get_message().data() + nfc::NCI_PKT_HEADER_SIZE, length); + } + this->disable(); + // semaphore to ensure transaction is complete before returning + if (this->wait_for_irq_(pn7160::NFCC_DEFAULT_TIMEOUT, false) != nfc::STATUS_OK) { + ESP_LOGW(TAG, "read_nfcc_() post-read timeout waiting for IRQ line to clear"); + return nfc::STATUS_FAILED; + } + return nfc::STATUS_OK; +} + +uint8_t PN7160Spi::write_nfcc(nfc::NciMessage &tx) { + this->enable(); + this->write_byte(TDD_SPI_WRITE); // send "transfer direction detector" + this->write_array(tx.encode().data(), tx.encode().size()); + this->disable(); + return nfc::STATUS_OK; +} + +void PN7160Spi::dump_config() { + PN7160::dump_config(); + LOG_PIN(" CS Pin: ", this->cs_); +} + +} // namespace pn7160_spi +} // namespace esphome diff --git a/esphome/components/pn7160_spi/pn7160_spi.h b/esphome/components/pn7160_spi/pn7160_spi.h new file mode 100644 index 0000000000..7d4460a76d --- /dev/null +++ b/esphome/components/pn7160_spi/pn7160_spi.h @@ -0,0 +1,30 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/nfc/nci_core.h" +#include "esphome/components/pn7160/pn7160.h" +#include "esphome/components/spi/spi.h" + +#include + +namespace esphome { +namespace pn7160_spi { + +static const uint8_t TDD_SPI_READ = 0xFF; +static const uint8_t TDD_SPI_WRITE = 0x0A; + +class PN7160Spi : public pn7160::PN7160, + public spi::SPIDevice { + public: + void setup() override; + + void dump_config() override; + + protected: + uint8_t read_nfcc(nfc::NciMessage &rx, uint16_t timeout) override; + uint8_t write_nfcc(nfc::NciMessage &tx) override; +}; + +} // namespace pn7160_spi +} // namespace esphome diff --git a/tests/test1.yaml b/tests/test1.yaml index b77cff7619..59818bbde5 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -3388,6 +3388,43 @@ pn532_spi: pn532_i2c: i2c_id: i2c_bus +pn7160_i2c: + id: nfcc_pn7160_i2c + i2c_id: i2c_bus + dwl_req_pin: + allow_other_uses: true + number: GPIO17 + irq_pin: + allow_other_uses: true + number: GPIO35 + ven_pin: + allow_other_uses: true + number: GPIO16 + wkup_req_pin: + allow_other_uses: true + number: GPIO21 + emulation_message: https://www.home-assistant.io/tag/pulse_ce + tag_ttl: 1000ms + +pn7160_spi: + id: nfcc_pn7160_spi + cs_pin: + number: GPIO15 + dwl_req_pin: + allow_other_uses: true + number: GPIO17 + irq_pin: + allow_other_uses: true + number: GPIO35 + ven_pin: + allow_other_uses: true + number: GPIO16 + wkup_req_pin: + allow_other_uses: true + number: GPIO21 + emulation_message: https://www.home-assistant.io/tag/pulse_ce + tag_ttl: 1000ms + rdm6300: uart_id: uart_0 From 76a6e288b6e7667f8620b85ea1160f9a89307c20 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Wed, 13 Dec 2023 17:27:35 -0600 Subject: [PATCH 81/89] Add support for PN7150 (#5487) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 2 + esphome/components/pn7150/__init__.py | 215 ++++ esphome/components/pn7150/automation.h | 82 ++ esphome/components/pn7150/pn7150.cpp | 1137 +++++++++++++++++ esphome/components/pn7150/pn7150.h | 296 +++++ .../pn7150/pn7150_mifare_classic.cpp | 322 +++++ .../pn7150/pn7150_mifare_ultralight.cpp | 186 +++ esphome/components/pn7150_i2c/__init__.py | 25 + esphome/components/pn7150_i2c/pn7150_i2c.cpp | 49 + esphome/components/pn7150_i2c/pn7150_i2c.h | 22 + tests/test1.yaml | 9 + 11 files changed, 2345 insertions(+) create mode 100644 esphome/components/pn7150/__init__.py create mode 100644 esphome/components/pn7150/automation.h create mode 100644 esphome/components/pn7150/pn7150.cpp create mode 100644 esphome/components/pn7150/pn7150.h create mode 100644 esphome/components/pn7150/pn7150_mifare_classic.cpp create mode 100644 esphome/components/pn7150/pn7150_mifare_ultralight.cpp create mode 100644 esphome/components/pn7150_i2c/__init__.py create mode 100644 esphome/components/pn7150_i2c/pn7150_i2c.cpp create mode 100644 esphome/components/pn7150_i2c/pn7150_i2c.h diff --git a/CODEOWNERS b/CODEOWNERS index 2269d580e4..320a23ffaa 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -240,6 +240,8 @@ esphome/components/pmwcs3/* @SeByDocKy esphome/components/pn532/* @OttoWinter @jesserockz esphome/components/pn532_i2c/* @OttoWinter @jesserockz esphome/components/pn532_spi/* @OttoWinter @jesserockz +esphome/components/pn7150/* @jesserockz @kbx81 +esphome/components/pn7150_i2c/* @jesserockz @kbx81 esphome/components/pn7160/* @jesserockz @kbx81 esphome/components/pn7160_i2c/* @jesserockz @kbx81 esphome/components/pn7160_spi/* @jesserockz @kbx81 diff --git a/esphome/components/pn7150/__init__.py b/esphome/components/pn7150/__init__.py new file mode 100644 index 0000000000..3b80b574e9 --- /dev/null +++ b/esphome/components/pn7150/__init__.py @@ -0,0 +1,215 @@ +from esphome import automation, pins +from esphome.automation import maybe_simple_id +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import nfc +from esphome.const import ( + CONF_ID, + CONF_IRQ_PIN, + CONF_ON_TAG_REMOVED, + CONF_ON_TAG, + CONF_TRIGGER_ID, +) + +AUTO_LOAD = ["binary_sensor", "nfc"] +CODEOWNERS = ["@kbx81", "@jesserockz"] + +CONF_EMULATION_MESSAGE = "emulation_message" +CONF_EMULATION_OFF = "emulation_off" +CONF_EMULATION_ON = "emulation_on" +CONF_INCLUDE_ANDROID_APP_RECORD = "include_android_app_record" +CONF_MESSAGE = "message" +CONF_ON_FINISHED_WRITE = "on_finished_write" +CONF_ON_EMULATED_TAG_SCAN = "on_emulated_tag_scan" +CONF_PN7150_ID = "pn7150_id" +CONF_POLLING_OFF = "polling_off" +CONF_POLLING_ON = "polling_on" +CONF_SET_CLEAN_MODE = "set_clean_mode" +CONF_SET_EMULATION_MESSAGE = "set_emulation_message" +CONF_SET_FORMAT_MODE = "set_format_mode" +CONF_SET_READ_MODE = "set_read_mode" +CONF_SET_WRITE_MESSAGE = "set_write_message" +CONF_SET_WRITE_MODE = "set_write_mode" +CONF_TAG_TTL = "tag_ttl" +CONF_VEN_PIN = "ven_pin" + +pn7150_ns = cg.esphome_ns.namespace("pn7150") +PN7150 = pn7150_ns.class_("PN7150", cg.Component) + +EmulationOffAction = pn7150_ns.class_("EmulationOffAction", automation.Action) +EmulationOnAction = pn7150_ns.class_("EmulationOnAction", automation.Action) +PollingOffAction = pn7150_ns.class_("PollingOffAction", automation.Action) +PollingOnAction = pn7150_ns.class_("PollingOnAction", automation.Action) +SetCleanModeAction = pn7150_ns.class_("SetCleanModeAction", automation.Action) +SetEmulationMessageAction = pn7150_ns.class_( + "SetEmulationMessageAction", automation.Action +) +SetFormatModeAction = pn7150_ns.class_("SetFormatModeAction", automation.Action) +SetReadModeAction = pn7150_ns.class_("SetReadModeAction", automation.Action) +SetWriteMessageAction = pn7150_ns.class_("SetWriteMessageAction", automation.Action) +SetWriteModeAction = pn7150_ns.class_("SetWriteModeAction", automation.Action) + + +PN7150OnEmulatedTagScanTrigger = pn7150_ns.class_( + "PN7150OnEmulatedTagScanTrigger", automation.Trigger.template() +) + +PN7150OnFinishedWriteTrigger = pn7150_ns.class_( + "PN7150OnFinishedWriteTrigger", automation.Trigger.template() +) + +PN7150IsWritingCondition = pn7150_ns.class_( + "PN7150IsWritingCondition", automation.Condition +) + + +IsWritingCondition = nfc.nfc_ns.class_("IsWritingCondition", automation.Condition) + + +SIMPLE_ACTION_SCHEMA = maybe_simple_id( + { + cv.Required(CONF_ID): cv.use_id(PN7150), + } +) + +SET_MESSAGE_ACTION_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.use_id(PN7150), + cv.Required(CONF_MESSAGE): cv.templatable(cv.string), + cv.Optional(CONF_INCLUDE_ANDROID_APP_RECORD, default=True): cv.boolean, + } +) + +PN7150_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(PN7150), + cv.Optional(CONF_ON_EMULATED_TAG_SCAN): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + PN7150OnEmulatedTagScanTrigger + ), + } + ), + cv.Optional(CONF_ON_FINISHED_WRITE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + PN7150OnFinishedWriteTrigger + ), + } + ), + cv.Optional(CONF_ON_TAG): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(nfc.NfcOnTagTrigger), + } + ), + cv.Optional(CONF_ON_TAG_REMOVED): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(nfc.NfcOnTagTrigger), + } + ), + cv.Required(CONF_IRQ_PIN): pins.gpio_input_pin_schema, + cv.Required(CONF_VEN_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_EMULATION_MESSAGE): cv.string, + cv.Optional(CONF_TAG_TTL): cv.positive_time_period_milliseconds, + } +).extend(cv.COMPONENT_SCHEMA) + + +@automation.register_action( + "tag.set_emulation_message", + SetEmulationMessageAction, + SET_MESSAGE_ACTION_SCHEMA, +) +@automation.register_action( + "tag.set_write_message", + SetWriteMessageAction, + SET_MESSAGE_ACTION_SCHEMA, +) +async def pn7150_set_message_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + template_ = await cg.templatable(config[CONF_MESSAGE], args, cg.std_string) + cg.add(var.set_message(template_)) + template_ = await cg.templatable( + config[CONF_INCLUDE_ANDROID_APP_RECORD], args, cg.bool_ + ) + cg.add(var.set_include_android_app_record(template_)) + return var + + +@automation.register_action( + "tag.emulation_off", EmulationOffAction, SIMPLE_ACTION_SCHEMA +) +@automation.register_action("tag.emulation_on", EmulationOnAction, SIMPLE_ACTION_SCHEMA) +@automation.register_action("tag.polling_off", PollingOffAction, SIMPLE_ACTION_SCHEMA) +@automation.register_action("tag.polling_on", PollingOnAction, SIMPLE_ACTION_SCHEMA) +@automation.register_action( + "tag.set_clean_mode", SetCleanModeAction, SIMPLE_ACTION_SCHEMA +) +@automation.register_action( + "tag.set_format_mode", SetFormatModeAction, SIMPLE_ACTION_SCHEMA +) +@automation.register_action( + "tag.set_read_mode", SetReadModeAction, SIMPLE_ACTION_SCHEMA +) +@automation.register_action( + "tag.set_write_mode", SetWriteModeAction, SIMPLE_ACTION_SCHEMA +) +async def pn7150_simple_action_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var + + +async def setup_pn7150(var, config): + await cg.register_component(var, config) + + pin = await cg.gpio_pin_expression(config[CONF_IRQ_PIN]) + cg.add(var.set_irq_pin(pin)) + + pin = await cg.gpio_pin_expression(config[CONF_VEN_PIN]) + cg.add(var.set_ven_pin(pin)) + + if emulation_message_config := config.get(CONF_EMULATION_MESSAGE): + cg.add(var.set_tag_emulation_message(emulation_message_config)) + cg.add(var.set_tag_emulation_on()) + + if CONF_TAG_TTL in config: + cg.add(var.set_tag_ttl(config[CONF_TAG_TTL])) + + for conf in config.get(CONF_ON_TAG, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) + cg.add(var.register_ontag_trigger(trigger)) + await automation.build_automation( + trigger, [(cg.std_string, "x"), (nfc.NfcTag, "tag")], conf + ) + + for conf in config.get(CONF_ON_TAG_REMOVED, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) + cg.add(var.register_ontagremoved_trigger(trigger)) + await automation.build_automation( + trigger, [(cg.std_string, "x"), (nfc.NfcTag, "tag")], conf + ) + + for conf in config.get(CONF_ON_EMULATED_TAG_SCAN, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + + for conf in config.get(CONF_ON_FINISHED_WRITE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + + +@automation.register_condition( + "pn7150.is_writing", + PN7150IsWritingCondition, + cv.Schema( + { + cv.GenerateID(): cv.use_id(PN7150), + } + ), +) +async def pn7150_is_writing_to_code(config, condition_id, template_arg, args): + var = cg.new_Pvariable(condition_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var diff --git a/esphome/components/pn7150/automation.h b/esphome/components/pn7150/automation.h new file mode 100644 index 0000000000..aebb1b7573 --- /dev/null +++ b/esphome/components/pn7150/automation.h @@ -0,0 +1,82 @@ +#pragma once + +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/components/pn7150/pn7150.h" + +namespace esphome { +namespace pn7150 { + +class PN7150OnEmulatedTagScanTrigger : public Trigger<> { + public: + explicit PN7150OnEmulatedTagScanTrigger(PN7150 *parent) { + parent->add_on_emulated_tag_scan_callback([this]() { this->trigger(); }); + } +}; + +class PN7150OnFinishedWriteTrigger : public Trigger<> { + public: + explicit PN7150OnFinishedWriteTrigger(PN7150 *parent) { + parent->add_on_finished_write_callback([this]() { this->trigger(); }); + } +}; + +template class PN7150IsWritingCondition : public Condition, public Parented { + public: + bool check(Ts... x) override { return this->parent_->is_writing(); } +}; + +template class EmulationOffAction : public Action, public Parented { + void play(Ts... x) override { this->parent_->set_tag_emulation_off(); } +}; + +template class EmulationOnAction : public Action, public Parented { + void play(Ts... x) override { this->parent_->set_tag_emulation_on(); } +}; + +template class PollingOffAction : public Action, public Parented { + void play(Ts... x) override { this->parent_->set_polling_off(); } +}; + +template class PollingOnAction : public Action, public Parented { + void play(Ts... x) override { this->parent_->set_polling_on(); } +}; + +template class SetCleanModeAction : public Action, public Parented { + void play(Ts... x) override { this->parent_->clean_mode(); } +}; + +template class SetFormatModeAction : public Action, public Parented { + void play(Ts... x) override { this->parent_->format_mode(); } +}; + +template class SetReadModeAction : public Action, public Parented { + void play(Ts... x) override { this->parent_->read_mode(); } +}; + +template class SetEmulationMessageAction : public Action, public Parented { + TEMPLATABLE_VALUE(std::string, message) + TEMPLATABLE_VALUE(bool, include_android_app_record) + + void play(Ts... x) override { + this->parent_->set_tag_emulation_message(this->message_.optional_value(x...), + this->include_android_app_record_.optional_value(x...)); + } +}; + +template class SetWriteMessageAction : public Action, public Parented { + TEMPLATABLE_VALUE(std::string, message) + TEMPLATABLE_VALUE(bool, include_android_app_record) + + void play(Ts... x) override { + this->parent_->set_tag_write_message(this->message_.optional_value(x...), + this->include_android_app_record_.optional_value(x...)); + } +}; + +template class SetWriteModeAction : public Action, public Parented { + void play(Ts... x) override { this->parent_->write_mode(); } +}; + +} // namespace pn7150 +} // namespace esphome diff --git a/esphome/components/pn7150/pn7150.cpp b/esphome/components/pn7150/pn7150.cpp new file mode 100644 index 0000000000..6703ab6a12 --- /dev/null +++ b/esphome/components/pn7150/pn7150.cpp @@ -0,0 +1,1137 @@ +#include "automation.h" +#include "pn7150.h" + +#include + +#include "esphome/core/hal.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace pn7150 { + +static const char *const TAG = "pn7150"; + +void PN7150::setup() { + this->irq_pin_->setup(); + this->ven_pin_->setup(); + + this->nci_fsm_transition_(); // kick off reset & init processes +} + +void PN7150::dump_config() { + ESP_LOGCONFIG(TAG, "PN7150:"); + LOG_PIN(" IRQ pin: ", this->irq_pin_); + LOG_PIN(" VEN pin: ", this->ven_pin_); +} + +void PN7150::loop() { + this->nci_fsm_transition_(); + this->purge_old_tags_(); +} + +void PN7150::set_tag_emulation_message(std::shared_ptr message) { + this->card_emulation_message_ = std::move(message); + ESP_LOGD(TAG, "Tag emulation message set"); +} + +void PN7150::set_tag_emulation_message(const optional &message, + const optional include_android_app_record) { + if (!message.has_value()) { + return; + } + + auto ndef_message = make_unique(); + + ndef_message->add_uri_record(message.value()); + + if (!include_android_app_record.has_value() || include_android_app_record.value()) { + auto ext_record = make_unique(); + ext_record->set_tnf(nfc::TNF_EXTERNAL_TYPE); + ext_record->set_type(nfc::HA_TAG_ID_EXT_RECORD_TYPE); + ext_record->set_payload(nfc::HA_TAG_ID_EXT_RECORD_PAYLOAD); + ndef_message->add_record(std::move(ext_record)); + } + + this->card_emulation_message_ = std::move(ndef_message); + ESP_LOGD(TAG, "Tag emulation message set"); +} + +void PN7150::set_tag_emulation_message(const char *message, const bool include_android_app_record) { + this->set_tag_emulation_message(std::string(message), include_android_app_record); +} + +void PN7150::set_tag_emulation_off() { + if (this->listening_enabled_) { + this->listening_enabled_ = false; + this->config_refresh_pending_ = true; + } + ESP_LOGD(TAG, "Tag emulation disabled"); +} + +void PN7150::set_tag_emulation_on() { + if (this->card_emulation_message_ == nullptr) { + ESP_LOGE(TAG, "No NDEF message is set; tag emulation cannot be enabled"); + return; + } + if (!this->listening_enabled_) { + this->listening_enabled_ = true; + this->config_refresh_pending_ = true; + } + ESP_LOGD(TAG, "Tag emulation enabled"); +} + +void PN7150::set_polling_off() { + if (this->polling_enabled_) { + this->polling_enabled_ = false; + this->config_refresh_pending_ = true; + } + ESP_LOGD(TAG, "Tag polling disabled"); +} + +void PN7150::set_polling_on() { + if (!this->polling_enabled_) { + this->polling_enabled_ = true; + this->config_refresh_pending_ = true; + } + ESP_LOGD(TAG, "Tag polling enabled"); +} + +void PN7150::read_mode() { + this->next_task_ = EP_READ; + ESP_LOGD(TAG, "Waiting to read next tag"); +} + +void PN7150::clean_mode() { + this->next_task_ = EP_CLEAN; + ESP_LOGD(TAG, "Waiting to clean next tag"); +} + +void PN7150::format_mode() { + this->next_task_ = EP_FORMAT; + ESP_LOGD(TAG, "Waiting to format next tag"); +} + +void PN7150::write_mode() { + if (this->next_task_message_to_write_ == nullptr) { + ESP_LOGW(TAG, "Message to write must be set before setting write mode"); + return; + } + + this->next_task_ = EP_WRITE; + ESP_LOGD(TAG, "Waiting to write next tag"); +} + +void PN7150::set_tag_write_message(std::shared_ptr message) { + this->next_task_message_to_write_ = std::move(message); + ESP_LOGD(TAG, "Message to write has been set"); +} + +void PN7150::set_tag_write_message(optional message, optional include_android_app_record) { + if (!message.has_value()) { + return; + } + + auto ndef_message = make_unique(); + + ndef_message->add_uri_record(message.value()); + + if (!include_android_app_record.has_value() || include_android_app_record.value()) { + auto ext_record = make_unique(); + ext_record->set_tnf(nfc::TNF_EXTERNAL_TYPE); + ext_record->set_type(nfc::HA_TAG_ID_EXT_RECORD_TYPE); + ext_record->set_payload(nfc::HA_TAG_ID_EXT_RECORD_PAYLOAD); + ndef_message->add_record(std::move(ext_record)); + } + + this->next_task_message_to_write_ = std::move(ndef_message); + ESP_LOGD(TAG, "Message to write has been set"); +} + +uint8_t PN7150::set_test_mode(const TestMode test_mode, const std::vector &data, + std::vector &result) { + auto test_oid = TEST_PRBS_OID; + + switch (test_mode) { + case TestMode::TEST_PRBS: + // test_oid = TEST_PRBS_OID; + break; + + case TestMode::TEST_ANTENNA: + test_oid = TEST_ANTENNA_OID; + break; + + case TestMode::TEST_GET_REGISTER: + test_oid = TEST_GET_REGISTER_OID; + break; + + case TestMode::TEST_NONE: + default: + ESP_LOGD(TAG, "Exiting test mode"); + this->nci_fsm_set_state_(NCIState::NFCC_RESET); + return nfc::STATUS_OK; + } + + if (this->reset_core_(true, true) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Failed to reset NCI core"); + this->nci_fsm_set_error_state_(NCIState::NFCC_RESET); + result.clear(); + return nfc::STATUS_FAILED; + } else { + this->nci_fsm_set_state_(NCIState::NFCC_INIT); + } + if (this->init_core_() != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Failed to initialise NCI core"); + this->nci_fsm_set_error_state_(NCIState::NFCC_INIT); + result.clear(); + return nfc::STATUS_FAILED; + } else { + this->nci_fsm_set_state_(NCIState::TEST); + } + + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::NCI_PROPRIETARY_GID, test_oid, data); + + ESP_LOGW(TAG, "Starting test mode, OID 0x%02X", test_oid); + auto status = this->transceive_(tx, rx, NFCC_INIT_TIMEOUT); + + if (status != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Failed to start test mode, OID 0x%02X", test_oid); + this->nci_fsm_set_state_(NCIState::NFCC_RESET); + result.clear(); + } else { + result = rx.get_message(); + result.erase(result.begin(), result.begin() + 4); // remove NCI header + if (!result.empty()) { + ESP_LOGW(TAG, "Test results: %s", nfc::format_bytes(result).c_str()); + } + } + return status; +} + +uint8_t PN7150::reset_core_(const bool reset_config, const bool power) { + if (power) { + this->ven_pin_->digital_write(true); + delay(NFCC_DEFAULT_TIMEOUT); + this->ven_pin_->digital_write(false); + delay(NFCC_DEFAULT_TIMEOUT); + this->ven_pin_->digital_write(true); + delay(NFCC_INIT_TIMEOUT); + } + + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::NCI_CORE_GID, nfc::NCI_CORE_RESET_OID, + {(uint8_t) reset_config}); + + if (this->transceive_(tx, rx, NFCC_INIT_TIMEOUT) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error sending reset command"); + return nfc::STATUS_FAILED; + } + + if (!rx.simple_status_response_is(nfc::STATUS_OK)) { + ESP_LOGE(TAG, "Invalid reset response: %s", nfc::format_bytes(rx.get_message()).c_str()); + return rx.get_simple_status_response(); + } + // verify reset response + if ((!rx.message_type_is(nfc::NCI_PKT_MT_CTRL_RESPONSE)) || (!rx.message_length_is(3)) || + (rx.get_message()[nfc::NCI_PKT_PAYLOAD_OFFSET + 1] != 0x11) || + (rx.get_message()[nfc::NCI_PKT_PAYLOAD_OFFSET + 2] != (uint8_t) reset_config)) { + ESP_LOGE(TAG, "Reset response was malformed: %s", nfc::format_bytes(rx.get_message()).c_str()); + return nfc::STATUS_FAILED; + } + + ESP_LOGD(TAG, "Configuration %s", rx.get_message()[nfc::NCI_PKT_PAYLOAD_OFFSET + 2] ? "reset" : "retained"); + ESP_LOGD(TAG, "NCI version: %s", rx.get_message()[nfc::NCI_PKT_PAYLOAD_OFFSET + 1] == 0x20 ? "2.0" : "1.0"); + + return nfc::STATUS_OK; +} + +uint8_t PN7150::init_core_() { + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::NCI_CORE_GID, nfc::NCI_CORE_INIT_OID); + + if (this->transceive_(tx, rx) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error sending initialise command"); + return nfc::STATUS_FAILED; + } + + if (!rx.simple_status_response_is(nfc::STATUS_OK)) { + ESP_LOGE(TAG, "Invalid initialise response: %s", nfc::format_bytes(rx.get_message()).c_str()); + return nfc::STATUS_FAILED; + } + + uint8_t manf_id = rx.get_message()[15 + rx.get_message()[8]]; + uint8_t hw_version = rx.get_message()[16 + rx.get_message()[8]]; + uint8_t rom_code_version = rx.get_message()[17 + rx.get_message()[8]]; + uint8_t flash_major_version = rx.get_message()[18 + rx.get_message()[8]]; + uint8_t flash_minor_version = rx.get_message()[19 + rx.get_message()[8]]; + + ESP_LOGD(TAG, "Manufacturer ID: 0x%02X", manf_id); + ESP_LOGD(TAG, "Hardware version: 0x%02X", hw_version); + ESP_LOGD(TAG, "ROM code version: 0x%02X", rom_code_version); + ESP_LOGD(TAG, "FLASH major version: 0x%02X", flash_major_version); + ESP_LOGD(TAG, "FLASH minor version: 0x%02X", flash_minor_version); + + return rx.get_simple_status_response(); +} + +uint8_t PN7150::send_init_config_() { + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::NCI_PROPRIETARY_GID, nfc::NCI_CORE_SET_CONFIG_OID); + + if (this->transceive_(tx, rx) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error enabling proprietary extensions"); + return nfc::STATUS_FAILED; + } + + tx.set_message(nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::NCI_CORE_GID, nfc::NCI_CORE_SET_CONFIG_OID, + std::vector(std::begin(PMU_CFG), std::end(PMU_CFG))); + + if (this->transceive_(tx, rx) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error sending PMU config"); + return nfc::STATUS_FAILED; + } + + return this->send_core_config_(); +} + +uint8_t PN7150::send_core_config_() { + const auto *core_config_begin = std::begin(CORE_CONFIG_SOLO); + const auto *core_config_end = std::end(CORE_CONFIG_SOLO); + this->core_config_is_solo_ = true; + + if (this->listening_enabled_ && this->polling_enabled_) { + core_config_begin = std::begin(CORE_CONFIG_RW_CE); + core_config_end = std::end(CORE_CONFIG_RW_CE); + this->core_config_is_solo_ = false; + } + + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::NCI_CORE_GID, nfc::NCI_CORE_SET_CONFIG_OID, + std::vector(core_config_begin, core_config_end)); + + if (this->transceive_(tx, rx) != nfc::STATUS_OK) { + ESP_LOGW(TAG, "Error sending core config"); + return nfc::STATUS_FAILED; + } + + return nfc::STATUS_OK; +} + +uint8_t PN7150::refresh_core_config_() { + bool core_config_should_be_solo = !(this->listening_enabled_ && this->polling_enabled_); + + if (this->nci_state_ == NCIState::RFST_DISCOVERY) { + if (this->stop_discovery_() != nfc::STATUS_OK) { + this->nci_fsm_set_state_(NCIState::NFCC_RESET); + return nfc::STATUS_FAILED; + } + this->nci_fsm_set_state_(NCIState::RFST_IDLE); + } + + if (this->core_config_is_solo_ != core_config_should_be_solo) { + if (this->send_core_config_() != nfc::STATUS_OK) { + ESP_LOGV(TAG, "Failed to refresh core config"); + return nfc::STATUS_FAILED; + } + } + this->config_refresh_pending_ = false; + return nfc::STATUS_OK; +} + +uint8_t PN7150::set_discover_map_() { + std::vector discover_map = {sizeof(RF_DISCOVER_MAP_CONFIG) / 3}; + discover_map.insert(discover_map.end(), std::begin(RF_DISCOVER_MAP_CONFIG), std::end(RF_DISCOVER_MAP_CONFIG)); + + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::RF_GID, nfc::RF_DISCOVER_MAP_OID, discover_map); + + if (this->transceive_(tx, rx, NFCC_INIT_TIMEOUT) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error sending discover map poll config"); + return nfc::STATUS_FAILED; + } + return nfc::STATUS_OK; +} + +uint8_t PN7150::set_listen_mode_routing_() { + nfc::NciMessage rx; + nfc::NciMessage tx( + nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::RF_GID, nfc::RF_SET_LISTEN_MODE_ROUTING_OID, + std::vector(std::begin(RF_LISTEN_MODE_ROUTING_CONFIG), std::end(RF_LISTEN_MODE_ROUTING_CONFIG))); + + if (this->transceive_(tx, rx, NFCC_INIT_TIMEOUT) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error setting listen mode routing config"); + return nfc::STATUS_FAILED; + } + return nfc::STATUS_OK; +} + +uint8_t PN7150::start_discovery_() { + const uint8_t *rf_discovery_config = RF_DISCOVERY_CONFIG; + uint8_t length = sizeof(RF_DISCOVERY_CONFIG); + + if (!this->listening_enabled_) { + length = sizeof(RF_DISCOVERY_POLL_CONFIG); + rf_discovery_config = RF_DISCOVERY_POLL_CONFIG; + } else if (!this->polling_enabled_) { + length = sizeof(RF_DISCOVERY_LISTEN_CONFIG); + rf_discovery_config = RF_DISCOVERY_LISTEN_CONFIG; + } + + std::vector discover_config = std::vector((length * 2) + 1); + + discover_config[0] = length; + for (uint8_t i = 0; i < length; i++) { + discover_config[(i * 2) + 1] = rf_discovery_config[i]; + discover_config[(i * 2) + 2] = 0x01; // RF Technology and Mode will be executed in every discovery period + } + + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::RF_GID, nfc::RF_DISCOVER_OID, discover_config); + + if (this->transceive_(tx, rx) != nfc::STATUS_OK) { + switch (rx.get_simple_status_response()) { + // in any of these cases, we are either already in or will remain in discovery, which satisfies the function call + case nfc::STATUS_OK: + case nfc::DISCOVERY_ALREADY_STARTED: + case nfc::DISCOVERY_TARGET_ACTIVATION_FAILED: + case nfc::DISCOVERY_TEAR_DOWN: + return nfc::STATUS_OK; + + default: + ESP_LOGE(TAG, "Error starting discovery"); + return nfc::STATUS_FAILED; + } + } + + return nfc::STATUS_OK; +} + +uint8_t PN7150::stop_discovery_() { return this->deactivate_(nfc::DEACTIVATION_TYPE_IDLE, NFCC_TAG_WRITE_TIMEOUT); } + +uint8_t PN7150::deactivate_(const uint8_t type, const uint16_t timeout) { + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::RF_GID, nfc::RF_DEACTIVATE_OID, {type}); + + auto status = this->transceive_(tx, rx, timeout); + // if (status != nfc::STATUS_OK) { + // ESP_LOGE(TAG, "Error sending deactivate type %u", type); + // return nfc::STATUS_FAILED; + // } + return status; +} + +void PN7150::select_endpoint_() { + if (this->discovered_endpoint_.empty()) { + ESP_LOGW(TAG, "No cached tags to select"); + this->stop_discovery_(); + this->nci_fsm_set_state_(NCIState::RFST_IDLE); + return; + } + std::vector endpoint_data = {this->discovered_endpoint_[0].id, this->discovered_endpoint_[0].protocol, + 0x01}; // that last byte is the interface ID + for (size_t i = 0; i < this->discovered_endpoint_.size(); i++) { + if (!this->discovered_endpoint_[i].trig_called) { + endpoint_data = {this->discovered_endpoint_[i].id, this->discovered_endpoint_[i].protocol, + 0x01}; // that last byte is the interface ID + this->selecting_endpoint_ = i; + break; + } + } + + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_CTRL_COMMAND, nfc::RF_GID, nfc::RF_DISCOVER_SELECT_OID, endpoint_data); + + if (this->transceive_(tx, rx) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error selecting endpoint"); + } else { + this->nci_fsm_set_state_(NCIState::EP_SELECTING); + } +} + +uint8_t PN7150::read_endpoint_data_(nfc::NfcTag &tag) { + uint8_t type = nfc::guess_tag_type(tag.get_uid().size()); + + switch (type) { + case nfc::TAG_TYPE_MIFARE_CLASSIC: + ESP_LOGV(TAG, "Reading Mifare classic"); + return this->read_mifare_classic_tag_(tag); + + case nfc::TAG_TYPE_2: + ESP_LOGV(TAG, "Reading Mifare ultralight"); + return this->read_mifare_ultralight_tag_(tag); + + case nfc::TAG_TYPE_UNKNOWN: + default: + ESP_LOGV(TAG, "Cannot determine tag type"); + break; + } + return nfc::STATUS_FAILED; +} + +uint8_t PN7150::clean_endpoint_(std::vector &uid) { + uint8_t type = nfc::guess_tag_type(uid.size()); + switch (type) { + case nfc::TAG_TYPE_MIFARE_CLASSIC: + return this->format_mifare_classic_mifare_(); + + case nfc::TAG_TYPE_2: + return this->clean_mifare_ultralight_(); + + default: + ESP_LOGE(TAG, "Unsupported tag for cleaning"); + break; + } + return nfc::STATUS_FAILED; +} + +uint8_t PN7150::format_endpoint_(std::vector &uid) { + uint8_t type = nfc::guess_tag_type(uid.size()); + switch (type) { + case nfc::TAG_TYPE_MIFARE_CLASSIC: + return this->format_mifare_classic_ndef_(); + + case nfc::TAG_TYPE_2: + return this->clean_mifare_ultralight_(); + + default: + ESP_LOGE(TAG, "Unsupported tag for formatting"); + break; + } + return nfc::STATUS_FAILED; +} + +uint8_t PN7150::write_endpoint_(std::vector &uid, std::shared_ptr &message) { + uint8_t type = nfc::guess_tag_type(uid.size()); + switch (type) { + case nfc::TAG_TYPE_MIFARE_CLASSIC: + return this->write_mifare_classic_tag_(message); + + case nfc::TAG_TYPE_2: + return this->write_mifare_ultralight_tag_(uid, message); + + default: + ESP_LOGE(TAG, "Unsupported tag for writing"); + break; + } + return nfc::STATUS_FAILED; +} + +std::unique_ptr PN7150::build_tag_(const uint8_t mode_tech, const std::vector &data) { + switch (mode_tech) { + case (nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCA): { + uint8_t uid_length = data[2]; + if (!uid_length) { + ESP_LOGE(TAG, "UID length cannot be zero"); + return nullptr; + } + std::vector uid(data.begin() + 3, data.begin() + 3 + uid_length); + const auto *tag_type_str = + nfc::guess_tag_type(uid_length) == nfc::TAG_TYPE_MIFARE_CLASSIC ? nfc::MIFARE_CLASSIC : nfc::NFC_FORUM_TYPE_2; + return make_unique(uid, tag_type_str); + } + } + return nullptr; +} + +optional PN7150::find_tag_uid_(const std::vector &uid) { + if (!this->discovered_endpoint_.empty()) { + for (size_t i = 0; i < this->discovered_endpoint_.size(); i++) { + auto existing_tag_uid = this->discovered_endpoint_[i].tag->get_uid(); + bool uid_match = (uid.size() == existing_tag_uid.size()); + + if (uid_match) { + for (size_t i = 0; i < uid.size(); i++) { + uid_match &= (uid[i] == existing_tag_uid[i]); + } + if (uid_match) { + return i; + } + } + } + } + return nullopt; +} + +void PN7150::purge_old_tags_() { + for (size_t i = 0; i < this->discovered_endpoint_.size(); i++) { + if (millis() - this->discovered_endpoint_[i].last_seen > this->tag_ttl_) { + this->erase_tag_(i); + } + } +} + +void PN7150::erase_tag_(const uint8_t tag_index) { + if (tag_index < this->discovered_endpoint_.size()) { + for (auto *trigger : this->triggers_ontagremoved_) { + trigger->process(this->discovered_endpoint_[tag_index].tag); + } + ESP_LOGI(TAG, "Tag %s removed", nfc::format_uid(this->discovered_endpoint_[tag_index].tag->get_uid()).c_str()); + this->discovered_endpoint_.erase(this->discovered_endpoint_.begin() + tag_index); + } +} + +void PN7150::nci_fsm_transition_() { + switch (this->nci_state_) { + case NCIState::NFCC_RESET: + if (this->reset_core_(true, true) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Failed to reset NCI core"); + this->nci_fsm_set_error_state_(NCIState::NFCC_RESET); + return; + } else { + this->nci_fsm_set_state_(NCIState::NFCC_INIT); + } + // fall through + + case NCIState::NFCC_INIT: + if (this->init_core_() != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Failed to initialise NCI core"); + this->nci_fsm_set_error_state_(NCIState::NFCC_INIT); + return; + } else { + this->nci_fsm_set_state_(NCIState::NFCC_CONFIG); + } + // fall through + + case NCIState::NFCC_CONFIG: + if (this->send_init_config_() != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Failed to send initial config"); + this->nci_fsm_set_error_state_(NCIState::NFCC_CONFIG); + return; + } else { + this->config_refresh_pending_ = false; + this->nci_fsm_set_state_(NCIState::NFCC_SET_DISCOVER_MAP); + } + // fall through + + case NCIState::NFCC_SET_DISCOVER_MAP: + if (this->set_discover_map_() != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Failed to set discover map"); + this->nci_fsm_set_error_state_(NCIState::NFCC_SET_LISTEN_MODE_ROUTING); + return; + } else { + this->nci_fsm_set_state_(NCIState::NFCC_SET_LISTEN_MODE_ROUTING); + } + // fall through + + case NCIState::NFCC_SET_LISTEN_MODE_ROUTING: + if (this->set_listen_mode_routing_() != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Failed to set listen mode routing"); + this->nci_fsm_set_error_state_(NCIState::RFST_IDLE); + return; + } else { + this->nci_fsm_set_state_(NCIState::RFST_IDLE); + } + // fall through + + case NCIState::RFST_IDLE: + if (this->nci_state_error_ == NCIState::RFST_DISCOVERY) { + this->stop_discovery_(); + } + + if (this->config_refresh_pending_) { + this->refresh_core_config_(); + } + + if (!this->listening_enabled_ && !this->polling_enabled_) { + return; + } + + if (this->start_discovery_() != nfc::STATUS_OK) { + ESP_LOGV(TAG, "Failed to start discovery"); + this->nci_fsm_set_error_state_(NCIState::RFST_DISCOVERY); + } else { + this->nci_fsm_set_state_(NCIState::RFST_DISCOVERY); + } + return; + + case NCIState::RFST_W4_HOST_SELECT: + select_endpoint_(); + // fall through + + // All cases below are waiting for NOTIFICATION messages + case NCIState::RFST_DISCOVERY: + if (this->config_refresh_pending_) { + this->refresh_core_config_(); + } + // fall through + + case NCIState::RFST_LISTEN_ACTIVE: + case NCIState::RFST_LISTEN_SLEEP: + case NCIState::RFST_POLL_ACTIVE: + case NCIState::EP_SELECTING: + case NCIState::EP_DEACTIVATING: + if (this->irq_pin_->digital_read()) { + this->process_message_(); + } + break; + + case NCIState::TEST: + case NCIState::FAILED: + case NCIState::NONE: + default: + return; + } +} + +void PN7150::nci_fsm_set_state_(NCIState new_state) { + ESP_LOGVV(TAG, "nci_fsm_set_state_(%u)", (uint8_t) new_state); + this->nci_state_ = new_state; + this->nci_state_error_ = NCIState::NONE; + this->error_count_ = 0; + this->last_nci_state_change_ = millis(); +} + +bool PN7150::nci_fsm_set_error_state_(NCIState new_state) { + ESP_LOGVV(TAG, "nci_fsm_set_error_state_(%u); error_count_ = %u", (uint8_t) new_state, this->error_count_); + this->nci_state_error_ = new_state; + if (this->error_count_++ > NFCC_MAX_ERROR_COUNT) { + if ((this->nci_state_error_ == NCIState::NFCC_RESET) || (this->nci_state_error_ == NCIState::NFCC_INIT) || + (this->nci_state_error_ == NCIState::NFCC_CONFIG)) { + ESP_LOGE(TAG, "Too many initialization failures -- check device connections"); + this->mark_failed(); + this->nci_fsm_set_state_(NCIState::FAILED); + } else { + ESP_LOGW(TAG, "Too many errors transitioning to state %u; resetting NFCC", (uint8_t) this->nci_state_error_); + this->nci_fsm_set_state_(NCIState::NFCC_RESET); + } + } + return this->error_count_ > NFCC_MAX_ERROR_COUNT; +} + +void PN7150::process_message_() { + nfc::NciMessage rx; + if (this->read_nfcc(rx, NFCC_DEFAULT_TIMEOUT) != nfc::STATUS_OK) { + return; // No data + } + + switch (rx.get_message_type()) { + case nfc::NCI_PKT_MT_CTRL_NOTIFICATION: + if (rx.get_gid() == nfc::RF_GID) { + switch (rx.get_oid()) { + case nfc::RF_INTF_ACTIVATED_OID: + ESP_LOGVV(TAG, "RF_INTF_ACTIVATED_OID"); + this->process_rf_intf_activated_oid_(rx); + return; + + case nfc::RF_DISCOVER_OID: + ESP_LOGVV(TAG, "RF_DISCOVER_OID"); + this->process_rf_discover_oid_(rx); + return; + + case nfc::RF_DEACTIVATE_OID: + ESP_LOGVV(TAG, "RF_DEACTIVATE_OID: type: 0x%02X, reason: 0x%02X", rx.get_message()[3], rx.get_message()[4]); + this->process_rf_deactivate_oid_(rx); + return; + + default: + ESP_LOGV(TAG, "Unimplemented RF OID received: 0x%02X", rx.get_oid()); + } + } else if (rx.get_gid() == nfc::NCI_CORE_GID) { + switch (rx.get_oid()) { + case nfc::NCI_CORE_GENERIC_ERROR_OID: + ESP_LOGV(TAG, "NCI_CORE_GENERIC_ERROR_OID:"); + switch (rx.get_simple_status_response()) { + case nfc::DISCOVERY_ALREADY_STARTED: + ESP_LOGV(TAG, " DISCOVERY_ALREADY_STARTED"); + break; + + case nfc::DISCOVERY_TARGET_ACTIVATION_FAILED: + // Tag removed too soon + ESP_LOGV(TAG, " DISCOVERY_TARGET_ACTIVATION_FAILED"); + if (this->nci_state_ == NCIState::EP_SELECTING) { + this->nci_fsm_set_state_(NCIState::RFST_W4_HOST_SELECT); + if (!this->discovered_endpoint_.empty()) { + this->erase_tag_(this->selecting_endpoint_); + } + } else { + this->stop_discovery_(); + this->nci_fsm_set_state_(NCIState::RFST_IDLE); + } + break; + + case nfc::DISCOVERY_TEAR_DOWN: + ESP_LOGV(TAG, " DISCOVERY_TEAR_DOWN"); + break; + + default: + ESP_LOGW(TAG, "Unknown error: 0x%02X", rx.get_simple_status_response()); + break; + } + break; + + default: + ESP_LOGV(TAG, "Unimplemented NCI Core OID received: 0x%02X", rx.get_oid()); + } + } else { + ESP_LOGV(TAG, "Unimplemented notification: %s", nfc::format_bytes(rx.get_message()).c_str()); + } + break; + + case nfc::NCI_PKT_MT_CTRL_RESPONSE: + ESP_LOGV(TAG, "Unimplemented GID: 0x%02X OID: 0x%02X Full response: %s", rx.get_gid(), rx.get_oid(), + nfc::format_bytes(rx.get_message()).c_str()); + break; + + case nfc::NCI_PKT_MT_CTRL_COMMAND: + ESP_LOGV(TAG, "Unimplemented command: %s", nfc::format_bytes(rx.get_message()).c_str()); + break; + + case nfc::NCI_PKT_MT_DATA: + this->process_data_message_(rx); + break; + + default: + ESP_LOGV(TAG, "Unimplemented message type: %s", nfc::format_bytes(rx.get_message()).c_str()); + break; + } +} + +void PN7150::process_rf_intf_activated_oid_(nfc::NciMessage &rx) { // an endpoint was activated + uint8_t discovery_id = rx.get_message_byte(nfc::RF_INTF_ACTIVATED_NTF_DISCOVERY_ID); + uint8_t interface = rx.get_message_byte(nfc::RF_INTF_ACTIVATED_NTF_INTERFACE); + uint8_t protocol = rx.get_message_byte(nfc::RF_INTF_ACTIVATED_NTF_PROTOCOL); + uint8_t mode_tech = rx.get_message_byte(nfc::RF_INTF_ACTIVATED_NTF_MODE_TECH); + uint8_t max_size = rx.get_message_byte(nfc::RF_INTF_ACTIVATED_NTF_MAX_SIZE); + + ESP_LOGVV(TAG, "Endpoint activated -- interface: 0x%02X, protocol: 0x%02X, mode&tech: 0x%02X, max payload: %u", + interface, protocol, mode_tech, max_size); + + if (mode_tech & nfc::MODE_LISTEN_MASK) { + ESP_LOGVV(TAG, "Tag activated in listen mode"); + this->nci_fsm_set_state_(NCIState::RFST_LISTEN_ACTIVE); + return; + } + + this->nci_fsm_set_state_(NCIState::RFST_POLL_ACTIVE); + auto incoming_tag = + this->build_tag_(mode_tech, std::vector(rx.get_message().begin() + 10, rx.get_message().end())); + + if (incoming_tag == nullptr) { + ESP_LOGE(TAG, "Could not build tag"); + } else { + auto tag_loc = this->find_tag_uid_(incoming_tag->get_uid()); + if (tag_loc.has_value()) { + this->discovered_endpoint_[tag_loc.value()].id = discovery_id; + this->discovered_endpoint_[tag_loc.value()].protocol = protocol; + this->discovered_endpoint_[tag_loc.value()].last_seen = millis(); + ESP_LOGVV(TAG, "Tag cache updated"); + } else { + this->discovered_endpoint_.emplace_back( + DiscoveredEndpoint{discovery_id, protocol, millis(), std::move(incoming_tag), false}); + tag_loc = this->discovered_endpoint_.size() - 1; + ESP_LOGVV(TAG, "Tag added to cache"); + } + + auto &working_endpoint = this->discovered_endpoint_[tag_loc.value()]; + + switch (this->next_task_) { + case EP_CLEAN: + ESP_LOGD(TAG, " Tag cleaning..."); + if (this->clean_endpoint_(working_endpoint.tag->get_uid()) != nfc::STATUS_OK) { + ESP_LOGE(TAG, " Tag cleaning incomplete"); + } + ESP_LOGD(TAG, " Tag cleaned!"); + break; + + case EP_FORMAT: + ESP_LOGD(TAG, " Tag formatting..."); + if (this->format_endpoint_(working_endpoint.tag->get_uid()) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error formatting tag as NDEF"); + } + ESP_LOGD(TAG, " Tag formatted!"); + break; + + case EP_WRITE: + if (this->next_task_message_to_write_ != nullptr) { + ESP_LOGD(TAG, " Tag writing..."); + ESP_LOGD(TAG, " Tag formatting..."); + if (this->format_endpoint_(working_endpoint.tag->get_uid()) != nfc::STATUS_OK) { + ESP_LOGE(TAG, " Tag could not be formatted for writing"); + } else { + ESP_LOGD(TAG, " Writing NDEF data"); + if (this->write_endpoint_(working_endpoint.tag->get_uid(), this->next_task_message_to_write_) != + nfc::STATUS_OK) { + ESP_LOGE(TAG, " Failed to write message to tag"); + } + ESP_LOGD(TAG, " Finished writing NDEF data"); + this->next_task_message_to_write_ = nullptr; + this->on_finished_write_callback_.call(); + } + } + break; + + case EP_READ: + default: + if (!working_endpoint.trig_called) { + ESP_LOGI(TAG, "Read tag type %s with UID %s", working_endpoint.tag->get_tag_type().c_str(), + nfc::format_uid(working_endpoint.tag->get_uid()).c_str()); + if (this->read_endpoint_data_(*working_endpoint.tag) != nfc::STATUS_OK) { + ESP_LOGW(TAG, " Unable to read NDEF record(s)"); + } else if (working_endpoint.tag->has_ndef_message()) { + const auto message = working_endpoint.tag->get_ndef_message(); + const auto records = message->get_records(); + ESP_LOGD(TAG, " NDEF record(s):"); + for (const auto &record : records) { + ESP_LOGD(TAG, " %s - %s", record->get_type().c_str(), record->get_payload().c_str()); + } + } else { + ESP_LOGW(TAG, " No NDEF records found"); + } + for (auto *trigger : this->triggers_ontag_) { + trigger->process(working_endpoint.tag); + } + working_endpoint.trig_called = true; + break; + } + } + if (working_endpoint.tag->get_tag_type() == nfc::MIFARE_CLASSIC) { + this->halt_mifare_classic_tag_(); + } + } + if (this->next_task_ != EP_READ) { + this->read_mode(); + } + + this->stop_discovery_(); + this->nci_fsm_set_state_(NCIState::EP_DEACTIVATING); +} + +void PN7150::process_rf_discover_oid_(nfc::NciMessage &rx) { + auto incoming_tag = this->build_tag_(rx.get_message_byte(nfc::RF_DISCOVER_NTF_MODE_TECH), + std::vector(rx.get_message().begin() + 7, rx.get_message().end())); + + if (incoming_tag == nullptr) { + ESP_LOGE(TAG, "Could not build tag!"); + } else { + auto tag_loc = this->find_tag_uid_(incoming_tag->get_uid()); + if (tag_loc.has_value()) { + this->discovered_endpoint_[tag_loc.value()].id = rx.get_message_byte(nfc::RF_DISCOVER_NTF_DISCOVERY_ID); + this->discovered_endpoint_[tag_loc.value()].protocol = rx.get_message_byte(nfc::RF_DISCOVER_NTF_PROTOCOL); + this->discovered_endpoint_[tag_loc.value()].last_seen = millis(); + ESP_LOGVV(TAG, "Tag found & updated"); + } else { + this->discovered_endpoint_.emplace_back(DiscoveredEndpoint{rx.get_message_byte(nfc::RF_DISCOVER_NTF_DISCOVERY_ID), + rx.get_message_byte(nfc::RF_DISCOVER_NTF_PROTOCOL), + millis(), std::move(incoming_tag), false}); + ESP_LOGVV(TAG, "Tag saved"); + } + } + + if (rx.get_message().back() != nfc::RF_DISCOVER_NTF_NT_MORE) { + this->nci_fsm_set_state_(NCIState::RFST_W4_HOST_SELECT); + ESP_LOGVV(TAG, "Discovered %u endpoints", this->discovered_endpoint_.size()); + } +} + +void PN7150::process_rf_deactivate_oid_(nfc::NciMessage &rx) { + this->ce_state_ = CardEmulationState::CARD_EMU_IDLE; + + switch (rx.get_simple_status_response()) { + case nfc::DEACTIVATION_TYPE_DISCOVERY: + this->nci_fsm_set_state_(NCIState::RFST_DISCOVERY); + break; + + case nfc::DEACTIVATION_TYPE_IDLE: + this->nci_fsm_set_state_(NCIState::RFST_IDLE); + break; + + case nfc::DEACTIVATION_TYPE_SLEEP: + case nfc::DEACTIVATION_TYPE_SLEEP_AF: + if (this->nci_state_ == NCIState::RFST_LISTEN_ACTIVE) { + this->nci_fsm_set_state_(NCIState::RFST_LISTEN_SLEEP); + } else if (this->nci_state_ == NCIState::RFST_POLL_ACTIVE) { + this->nci_fsm_set_state_(NCIState::RFST_W4_HOST_SELECT); + } else { + this->nci_fsm_set_state_(NCIState::RFST_IDLE); + } + break; + + default: + break; + } +} + +void PN7150::process_data_message_(nfc::NciMessage &rx) { + ESP_LOGVV(TAG, "Received data message: %s", nfc::format_bytes(rx.get_message()).c_str()); + + std::vector ndef_response; + this->card_emu_t4t_get_response_(rx.get_message(), ndef_response); + + uint16_t ndef_response_size = ndef_response.size(); + if (!ndef_response_size) { + return; // no message returned, we cannot respond + } + + std::vector tx_msg = {nfc::NCI_PKT_MT_DATA, uint8_t((ndef_response_size & 0xFF00) >> 8), + uint8_t(ndef_response_size & 0x00FF)}; + tx_msg.insert(tx_msg.end(), ndef_response.begin(), ndef_response.end()); + nfc::NciMessage tx(tx_msg); + ESP_LOGVV(TAG, "Sending data message: %s", nfc::format_bytes(tx.get_message()).c_str()); + if (this->transceive_(tx, rx, NFCC_DEFAULT_TIMEOUT, false) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Sending reply for card emulation failed"); + } +} + +void PN7150::card_emu_t4t_get_response_(std::vector &response, std::vector &ndef_response) { + if (this->card_emulation_message_ == nullptr) { + ESP_LOGE(TAG, "No NDEF message is set; tag emulation not possible"); + ndef_response.clear(); + return; + } + + if (equal(response.begin() + nfc::NCI_PKT_HEADER_SIZE, response.end(), std::begin(CARD_EMU_T4T_APP_SELECT))) { + // CARD_EMU_T4T_APP_SELECT + ESP_LOGVV(TAG, "CARD_EMU_NDEF_APP_SELECTED"); + this->ce_state_ = CardEmulationState::CARD_EMU_NDEF_APP_SELECTED; + ndef_response.insert(ndef_response.begin(), std::begin(CARD_EMU_T4T_OK), std::end(CARD_EMU_T4T_OK)); + } else if (equal(response.begin() + nfc::NCI_PKT_HEADER_SIZE, response.end(), std::begin(CARD_EMU_T4T_CC_SELECT))) { + // CARD_EMU_T4T_CC_SELECT + if (this->ce_state_ == CardEmulationState::CARD_EMU_NDEF_APP_SELECTED) { + ESP_LOGVV(TAG, "CARD_EMU_CC_SELECTED"); + this->ce_state_ = CardEmulationState::CARD_EMU_CC_SELECTED; + ndef_response.insert(ndef_response.begin(), std::begin(CARD_EMU_T4T_OK), std::end(CARD_EMU_T4T_OK)); + } + } else if (equal(response.begin() + nfc::NCI_PKT_HEADER_SIZE, response.end(), std::begin(CARD_EMU_T4T_NDEF_SELECT))) { + // CARD_EMU_T4T_NDEF_SELECT + ESP_LOGVV(TAG, "CARD_EMU_NDEF_SELECTED"); + this->ce_state_ = CardEmulationState::CARD_EMU_NDEF_SELECTED; + ndef_response.insert(ndef_response.begin(), std::begin(CARD_EMU_T4T_OK), std::end(CARD_EMU_T4T_OK)); + } else if (equal(response.begin() + nfc::NCI_PKT_HEADER_SIZE, + response.begin() + nfc::NCI_PKT_HEADER_SIZE + sizeof(CARD_EMU_T4T_READ), + std::begin(CARD_EMU_T4T_READ))) { + // CARD_EMU_T4T_READ + if (this->ce_state_ == CardEmulationState::CARD_EMU_CC_SELECTED) { + // CARD_EMU_T4T_READ with CARD_EMU_CC_SELECTED + ESP_LOGVV(TAG, "CARD_EMU_T4T_READ with CARD_EMU_CC_SELECTED"); + uint16_t offset = (response[nfc::NCI_PKT_HEADER_SIZE + 2] << 8) + response[nfc::NCI_PKT_HEADER_SIZE + 3]; + uint8_t length = response[nfc::NCI_PKT_HEADER_SIZE + 4]; + + if (length <= (sizeof(CARD_EMU_T4T_CC) + offset + 2)) { + ndef_response.insert(ndef_response.begin(), std::begin(CARD_EMU_T4T_CC) + offset, + std::begin(CARD_EMU_T4T_CC) + offset + length); + ndef_response.insert(ndef_response.end(), std::begin(CARD_EMU_T4T_OK), std::end(CARD_EMU_T4T_OK)); + } + } else if (this->ce_state_ == CardEmulationState::CARD_EMU_NDEF_SELECTED) { + // CARD_EMU_T4T_READ with CARD_EMU_NDEF_SELECTED + ESP_LOGVV(TAG, "CARD_EMU_T4T_READ with CARD_EMU_NDEF_SELECTED"); + auto ndef_message = this->card_emulation_message_->encode(); + uint16_t ndef_msg_size = ndef_message.size(); + uint16_t offset = (response[nfc::NCI_PKT_HEADER_SIZE + 2] << 8) + response[nfc::NCI_PKT_HEADER_SIZE + 3]; + uint8_t length = response[nfc::NCI_PKT_HEADER_SIZE + 4]; + + ESP_LOGVV(TAG, "Encoded NDEF message: %s", nfc::format_bytes(ndef_message).c_str()); + + if (length <= (ndef_msg_size + offset + 2)) { + if (offset == 0) { + ndef_response.resize(2); + ndef_response[0] = (ndef_msg_size & 0xFF00) >> 8; + ndef_response[1] = (ndef_msg_size & 0x00FF); + if (length > 2) { + ndef_response.insert(ndef_response.end(), ndef_message.begin(), ndef_message.begin() + length - 2); + } + } else if (offset == 1) { + ndef_response.resize(1); + ndef_response[0] = (ndef_msg_size & 0x00FF); + if (length > 1) { + ndef_response.insert(ndef_response.end(), ndef_message.begin(), ndef_message.begin() + length - 1); + } + } else { + ndef_response.insert(ndef_response.end(), ndef_message.begin(), ndef_message.begin() + length); + } + + ndef_response.insert(ndef_response.end(), std::begin(CARD_EMU_T4T_OK), std::end(CARD_EMU_T4T_OK)); + + if ((offset + length) >= (ndef_msg_size + 2)) { + ESP_LOGD(TAG, "NDEF message sent"); + this->on_emulated_tag_scan_callback_.call(); + } + } + } + } else if (equal(response.begin() + nfc::NCI_PKT_HEADER_SIZE, + response.begin() + nfc::NCI_PKT_HEADER_SIZE + sizeof(CARD_EMU_T4T_WRITE), + std::begin(CARD_EMU_T4T_WRITE))) { + // CARD_EMU_T4T_WRITE + if (this->ce_state_ == CardEmulationState::CARD_EMU_NDEF_SELECTED) { + ESP_LOGVV(TAG, "CARD_EMU_T4T_WRITE"); + uint8_t length = response[nfc::NCI_PKT_HEADER_SIZE + 4]; + std::vector ndef_msg_written; + + ndef_msg_written.insert(ndef_msg_written.end(), response.begin() + nfc::NCI_PKT_HEADER_SIZE + 5, + response.begin() + nfc::NCI_PKT_HEADER_SIZE + 5 + length); + ESP_LOGD(TAG, "Received %u-byte NDEF message: %s", length, nfc::format_bytes(ndef_msg_written).c_str()); + ndef_response.insert(ndef_response.end(), std::begin(CARD_EMU_T4T_OK), std::end(CARD_EMU_T4T_OK)); + } + } +} + +uint8_t PN7150::transceive_(nfc::NciMessage &tx, nfc::NciMessage &rx, const uint16_t timeout, + const bool expect_notification) { + uint8_t retries = NFCC_MAX_COMM_FAILS; + + while (retries) { + // first, send the message we need to send + if (this->write_nfcc(tx) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error sending message"); + return nfc::STATUS_FAILED; + } + ESP_LOGVV(TAG, "Wrote: %s", nfc::format_bytes(tx.get_message()).c_str()); + // next, the NFCC should send back a response + if (this->read_nfcc(rx, timeout) != nfc::STATUS_OK) { + ESP_LOGW(TAG, "Error receiving message"); + if (!retries--) { + ESP_LOGE(TAG, " ...giving up"); + return nfc::STATUS_FAILED; + } + } else { + break; + } + } + ESP_LOGVV(TAG, "Read: %s", nfc::format_bytes(rx.get_message()).c_str()); + // validate the response based on the message type that was sent (command vs. data) + if (!tx.message_type_is(nfc::NCI_PKT_MT_DATA)) { + // for commands, the GID and OID should match and the status should be OK + if ((rx.get_gid() != tx.get_gid()) || (rx.get_oid()) != tx.get_oid()) { + ESP_LOGE(TAG, "Incorrect response to command: %s", nfc::format_bytes(rx.get_message()).c_str()); + return nfc::STATUS_FAILED; + } + + if (!rx.simple_status_response_is(nfc::STATUS_OK)) { + ESP_LOGE(TAG, "Error in response to command: %s", nfc::format_bytes(rx.get_message()).c_str()); + } + return rx.get_simple_status_response(); + } else { + // when requesting data from the endpoint, the first response is from the NFCC; we must validate this, first + if ((!rx.message_type_is(nfc::NCI_PKT_MT_CTRL_NOTIFICATION)) || (!rx.gid_is(nfc::NCI_CORE_GID)) || + (!rx.oid_is(nfc::NCI_CORE_CONN_CREDITS_OID)) || (!rx.message_length_is(3))) { + ESP_LOGE(TAG, "Incorrect response to data message: %s", nfc::format_bytes(rx.get_message()).c_str()); + return nfc::STATUS_FAILED; + } + + if (expect_notification) { + // if the NFCC said "OK", there will be additional data to read; this comes back in a notification message + if (this->read_nfcc(rx, timeout) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error receiving data from endpoint"); + return nfc::STATUS_FAILED; + } + ESP_LOGVV(TAG, "Read: %s", nfc::format_bytes(rx.get_message()).c_str()); + } + + return nfc::STATUS_OK; + } +} + +uint8_t PN7150::wait_for_irq_(uint16_t timeout, bool pin_state) { + auto start_time = millis(); + + while (millis() - start_time < timeout) { + if (this->irq_pin_->digital_read() == pin_state) { + return nfc::STATUS_OK; + } + } + ESP_LOGW(TAG, "Timed out waiting for IRQ state"); + return nfc::STATUS_FAILED; +} + +} // namespace pn7150 +} // namespace esphome diff --git a/esphome/components/pn7150/pn7150.h b/esphome/components/pn7150/pn7150.h new file mode 100644 index 0000000000..4aad4e1720 --- /dev/null +++ b/esphome/components/pn7150/pn7150.h @@ -0,0 +1,296 @@ +#pragma once + +#include "esphome/components/nfc/automation.h" +#include "esphome/components/nfc/nci_core.h" +#include "esphome/components/nfc/nci_message.h" +#include "esphome/components/nfc/nfc.h" +#include "esphome/components/nfc/nfc_helpers.h" +#include "esphome/core/component.h" +#include "esphome/core/gpio.h" +#include "esphome/core/helpers.h" + +#include + +namespace esphome { +namespace pn7150 { + +static const uint16_t NFCC_DEFAULT_TIMEOUT = 10; +static const uint16_t NFCC_INIT_TIMEOUT = 50; +static const uint16_t NFCC_TAG_WRITE_TIMEOUT = 15; + +static const uint8_t NFCC_MAX_COMM_FAILS = 3; +static const uint8_t NFCC_MAX_ERROR_COUNT = 10; + +static const uint8_t XCHG_DATA_OID = 0x10; +static const uint8_t MF_SECTORSEL_OID = 0x32; +static const uint8_t MFC_AUTHENTICATE_OID = 0x40; +static const uint8_t TEST_PRBS_OID = 0x30; +static const uint8_t TEST_ANTENNA_OID = 0x3D; +static const uint8_t TEST_GET_REGISTER_OID = 0x33; + +static const uint8_t MFC_AUTHENTICATE_PARAM_KS_A = 0x00; // key select A +static const uint8_t MFC_AUTHENTICATE_PARAM_KS_B = 0x80; // key select B +static const uint8_t MFC_AUTHENTICATE_PARAM_EMBED_KEY = 0x10; + +static const uint8_t CARD_EMU_T4T_APP_SELECT[] = {0x00, 0xA4, 0x04, 0x00, 0x07, 0xD2, 0x76, + 0x00, 0x00, 0x85, 0x01, 0x01, 0x00}; +static const uint8_t CARD_EMU_T4T_CC[] = {0x00, 0x0F, 0x20, 0x00, 0xFF, 0x00, 0xFF, 0x04, + 0x06, 0xE1, 0x04, 0x00, 0xFF, 0x00, 0x00}; +static const uint8_t CARD_EMU_T4T_CC_SELECT[] = {0x00, 0xA4, 0x00, 0x0C, 0x02, 0xE1, 0x03}; +static const uint8_t CARD_EMU_T4T_NDEF_SELECT[] = {0x00, 0xA4, 0x00, 0x0C, 0x02, 0xE1, 0x04}; +static const uint8_t CARD_EMU_T4T_READ[] = {0x00, 0xB0}; +static const uint8_t CARD_EMU_T4T_WRITE[] = {0x00, 0xD6}; +static const uint8_t CARD_EMU_T4T_OK[] = {0x90, 0x00}; +static const uint8_t CARD_EMU_T4T_NOK[] = {0x6A, 0x82}; + +static const uint8_t CORE_CONFIG_SOLO[] = {0x01, // Number of parameter fields + 0x00, // config param identifier (TOTAL_DURATION) + 0x02, // length of value + 0x01, // TOTAL_DURATION (low)... + 0x00}; // TOTAL_DURATION (high): 1 ms + +static const uint8_t CORE_CONFIG_RW_CE[] = {0x01, // Number of parameter fields + 0x00, // config param identifier (TOTAL_DURATION) + 0x02, // length of value + 0xF8, // TOTAL_DURATION (low)... + 0x02}; // TOTAL_DURATION (high): 760 ms + +static const uint8_t PMU_CFG[] = { + 0x01, // Number of parameters + 0xA0, 0x0E, // ext. tag + 3, // length + 0x06, // VBAT1 connected to 5V (CFG2) + 0x64, // TVDD monitoring threshold = 5.0V; TxLDO voltage = 4.7V (in reader & card modes) + 0x01, // RFU; must be 0x00 for CFG1 and 0x01 for CFG2 +}; + +static const uint8_t RF_DISCOVER_MAP_CONFIG[] = { // poll modes + nfc::PROT_T1T, nfc::RF_DISCOVER_MAP_MODE_POLL, + nfc::INTF_FRAME, // poll mode + nfc::PROT_T2T, nfc::RF_DISCOVER_MAP_MODE_POLL, + nfc::INTF_FRAME, // poll mode + nfc::PROT_T3T, nfc::RF_DISCOVER_MAP_MODE_POLL, + nfc::INTF_FRAME, // poll mode + nfc::PROT_ISODEP, nfc::RF_DISCOVER_MAP_MODE_POLL | nfc::RF_DISCOVER_MAP_MODE_LISTEN, + nfc::INTF_ISODEP, // poll & listen mode + nfc::PROT_MIFARE, nfc::RF_DISCOVER_MAP_MODE_POLL, + nfc::INTF_TAGCMD}; // poll mode + +static const uint8_t RF_DISCOVERY_LISTEN_CONFIG[] = {nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCA, // listen mode + nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCB, // listen mode + nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCF}; // listen mode + +static const uint8_t RF_DISCOVERY_POLL_CONFIG[] = {nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCA, // poll mode + nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCB, // poll mode + nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCF}; // poll mode + +static const uint8_t RF_DISCOVERY_CONFIG[] = {nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCA, // poll mode + nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCB, // poll mode + nfc::MODE_POLL | nfc::TECH_PASSIVE_NFCF, // poll mode + nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCA, // listen mode + nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCB, // listen mode + nfc::MODE_LISTEN_MASK | nfc::TECH_PASSIVE_NFCF}; // listen mode + +static const uint8_t RF_LISTEN_MODE_ROUTING_CONFIG[] = {0x00, // "more" (another message is coming) + 1, // number of table entries + 0x01, // type = protocol-based + 3, // length + 0, // DH NFCEE ID, a static ID representing the DH-NFCEE + 0x01, // power state + nfc::PROT_ISODEP}; // protocol + +enum class CardEmulationState : uint8_t { + CARD_EMU_IDLE, + CARD_EMU_NDEF_APP_SELECTED, + CARD_EMU_CC_SELECTED, + CARD_EMU_NDEF_SELECTED, + CARD_EMU_DESFIRE_PROD, +}; + +enum class NCIState : uint8_t { + NONE = 0x00, + NFCC_RESET, + NFCC_INIT, + NFCC_CONFIG, + NFCC_SET_DISCOVER_MAP, + NFCC_SET_LISTEN_MODE_ROUTING, + RFST_IDLE, + RFST_DISCOVERY, + RFST_W4_ALL_DISCOVERIES, + RFST_W4_HOST_SELECT, + RFST_LISTEN_ACTIVE, + RFST_LISTEN_SLEEP, + RFST_POLL_ACTIVE, + EP_DEACTIVATING, + EP_SELECTING, + TEST = 0XFE, + FAILED = 0XFF, +}; + +enum class TestMode : uint8_t { + TEST_NONE = 0x00, + TEST_PRBS, + TEST_ANTENNA, + TEST_GET_REGISTER, +}; + +struct DiscoveredEndpoint { + uint8_t id; + uint8_t protocol; + uint32_t last_seen; + std::unique_ptr tag; + bool trig_called; +}; + +class PN7150 : public Component { + public: + void setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + void loop() override; + + void set_irq_pin(GPIOPin *irq_pin) { this->irq_pin_ = irq_pin; } + void set_ven_pin(GPIOPin *ven_pin) { this->ven_pin_ = ven_pin; } + + void set_tag_ttl(uint32_t ttl) { this->tag_ttl_ = ttl; } + void set_tag_emulation_message(std::shared_ptr message); + void set_tag_emulation_message(const optional &message, optional include_android_app_record); + void set_tag_emulation_message(const char *message, bool include_android_app_record = true); + void set_tag_emulation_off(); + void set_tag_emulation_on(); + bool tag_emulation_enabled() { return this->listening_enabled_; } + + void set_polling_off(); + void set_polling_on(); + bool polling_enabled() { return this->polling_enabled_; } + + void register_ontag_trigger(nfc::NfcOnTagTrigger *trig) { this->triggers_ontag_.push_back(trig); } + void register_ontagremoved_trigger(nfc::NfcOnTagTrigger *trig) { this->triggers_ontagremoved_.push_back(trig); } + + void add_on_emulated_tag_scan_callback(std::function callback) { + this->on_emulated_tag_scan_callback_.add(std::move(callback)); + } + + void add_on_finished_write_callback(std::function callback) { + this->on_finished_write_callback_.add(std::move(callback)); + } + + bool is_writing() { return this->next_task_ != EP_READ; }; + + void read_mode(); + void clean_mode(); + void format_mode(); + void write_mode(); + void set_tag_write_message(std::shared_ptr message); + void set_tag_write_message(optional message, optional include_android_app_record); + + uint8_t set_test_mode(TestMode test_mode, const std::vector &data, std::vector &result); + + protected: + uint8_t reset_core_(bool reset_config, bool power); + uint8_t init_core_(); + uint8_t send_init_config_(); + uint8_t send_core_config_(); + uint8_t refresh_core_config_(); + + uint8_t set_discover_map_(); + + uint8_t set_listen_mode_routing_(); + + uint8_t start_discovery_(); + uint8_t stop_discovery_(); + uint8_t deactivate_(uint8_t type, uint16_t timeout = NFCC_DEFAULT_TIMEOUT); + + void select_endpoint_(); + + uint8_t read_endpoint_data_(nfc::NfcTag &tag); + uint8_t clean_endpoint_(std::vector &uid); + uint8_t format_endpoint_(std::vector &uid); + uint8_t write_endpoint_(std::vector &uid, std::shared_ptr &message); + + std::unique_ptr build_tag_(uint8_t mode_tech, const std::vector &data); + optional find_tag_uid_(const std::vector &uid); + void purge_old_tags_(); + void erase_tag_(uint8_t tag_index); + + /// advance controller state as required + void nci_fsm_transition_(); + /// set new controller state + void nci_fsm_set_state_(NCIState new_state); + /// setting controller to this state caused an error; returns true if too many errors/failures + bool nci_fsm_set_error_state_(NCIState new_state); + /// parse & process incoming messages from the NFCC + void process_message_(); + void process_rf_intf_activated_oid_(nfc::NciMessage &rx); + void process_rf_discover_oid_(nfc::NciMessage &rx); + void process_rf_deactivate_oid_(nfc::NciMessage &rx); + void process_data_message_(nfc::NciMessage &rx); + + void card_emu_t4t_get_response_(std::vector &response, std::vector &ndef_response); + + uint8_t transceive_(nfc::NciMessage &tx, nfc::NciMessage &rx, uint16_t timeout = NFCC_DEFAULT_TIMEOUT, + bool expect_notification = true); + virtual uint8_t read_nfcc(nfc::NciMessage &rx, uint16_t timeout) = 0; + virtual uint8_t write_nfcc(nfc::NciMessage &tx) = 0; + + uint8_t wait_for_irq_(uint16_t timeout = NFCC_DEFAULT_TIMEOUT, bool pin_state = true); + + uint8_t read_mifare_classic_tag_(nfc::NfcTag &tag); + uint8_t read_mifare_classic_block_(uint8_t block_num, std::vector &data); + uint8_t write_mifare_classic_block_(uint8_t block_num, std::vector &data); + uint8_t auth_mifare_classic_block_(uint8_t block_num, uint8_t key_num, const uint8_t *key); + uint8_t sect_to_auth_(uint8_t block_num); + uint8_t format_mifare_classic_mifare_(); + uint8_t format_mifare_classic_ndef_(); + uint8_t write_mifare_classic_tag_(const std::shared_ptr &message); + uint8_t halt_mifare_classic_tag_(); + + uint8_t read_mifare_ultralight_tag_(nfc::NfcTag &tag); + uint8_t read_mifare_ultralight_bytes_(uint8_t start_page, uint16_t num_bytes, std::vector &data); + bool is_mifare_ultralight_formatted_(const std::vector &page_3_to_6); + uint16_t read_mifare_ultralight_capacity_(); + uint8_t find_mifare_ultralight_ndef_(const std::vector &page_3_to_6, uint8_t &message_length, + uint8_t &message_start_index); + uint8_t write_mifare_ultralight_page_(uint8_t page_num, std::vector &write_data); + uint8_t write_mifare_ultralight_tag_(std::vector &uid, const std::shared_ptr &message); + uint8_t clean_mifare_ultralight_(); + + enum NfcTask : uint8_t { + EP_READ = 0, + EP_CLEAN, + EP_FORMAT, + EP_WRITE, + } next_task_{EP_READ}; + + bool config_refresh_pending_{false}; + bool core_config_is_solo_{false}; + bool listening_enabled_{false}; + bool polling_enabled_{true}; + + uint8_t error_count_{0}; + uint8_t fail_count_{0}; + uint32_t last_nci_state_change_{0}; + uint8_t selecting_endpoint_{0}; + uint32_t tag_ttl_{250}; + + GPIOPin *irq_pin_{nullptr}; + GPIOPin *ven_pin_{nullptr}; + + CallbackManager on_emulated_tag_scan_callback_; + CallbackManager on_finished_write_callback_; + + std::vector discovered_endpoint_; + + CardEmulationState ce_state_{CardEmulationState::CARD_EMU_IDLE}; + NCIState nci_state_{NCIState::NFCC_RESET}; + NCIState nci_state_error_{NCIState::NONE}; + + std::shared_ptr card_emulation_message_; + std::shared_ptr next_task_message_to_write_; + + std::vector triggers_ontag_; + std::vector triggers_ontagremoved_; +}; + +} // namespace pn7150 +} // namespace esphome diff --git a/esphome/components/pn7150/pn7150_mifare_classic.cpp b/esphome/components/pn7150/pn7150_mifare_classic.cpp new file mode 100644 index 0000000000..0443929f69 --- /dev/null +++ b/esphome/components/pn7150/pn7150_mifare_classic.cpp @@ -0,0 +1,322 @@ +#include + +#include "pn7150.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace pn7150 { + +static const char *const TAG = "pn7150.mifare_classic"; + +uint8_t PN7150::read_mifare_classic_tag_(nfc::NfcTag &tag) { + uint8_t current_block = 4; + uint8_t message_start_index = 0; + uint32_t message_length = 0; + + if (this->auth_mifare_classic_block_(current_block, nfc::MIFARE_CMD_AUTH_A, nfc::NDEF_KEY) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Tag auth failed while attempting to read tag data"); + return nfc::STATUS_FAILED; + } + std::vector data; + + if (this->read_mifare_classic_block_(current_block, data) == nfc::STATUS_OK) { + if (!nfc::decode_mifare_classic_tlv(data, message_length, message_start_index)) { + return nfc::STATUS_FAILED; + } + } else { + ESP_LOGE(TAG, "Failed to read block %u", current_block); + return nfc::STATUS_FAILED; + } + + uint32_t index = 0; + uint32_t buffer_size = nfc::get_mifare_classic_buffer_size(message_length); + std::vector buffer; + + while (index < buffer_size) { + if (nfc::mifare_classic_is_first_block(current_block)) { + if (this->auth_mifare_classic_block_(current_block, nfc::MIFARE_CMD_AUTH_A, nfc::NDEF_KEY) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Block authentication failed for %u", current_block); + return nfc::STATUS_FAILED; + } + } + std::vector block_data; + if (this->read_mifare_classic_block_(current_block, block_data) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error reading block %u", current_block); + return nfc::STATUS_FAILED; + } else { + buffer.insert(buffer.end(), block_data.begin(), block_data.end()); + } + + index += nfc::MIFARE_CLASSIC_BLOCK_SIZE; + current_block++; + + if (nfc::mifare_classic_is_trailer_block(current_block)) { + current_block++; + } + } + + if (buffer.begin() + message_start_index < buffer.end()) { + buffer.erase(buffer.begin(), buffer.begin() + message_start_index); + } else { + return nfc::STATUS_FAILED; + } + + tag.set_ndef_message(make_unique(buffer)); + + return nfc::STATUS_OK; +} + +uint8_t PN7150::read_mifare_classic_block_(uint8_t block_num, std::vector &data) { + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, {XCHG_DATA_OID, nfc::MIFARE_CMD_READ, block_num}); + + ESP_LOGVV(TAG, "Read XCHG_DATA_REQ: %s", nfc::format_bytes(tx.get_message()).c_str()); + if (this->transceive_(tx, rx) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Timeout reading tag data"); + return nfc::STATUS_FAILED; + } + + if ((!rx.message_type_is(nfc::NCI_PKT_MT_DATA)) || (!rx.simple_status_response_is(XCHG_DATA_OID)) || + (!rx.message_length_is(18))) { + ESP_LOGE(TAG, "MFC read block failed - block 0x%02x", block_num); + ESP_LOGV(TAG, "Read response: %s", nfc::format_bytes(rx.get_message()).c_str()); + return nfc::STATUS_FAILED; + } + + data.insert(data.begin(), rx.get_message().begin() + 4, rx.get_message().end() - 1); + + ESP_LOGVV(TAG, " Block %u: %s", block_num, nfc::format_bytes(data).c_str()); + return nfc::STATUS_OK; +} + +uint8_t PN7150::auth_mifare_classic_block_(uint8_t block_num, uint8_t key_num, const uint8_t *key) { + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, {MFC_AUTHENTICATE_OID, this->sect_to_auth_(block_num), key_num}); + + switch (key_num) { + case nfc::MIFARE_CMD_AUTH_A: + tx.get_message().back() = MFC_AUTHENTICATE_PARAM_KS_A; + break; + + case nfc::MIFARE_CMD_AUTH_B: + tx.get_message().back() = MFC_AUTHENTICATE_PARAM_KS_B; + break; + + default: + break; + } + + if (key != nullptr) { + tx.get_message().back() |= MFC_AUTHENTICATE_PARAM_EMBED_KEY; + tx.get_message().insert(tx.get_message().end(), key, key + 6); + } + + ESP_LOGVV(TAG, "MFC_AUTHENTICATE_REQ: %s", nfc::format_bytes(tx.get_message()).c_str()); + if (this->transceive_(tx, rx) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Sending MFC_AUTHENTICATE_REQ failed"); + return nfc::STATUS_FAILED; + } + if ((!rx.message_type_is(nfc::NCI_PKT_MT_DATA)) || (!rx.simple_status_response_is(MFC_AUTHENTICATE_OID)) || + (rx.get_message()[4] != nfc::STATUS_OK)) { + ESP_LOGE(TAG, "MFC authentication failed - block 0x%02x", block_num); + ESP_LOGVV(TAG, "MFC_AUTHENTICATE_RSP: %s", nfc::format_bytes(rx.get_message()).c_str()); + return nfc::STATUS_FAILED; + } + + ESP_LOGV(TAG, "MFC block %u authentication succeeded", block_num); + return nfc::STATUS_OK; +} + +uint8_t PN7150::sect_to_auth_(const uint8_t block_num) { + const uint8_t first_high_block = nfc::MIFARE_CLASSIC_BLOCKS_PER_SECT_LOW * nfc::MIFARE_CLASSIC_16BLOCK_SECT_START; + if (block_num >= first_high_block) { + return ((block_num - first_high_block) / nfc::MIFARE_CLASSIC_BLOCKS_PER_SECT_HIGH) + + nfc::MIFARE_CLASSIC_16BLOCK_SECT_START; + } + return block_num / nfc::MIFARE_CLASSIC_BLOCKS_PER_SECT_LOW; +} + +uint8_t PN7150::format_mifare_classic_mifare_() { + std::vector blank_buffer( + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); + std::vector trailer_buffer( + {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x07, 0x80, 0x69, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}); + + auto status = nfc::STATUS_OK; + + for (int block = 0; block < 64; block += 4) { + if (this->auth_mifare_classic_block_(block + 3, nfc::MIFARE_CMD_AUTH_B, nfc::DEFAULT_KEY) != nfc::STATUS_OK) { + continue; + } + if (block != 0) { + if (this->write_mifare_classic_block_(block, blank_buffer) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Unable to write block %u", block); + status = nfc::STATUS_FAILED; + } + } + if (this->write_mifare_classic_block_(block + 1, blank_buffer) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Unable to write block %u", block + 1); + status = nfc::STATUS_FAILED; + } + if (this->write_mifare_classic_block_(block + 2, blank_buffer) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Unable to write block %u", block + 2); + status = nfc::STATUS_FAILED; + } + if (this->write_mifare_classic_block_(block + 3, trailer_buffer) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Unable to write block %u", block + 3); + status = nfc::STATUS_FAILED; + } + } + + return status; +} + +uint8_t PN7150::format_mifare_classic_ndef_() { + std::vector empty_ndef_message( + {0x03, 0x03, 0xD0, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); + std::vector blank_block( + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); + std::vector block_1_data( + {0x14, 0x01, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1}); + std::vector block_2_data( + {0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1}); + std::vector block_3_trailer( + {0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0x78, 0x77, 0x88, 0xC1, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}); + std::vector ndef_trailer( + {0xD3, 0xF7, 0xD3, 0xF7, 0xD3, 0xF7, 0x7F, 0x07, 0x88, 0x40, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}); + + if (this->auth_mifare_classic_block_(0, nfc::MIFARE_CMD_AUTH_B, nfc::DEFAULT_KEY) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Unable to authenticate block 0 for formatting"); + return nfc::STATUS_FAILED; + } + if (this->write_mifare_classic_block_(1, block_1_data) != nfc::STATUS_OK) { + return nfc::STATUS_FAILED; + } + if (this->write_mifare_classic_block_(2, block_2_data) != nfc::STATUS_OK) { + return nfc::STATUS_FAILED; + } + if (this->write_mifare_classic_block_(3, block_3_trailer) != nfc::STATUS_OK) { + return nfc::STATUS_FAILED; + } + + ESP_LOGD(TAG, "Sector 0 formatted with NDEF"); + + auto status = nfc::STATUS_OK; + + for (int block = 4; block < 64; block += 4) { + if (this->auth_mifare_classic_block_(block + 3, nfc::MIFARE_CMD_AUTH_B, nfc::DEFAULT_KEY) != nfc::STATUS_OK) { + return nfc::STATUS_FAILED; + } + if (block == 4) { + if (this->write_mifare_classic_block_(block, empty_ndef_message) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Unable to write block %u", block); + status = nfc::STATUS_FAILED; + } + } else { + if (this->write_mifare_classic_block_(block, blank_block) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Unable to write block %u", block); + status = nfc::STATUS_FAILED; + } + } + if (this->write_mifare_classic_block_(block + 1, blank_block) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Unable to write block %u", block + 1); + status = nfc::STATUS_FAILED; + } + if (this->write_mifare_classic_block_(block + 2, blank_block) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Unable to write block %u", block + 2); + status = nfc::STATUS_FAILED; + } + if (this->write_mifare_classic_block_(block + 3, ndef_trailer) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Unable to write trailer block %u", block + 3); + status = nfc::STATUS_FAILED; + } + } + return status; +} + +uint8_t PN7150::write_mifare_classic_block_(uint8_t block_num, std::vector &write_data) { + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, {XCHG_DATA_OID, nfc::MIFARE_CMD_WRITE, block_num}); + + ESP_LOGVV(TAG, "Write XCHG_DATA_REQ 1: %s", nfc::format_bytes(tx.get_message()).c_str()); + if (this->transceive_(tx, rx) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Sending XCHG_DATA_REQ failed"); + return nfc::STATUS_FAILED; + } + // write command part two + tx.set_payload({XCHG_DATA_OID}); + tx.get_message().insert(tx.get_message().end(), write_data.begin(), write_data.end()); + + ESP_LOGVV(TAG, "Write XCHG_DATA_REQ 2: %s", nfc::format_bytes(tx.get_message()).c_str()); + if (this->transceive_(tx, rx, NFCC_TAG_WRITE_TIMEOUT) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "MFC XCHG_DATA timed out waiting for XCHG_DATA_RSP during block write"); + return nfc::STATUS_FAILED; + } + + if ((!rx.message_type_is(nfc::NCI_PKT_MT_DATA)) || (!rx.simple_status_response_is(XCHG_DATA_OID)) || + (rx.get_message()[4] != nfc::MIFARE_CMD_ACK)) { + ESP_LOGE(TAG, "MFC write block failed - block 0x%02x", block_num); + ESP_LOGV(TAG, "Write response: %s", nfc::format_bytes(rx.get_message()).c_str()); + return nfc::STATUS_FAILED; + } + + return nfc::STATUS_OK; +} + +uint8_t PN7150::write_mifare_classic_tag_(const std::shared_ptr &message) { + auto encoded = message->encode(); + + uint32_t message_length = encoded.size(); + uint32_t buffer_length = nfc::get_mifare_classic_buffer_size(message_length); + + encoded.insert(encoded.begin(), 0x03); + if (message_length < 255) { + encoded.insert(encoded.begin() + 1, message_length); + } else { + encoded.insert(encoded.begin() + 1, 0xFF); + encoded.insert(encoded.begin() + 2, (message_length >> 8) & 0xFF); + encoded.insert(encoded.begin() + 3, message_length & 0xFF); + } + encoded.push_back(0xFE); + + encoded.resize(buffer_length, 0); + + uint32_t index = 0; + uint8_t current_block = 4; + + while (index < buffer_length) { + if (nfc::mifare_classic_is_first_block(current_block)) { + if (this->auth_mifare_classic_block_(current_block, nfc::MIFARE_CMD_AUTH_A, nfc::NDEF_KEY) != nfc::STATUS_OK) { + return nfc::STATUS_FAILED; + } + } + + std::vector data(encoded.begin() + index, encoded.begin() + index + nfc::MIFARE_CLASSIC_BLOCK_SIZE); + if (this->write_mifare_classic_block_(current_block, data) != nfc::STATUS_OK) { + return nfc::STATUS_FAILED; + } + index += nfc::MIFARE_CLASSIC_BLOCK_SIZE; + current_block++; + + if (nfc::mifare_classic_is_trailer_block(current_block)) { + // Skipping as cannot write to trailer + current_block++; + } + } + return nfc::STATUS_OK; +} + +uint8_t PN7150::halt_mifare_classic_tag_() { + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, {XCHG_DATA_OID, nfc::MIFARE_CMD_HALT, 0}); + + ESP_LOGVV(TAG, "Halt XCHG_DATA_REQ: %s", nfc::format_bytes(tx.get_message()).c_str()); + if (this->transceive_(tx, rx, NFCC_TAG_WRITE_TIMEOUT) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Sending halt XCHG_DATA_REQ failed"); + return nfc::STATUS_FAILED; + } + return nfc::STATUS_OK; +} + +} // namespace pn7150 +} // namespace esphome diff --git a/esphome/components/pn7150/pn7150_mifare_ultralight.cpp b/esphome/components/pn7150/pn7150_mifare_ultralight.cpp new file mode 100644 index 0000000000..791b0634d6 --- /dev/null +++ b/esphome/components/pn7150/pn7150_mifare_ultralight.cpp @@ -0,0 +1,186 @@ +#include +#include + +#include "pn7150.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace pn7150 { + +static const char *const TAG = "pn7150.mifare_ultralight"; + +uint8_t PN7150::read_mifare_ultralight_tag_(nfc::NfcTag &tag) { + std::vector data; + // pages 3 to 6 contain various info we are interested in -- do one read to grab it all + if (this->read_mifare_ultralight_bytes_(3, nfc::MIFARE_ULTRALIGHT_PAGE_SIZE * nfc::MIFARE_ULTRALIGHT_READ_SIZE, + data) != nfc::STATUS_OK) { + return nfc::STATUS_FAILED; + } + + if (!this->is_mifare_ultralight_formatted_(data)) { + ESP_LOGW(TAG, "Not NDEF formatted"); + return nfc::STATUS_FAILED; + } + + uint8_t message_length; + uint8_t message_start_index; + if (this->find_mifare_ultralight_ndef_(data, message_length, message_start_index) != nfc::STATUS_OK) { + ESP_LOGW(TAG, "Couldn't find NDEF message"); + return nfc::STATUS_FAILED; + } + ESP_LOGVV(TAG, "NDEF message length: %u, start: %u", message_length, message_start_index); + + if (message_length == 0) { + return nfc::STATUS_FAILED; + } + // we already read pages 3-6 earlier -- pick up where we left off so we're not re-reading pages + const uint8_t read_length = message_length + message_start_index > 12 ? message_length + message_start_index - 12 : 0; + if (read_length) { + if (read_mifare_ultralight_bytes_(nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE + 3, read_length, data) != + nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error reading tag data"); + return nfc::STATUS_FAILED; + } + } + // we need to trim off page 3 as well as any bytes ahead of message_start_index + data.erase(data.begin(), data.begin() + message_start_index + nfc::MIFARE_ULTRALIGHT_PAGE_SIZE); + + tag.set_ndef_message(make_unique(data)); + + return nfc::STATUS_OK; +} + +uint8_t PN7150::read_mifare_ultralight_bytes_(uint8_t start_page, uint16_t num_bytes, std::vector &data) { + const uint8_t read_increment = nfc::MIFARE_ULTRALIGHT_READ_SIZE * nfc::MIFARE_ULTRALIGHT_PAGE_SIZE; + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, {nfc::MIFARE_CMD_READ, start_page}); + + for (size_t i = 0; i * read_increment < num_bytes; i++) { + tx.get_message().back() = i * nfc::MIFARE_ULTRALIGHT_READ_SIZE + start_page; + do { // loop because sometimes we struggle here...???... + if (this->transceive_(tx, rx) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error reading tag data"); + return nfc::STATUS_FAILED; + } + } while (rx.get_payload_size() < read_increment); + uint16_t bytes_offset = (i + 1) * read_increment; + auto pages_in_end_itr = bytes_offset <= num_bytes ? rx.get_message().end() - 1 + : rx.get_message().end() - (bytes_offset - num_bytes + 1); + + if ((pages_in_end_itr > rx.get_message().begin()) && (pages_in_end_itr < rx.get_message().end())) { + data.insert(data.end(), rx.get_message().begin() + nfc::NCI_PKT_HEADER_SIZE, pages_in_end_itr); + } + } + + ESP_LOGVV(TAG, "Data read: %s", nfc::format_bytes(data).c_str()); + + return nfc::STATUS_OK; +} + +bool PN7150::is_mifare_ultralight_formatted_(const std::vector &page_3_to_6) { + const uint8_t p4_offset = nfc::MIFARE_ULTRALIGHT_PAGE_SIZE; // page 4 will begin 4 bytes into the vector + + return (page_3_to_6.size() > p4_offset + 3) && + !((page_3_to_6[p4_offset + 0] == 0xFF) && (page_3_to_6[p4_offset + 1] == 0xFF) && + (page_3_to_6[p4_offset + 2] == 0xFF) && (page_3_to_6[p4_offset + 3] == 0xFF)); +} + +uint16_t PN7150::read_mifare_ultralight_capacity_() { + std::vector data; + if (this->read_mifare_ultralight_bytes_(3, nfc::MIFARE_ULTRALIGHT_PAGE_SIZE, data) == nfc::STATUS_OK) { + ESP_LOGV(TAG, "Tag capacity is %u bytes", data[2] * 8U); + return data[2] * 8U; + } + return 0; +} + +uint8_t PN7150::find_mifare_ultralight_ndef_(const std::vector &page_3_to_6, uint8_t &message_length, + uint8_t &message_start_index) { + const uint8_t p4_offset = nfc::MIFARE_ULTRALIGHT_PAGE_SIZE; // page 4 will begin 4 bytes into the vector + + if (!(page_3_to_6.size() > p4_offset + 5)) { + return nfc::STATUS_FAILED; + } + + if (page_3_to_6[p4_offset + 0] == 0x03) { + message_length = page_3_to_6[p4_offset + 1]; + message_start_index = 2; + return nfc::STATUS_OK; + } else if (page_3_to_6[p4_offset + 5] == 0x03) { + message_length = page_3_to_6[p4_offset + 6]; + message_start_index = 7; + return nfc::STATUS_OK; + } + return nfc::STATUS_FAILED; +} + +uint8_t PN7150::write_mifare_ultralight_tag_(std::vector &uid, + const std::shared_ptr &message) { + uint32_t capacity = this->read_mifare_ultralight_capacity_(); + + auto encoded = message->encode(); + + uint32_t message_length = encoded.size(); + uint32_t buffer_length = nfc::get_mifare_ultralight_buffer_size(message_length); + + if (buffer_length > capacity) { + ESP_LOGE(TAG, "Message length exceeds tag capacity %" PRIu32 " > %" PRIu32, buffer_length, capacity); + return nfc::STATUS_FAILED; + } + + encoded.insert(encoded.begin(), 0x03); + if (message_length < 255) { + encoded.insert(encoded.begin() + 1, message_length); + } else { + encoded.insert(encoded.begin() + 1, 0xFF); + encoded.insert(encoded.begin() + 2, (message_length >> 8) & 0xFF); + encoded.insert(encoded.begin() + 2, message_length & 0xFF); + } + encoded.push_back(0xFE); + + encoded.resize(buffer_length, 0); + + uint32_t index = 0; + uint8_t current_page = nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE; + + while (index < buffer_length) { + std::vector data(encoded.begin() + index, encoded.begin() + index + nfc::MIFARE_ULTRALIGHT_PAGE_SIZE); + if (this->write_mifare_ultralight_page_(current_page, data) != nfc::STATUS_OK) { + return nfc::STATUS_FAILED; + } + index += nfc::MIFARE_ULTRALIGHT_PAGE_SIZE; + current_page++; + } + return nfc::STATUS_OK; +} + +uint8_t PN7150::clean_mifare_ultralight_() { + uint32_t capacity = this->read_mifare_ultralight_capacity_(); + uint8_t pages = (capacity / nfc::MIFARE_ULTRALIGHT_PAGE_SIZE) + nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE; + + std::vector blank_data = {0x00, 0x00, 0x00, 0x00}; + + for (int i = nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE; i < pages; i++) { + if (this->write_mifare_ultralight_page_(i, blank_data) != nfc::STATUS_OK) { + return nfc::STATUS_FAILED; + } + } + return nfc::STATUS_OK; +} + +uint8_t PN7150::write_mifare_ultralight_page_(uint8_t page_num, std::vector &write_data) { + std::vector payload = {nfc::MIFARE_CMD_WRITE_ULTRALIGHT, page_num}; + payload.insert(payload.end(), write_data.begin(), write_data.end()); + + nfc::NciMessage rx; + nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, payload); + + if (this->transceive_(tx, rx, NFCC_TAG_WRITE_TIMEOUT) != nfc::STATUS_OK) { + ESP_LOGE(TAG, "Error writing page %u", page_num); + return nfc::STATUS_FAILED; + } + return nfc::STATUS_OK; +} + +} // namespace pn7150 +} // namespace esphome diff --git a/esphome/components/pn7150_i2c/__init__.py b/esphome/components/pn7150_i2c/__init__.py new file mode 100644 index 0000000000..5f48a0f3cb --- /dev/null +++ b/esphome/components/pn7150_i2c/__init__.py @@ -0,0 +1,25 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, pn7150 +from esphome.const import CONF_ID + +AUTO_LOAD = ["pn7150"] +CODEOWNERS = ["@kbx81", "@jesserockz"] +DEPENDENCIES = ["i2c"] + +pn7150_i2c_ns = cg.esphome_ns.namespace("pn7150_i2c") +PN7150I2C = pn7150_i2c_ns.class_("PN7150I2C", pn7150.PN7150, i2c.I2CDevice) + +CONFIG_SCHEMA = cv.All( + pn7150.PN7150_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(PN7150I2C), + } + ).extend(i2c.i2c_device_schema(0x28)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await pn7150.setup_pn7150(var, config) + await i2c.register_i2c_device(var, config) diff --git a/esphome/components/pn7150_i2c/pn7150_i2c.cpp b/esphome/components/pn7150_i2c/pn7150_i2c.cpp new file mode 100644 index 0000000000..38b3102b37 --- /dev/null +++ b/esphome/components/pn7150_i2c/pn7150_i2c.cpp @@ -0,0 +1,49 @@ +#include "pn7150_i2c.h" +#include "esphome/core/log.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace pn7150_i2c { + +static const char *const TAG = "pn7150_i2c"; + +uint8_t PN7150I2C::read_nfcc(nfc::NciMessage &rx, const uint16_t timeout) { + if (this->wait_for_irq_(timeout) != nfc::STATUS_OK) { + ESP_LOGW(TAG, "read_nfcc_() timeout waiting for IRQ"); + return nfc::STATUS_FAILED; + } + + rx.get_message().resize(nfc::NCI_PKT_HEADER_SIZE); + if (!this->read_bytes_raw(rx.get_message().data(), nfc::NCI_PKT_HEADER_SIZE)) { + return nfc::STATUS_FAILED; + } + + uint8_t length = rx.get_payload_size(); + if (length > 0) { + rx.get_message().resize(length + nfc::NCI_PKT_HEADER_SIZE); + if (!this->read_bytes_raw(rx.get_message().data() + nfc::NCI_PKT_HEADER_SIZE, length)) { + return nfc::STATUS_FAILED; + } + } + // semaphore to ensure transaction is complete before returning + if (this->wait_for_irq_(pn7150::NFCC_DEFAULT_TIMEOUT, false) != nfc::STATUS_OK) { + ESP_LOGW(TAG, "read_nfcc_() post-read timeout waiting for IRQ line to clear"); + return nfc::STATUS_FAILED; + } + return nfc::STATUS_OK; +} + +uint8_t PN7150I2C::write_nfcc(nfc::NciMessage &tx) { + if (this->write(tx.encode().data(), tx.encode().size()) == i2c::ERROR_OK) { + return nfc::STATUS_OK; + } + return nfc::STATUS_FAILED; +} + +void PN7150I2C::dump_config() { + PN7150::dump_config(); + LOG_I2C_DEVICE(this); +} + +} // namespace pn7150_i2c +} // namespace esphome diff --git a/esphome/components/pn7150_i2c/pn7150_i2c.h b/esphome/components/pn7150_i2c/pn7150_i2c.h new file mode 100644 index 0000000000..9308dddd26 --- /dev/null +++ b/esphome/components/pn7150_i2c/pn7150_i2c.h @@ -0,0 +1,22 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/pn7150/pn7150.h" +#include "esphome/components/i2c/i2c.h" + +#include + +namespace esphome { +namespace pn7150_i2c { + +class PN7150I2C : public pn7150::PN7150, public i2c::I2CDevice { + public: + void dump_config() override; + + protected: + uint8_t read_nfcc(nfc::NciMessage &rx, uint16_t timeout) override; + uint8_t write_nfcc(nfc::NciMessage &tx) override; +}; + +} // namespace pn7150_i2c +} // namespace esphome diff --git a/tests/test1.yaml b/tests/test1.yaml index 59818bbde5..f7b433cce2 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -3388,6 +3388,15 @@ pn532_spi: pn532_i2c: i2c_id: i2c_bus +pn7150_i2c: + i2c_id: i2c_bus + irq_pin: + allow_other_uses: true + number: GPIO32 + ven_pin: + allow_other_uses: true + number: GPIO16 + pn7160_i2c: id: nfcc_pn7160_i2c i2c_id: i2c_bus From 9f27eadaee108cde1410c9de65fa5a82bd555e55 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 14 Dec 2023 08:30:45 +0900 Subject: [PATCH 82/89] Bump version to 2024.1.0-dev --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 101a690db5..5de34b86cd 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2023.12.0-dev" +__version__ = "2024.1.0-dev" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 777cdb1c21ec3b467f47cf71ffdc53ee9112b69a Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 14 Dec 2023 13:24:16 +0900 Subject: [PATCH 83/89] Allow use of CDC/JTAG loggers on esp32 variants with Arduino (#4658) * Allow use of CDC/JTAG loggers on esp32 variants with Arduino * Only on s2/s3 * Separate C3 from S2/S3 * C code builds & runs correctly, still needs work though * Works on S2 * It works! * Remove unnecessary header --------- Co-authored-by: Keith Burzinski --- esphome/components/logger/__init__.py | 2 -- esphome/components/logger/logger.cpp | 32 ++++++++++++++++++++++++--- esphome/components/logger/logger.h | 2 -- 3 files changed, 29 insertions(+), 7 deletions(-) diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index e431997276..11a7f996f0 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -124,8 +124,6 @@ is_log_level = cv.one_of(*LOG_LEVELS, upper=True) def uart_selection(value): if CORE.is_esp32: - if value.upper() in ESP_IDF_UARTS and not CORE.using_esp_idf: - raise cv.Invalid(f"Only esp-idf framework supports {value}.") variant = get_esp32_variant() if variant in UART_SELECTION_ESP32: return cv.one_of(*UART_SELECTION_ESP32[variant], upper=True)(value) diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index 2d2524b5f4..e0ca0806cb 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -236,8 +236,13 @@ void Logger::pre_setup() { this->hw_serial_ = &Serial1; Serial1.begin(this->baud_rate_); #else +#if ARDUINO_USB_CDC_ON_BOOT this->hw_serial_ = &Serial; Serial.begin(this->baud_rate_); +#else + this->hw_serial_ = &Serial; + Serial.begin(this->baud_rate_); +#endif #endif #ifdef USE_ESP8266 if (this->uart_ == UART_SELECTION_UART0_SWAP) { @@ -265,12 +270,35 @@ void Logger::pre_setup() { Serial2.begin(this->baud_rate_); break; #endif +#if defined(USE_ESP32) && \ + (defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32C3)) +#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) + case UART_SELECTION_USB_CDC: +#endif // USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 +#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S3) + case UART_SELECTION_USB_SERIAL_JTAG: +#endif // USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32S3 +#ifdef USE_ESP32_VARIANT_ESP32C3 + this->hw_serial_ = &Serial; + Serial.begin(this->baud_rate_); +#endif // USE_ESP32_VARIANT_ESP32C3 +#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) +#if ARDUINO_USB_CDC_ON_BOOT + this->hw_serial_ = &Serial; + Serial.begin(this->baud_rate_); +#else + this->hw_serial_ = &Serial; + Serial.begin(this->baud_rate_); +#endif // ARDUINO_USB_CDC_ON_BOOT +#endif // USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 + break; +#endif // USE_ESP32 && (USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32C3) #ifdef USE_RP2040 case UART_SELECTION_USB_CDC: this->hw_serial_ = &Serial; Serial.begin(this->baud_rate_); break; -#endif +#endif // USE_RP2040 } #endif // USE_ARDUINO #ifdef USE_ESP_IDF @@ -393,14 +421,12 @@ const char *const UART_SELECTIONS[] = { "UART2", #endif // !USE_ESP32_VARIANT_ESP32C3 && !USE_ESP32_VARINT_ESP32C6 && !USE_ESP32_VARIANT_ESP32S2 && // !USE_ESP32_VARIANT_ESP32S3 && !USE_ESP32_VARIANT_ESP32H2 -#if defined(USE_ESP_IDF) #if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) "USB_CDC", #endif // USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 #if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S3) "USB_SERIAL_JTAG", #endif // USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32S3 -#endif // USE_ESP_IDF }; #endif // USE_ESP32 #ifdef USE_ESP8266 diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index 3816b1dd14..68efc056df 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -45,7 +45,6 @@ enum UARTSelection { UART_SELECTION_UART2, #endif // !USE_ESP32_VARIANT_ESP32C3 && !USE_ESP32_VARIANT_ESP32C6 && !USE_ESP32_VARIANT_ESP32S2 && // !USE_ESP32_VARIANT_ESP32S3 && !USE_ESP32_VARIANT_ESP32H2 -#ifdef USE_ESP_IDF #if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) UART_SELECTION_USB_CDC, #endif // USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 @@ -54,7 +53,6 @@ enum UARTSelection { UART_SELECTION_USB_SERIAL_JTAG, #endif // USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32S3 || // USE_ESP32_VARIANT_ESP32H2 -#endif // USE_ESP_IDF #endif // USE_ESP32 #ifdef USE_ESP8266 UART_SELECTION_UART0_SWAP, From 8c37066ed90a5eba7cf7ca95783c6ce4c09ace7b Mon Sep 17 00:00:00 2001 From: Fabian Date: Thu, 14 Dec 2023 05:47:31 +0100 Subject: [PATCH 84/89] [Logger] ESP32 S3 serial logger (#4853) * Add support for ESP32 S3 logger. * fix default * Remove cpp & h changes to combine with PR #4658 * Not enough attention to details. * Add build flag * Validation fix * Fix validation for real this time --------- Co-authored-by: Your Name Co-authored-by: Keith Burzinski --- esphome/components/logger/__init__.py | 10 ++++- esphome/config_validation.py | 53 +++++++++++++++++++++++++-- 2 files changed, 58 insertions(+), 5 deletions(-) diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index 11a7f996f0..be302bd489 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -97,7 +97,7 @@ UART_SELECTION_LIBRETINY = { COMPONENT_RTL87XX: [DEFAULT, UART0, UART1, UART2], } -ESP_IDF_UARTS = [USB_CDC, USB_SERIAL_JTAG] +ESP_ARDUINO_UNSUPPORTED_USB_UARTS = [USB_SERIAL_JTAG] UART_SELECTION_RP2040 = [USB_CDC, UART0, UART1] @@ -124,6 +124,8 @@ is_log_level = cv.one_of(*LOG_LEVELS, upper=True) def uart_selection(value): if CORE.is_esp32: + if CORE.using_arduino and value.upper() in ESP_ARDUINO_UNSUPPORTED_USB_UARTS: + raise cv.Invalid(f"Arduino framework does not support {value}.") variant = get_esp32_variant() if variant in UART_SELECTION_ESP32: return cv.one_of(*UART_SELECTION_ESP32[variant], upper=True)(value) @@ -169,6 +171,8 @@ CONFIG_SCHEMA = cv.All( CONF_HARDWARE_UART, esp8266=UART0, esp32=UART0, + esp32_s2=USB_CDC, + esp32_s3=USB_CDC, rp2040=USB_CDC, bk72xx=DEFAULT, rtl87xx=DEFAULT, @@ -256,6 +260,10 @@ async def to_code(config): if config.get(CONF_ESP8266_STORE_LOG_STRINGS_IN_FLASH): cg.add_build_flag("-DUSE_STORE_LOG_STR_IN_FLASH") + if CORE.using_arduino: + if config[CONF_HARDWARE_UART] == USB_CDC: + cg.add_build_flag("-DARDUINO_USB_CDC_ON_BOOT=1") + if CORE.using_esp_idf: if config[CONF_HARDWARE_UART] == USB_CDC: add_idf_sdkconfig_option("CONFIG_ESP_CONSOLE_USB_CDC", True) diff --git a/esphome/config_validation.py b/esphome/config_validation.py index ad2ee11512..fdbe7d6320 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -1528,6 +1528,12 @@ class SplitDefault(Optional): esp32=vol.UNDEFINED, esp32_arduino=vol.UNDEFINED, esp32_idf=vol.UNDEFINED, + esp32_s2=vol.UNDEFINED, + esp32_s2_arduino=vol.UNDEFINED, + esp32_s2_idf=vol.UNDEFINED, + esp32_s3=vol.UNDEFINED, + esp32_s3_arduino=vol.UNDEFINED, + esp32_s3_idf=vol.UNDEFINED, rp2040=vol.UNDEFINED, bk72xx=vol.UNDEFINED, rtl87xx=vol.UNDEFINED, @@ -1541,6 +1547,26 @@ class SplitDefault(Optional): self._esp32_idf_default = vol.default_factory( esp32_idf if esp32 is vol.UNDEFINED else esp32 ) + self._esp32_s2_arduino_default = vol.default_factory( + (esp32_s2_arduino if esp32 is vol.UNDEFINED else esp32) + if esp32_s2 is vol.UNDEFINED + else esp32_s2 + ) + self._esp32_s2_idf_default = vol.default_factory( + (esp32_s2_idf if esp32 is vol.UNDEFINED else esp32) + if esp32_s2 is vol.UNDEFINED + else esp32_s2 + ) + self._esp32_s3_arduino_default = vol.default_factory( + (esp32_s3_arduino if esp32 is vol.UNDEFINED else esp32) + if esp32_s3 is vol.UNDEFINED + else esp32_s3 + ) + self._esp32_s3_idf_default = vol.default_factory( + (esp32_s3_idf if esp32 is vol.UNDEFINED else esp32) + if esp32_s3 is vol.UNDEFINED + else esp32_s3 + ) self._rp2040_default = vol.default_factory(rp2040) self._bk72xx_default = vol.default_factory(bk72xx) self._rtl87xx_default = vol.default_factory(rtl87xx) @@ -1550,10 +1576,29 @@ class SplitDefault(Optional): def default(self): if CORE.is_esp8266: return self._esp8266_default - if CORE.is_esp32 and CORE.using_arduino: - return self._esp32_arduino_default - if CORE.is_esp32 and CORE.using_esp_idf: - return self._esp32_idf_default + if CORE.is_esp32: + from esphome.components.esp32 import get_esp32_variant + from esphome.components.esp32.const import ( + VARIANT_ESP32S2, + VARIANT_ESP32S3, + ) + + variant = get_esp32_variant() + if variant == VARIANT_ESP32S2: + if CORE.using_arduino: + return self._esp32_s2_arduino_default + if CORE.using_esp_idf: + return self._esp32_s2_idf_default + elif variant == VARIANT_ESP32S3: + if CORE.using_arduino: + return self._esp32_s3_arduino_default + if CORE.using_esp_idf: + return self._esp32_s3_idf_default + else: + if CORE.using_arduino: + return self._esp32_arduino_default + if CORE.using_esp_idf: + return self._esp32_idf_default if CORE.is_rp2040: return self._rp2040_default if CORE.is_bk72xx: From a3cc5518565c56239808b34e3881090c5385632a Mon Sep 17 00:00:00 2001 From: jochenvg Date: Thu, 14 Dec 2023 05:01:01 +0000 Subject: [PATCH 85/89] Support toggle action for template cover (#5917) --- esphome/components/template/cover/__init__.py | 7 +++++++ esphome/components/template/cover/template_cover.cpp | 10 ++++++++++ esphome/components/template/cover/template_cover.h | 4 ++++ 3 files changed, 21 insertions(+) diff --git a/esphome/components/template/cover/__init__.py b/esphome/components/template/cover/__init__.py index 8844ddd6ab..43d0be99b4 100644 --- a/esphome/components/template/cover/__init__.py +++ b/esphome/components/template/cover/__init__.py @@ -31,6 +31,7 @@ RESTORE_MODES = { } CONF_HAS_POSITION = "has_position" +CONF_TOGGLE_ACTION = "toggle_action" CONFIG_SCHEMA = cover.COVER_SCHEMA.extend( { @@ -44,6 +45,7 @@ CONFIG_SCHEMA = cover.COVER_SCHEMA.extend( cv.Optional(CONF_STOP_ACTION): automation.validate_automation(single=True), cv.Optional(CONF_TILT_ACTION): automation.validate_automation(single=True), cv.Optional(CONF_TILT_LAMBDA): cv.returning_lambda, + cv.Optional(CONF_TOGGLE_ACTION): automation.validate_automation(single=True), cv.Optional(CONF_POSITION_ACTION): automation.validate_automation(single=True), cv.Optional(CONF_RESTORE_MODE, default="RESTORE"): cv.enum( RESTORE_MODES, upper=True @@ -74,6 +76,11 @@ async def to_code(config): var.get_stop_trigger(), [], config[CONF_STOP_ACTION] ) cg.add(var.set_has_stop(True)) + if CONF_TOGGLE_ACTION in config: + await automation.build_automation( + var.get_toggle_trigger(), [], config[CONF_TOGGLE_ACTION] + ) + cg.add(var.set_has_toggle(True)) if CONF_TILT_ACTION in config: await automation.build_automation( var.get_tilt_trigger(), [(float, "tilt")], config[CONF_TILT_ACTION] diff --git a/esphome/components/template/cover/template_cover.cpp b/esphome/components/template/cover/template_cover.cpp index b16e439943..2d6c3087ae 100644 --- a/esphome/components/template/cover/template_cover.cpp +++ b/esphome/components/template/cover/template_cover.cpp @@ -12,6 +12,7 @@ TemplateCover::TemplateCover() : open_trigger_(new Trigger<>()), close_trigger_(new Trigger<>), stop_trigger_(new Trigger<>()), + toggle_trigger_(new Trigger<>()), position_trigger_(new Trigger()), tilt_trigger_(new Trigger()) {} void TemplateCover::setup() { @@ -68,6 +69,7 @@ float TemplateCover::get_setup_priority() const { return setup_priority::HARDWAR Trigger<> *TemplateCover::get_open_trigger() const { return this->open_trigger_; } Trigger<> *TemplateCover::get_close_trigger() const { return this->close_trigger_; } Trigger<> *TemplateCover::get_stop_trigger() const { return this->stop_trigger_; } +Trigger<> *TemplateCover::get_toggle_trigger() const { return this->toggle_trigger_; } void TemplateCover::dump_config() { LOG_COVER("", "Template Cover", this); } void TemplateCover::control(const CoverCall &call) { if (call.get_stop()) { @@ -76,6 +78,12 @@ void TemplateCover::control(const CoverCall &call) { this->prev_command_trigger_ = this->stop_trigger_; this->publish_state(); } + if (call.get_toggle().has_value()) { + this->stop_prev_trigger_(); + this->toggle_trigger_->trigger(); + this->prev_command_trigger_ = this->toggle_trigger_; + this->publish_state(); + } if (call.get_position().has_value()) { auto pos = *call.get_position(); this->stop_prev_trigger_(); @@ -110,6 +118,7 @@ CoverTraits TemplateCover::get_traits() { auto traits = CoverTraits(); traits.set_is_assumed_state(this->assumed_state_); traits.set_supports_stop(this->has_stop_); + traits.set_supports_toggle(this->has_toggle_); traits.set_supports_position(this->has_position_); traits.set_supports_tilt(this->has_tilt_); return traits; @@ -118,6 +127,7 @@ Trigger *TemplateCover::get_position_trigger() const { return this->posit Trigger *TemplateCover::get_tilt_trigger() const { return this->tilt_trigger_; } void TemplateCover::set_tilt_lambda(std::function()> &&tilt_f) { this->tilt_f_ = tilt_f; } void TemplateCover::set_has_stop(bool has_stop) { this->has_stop_ = has_stop; } +void TemplateCover::set_has_toggle(bool has_toggle) { this->has_toggle_ = has_toggle; } void TemplateCover::set_has_position(bool has_position) { this->has_position_ = has_position; } void TemplateCover::set_has_tilt(bool has_tilt) { this->has_tilt_ = has_tilt; } void TemplateCover::stop_prev_trigger_() { diff --git a/esphome/components/template/cover/template_cover.h b/esphome/components/template/cover/template_cover.h index 4ff5caf1db..958c94b0a6 100644 --- a/esphome/components/template/cover/template_cover.h +++ b/esphome/components/template/cover/template_cover.h @@ -21,6 +21,7 @@ class TemplateCover : public cover::Cover, public Component { Trigger<> *get_open_trigger() const; Trigger<> *get_close_trigger() const; Trigger<> *get_stop_trigger() const; + Trigger<> *get_toggle_trigger() const; Trigger *get_position_trigger() const; Trigger *get_tilt_trigger() const; void set_optimistic(bool optimistic); @@ -29,6 +30,7 @@ class TemplateCover : public cover::Cover, public Component { void set_has_stop(bool has_stop); void set_has_position(bool has_position); void set_has_tilt(bool has_tilt); + void set_has_toggle(bool has_toggle); void set_restore_mode(TemplateCoverRestoreMode restore_mode) { restore_mode_ = restore_mode; } void setup() override; @@ -50,7 +52,9 @@ class TemplateCover : public cover::Cover, public Component { Trigger<> *open_trigger_; Trigger<> *close_trigger_; bool has_stop_{false}; + bool has_toggle_{false}; Trigger<> *stop_trigger_; + Trigger<> *toggle_trigger_; Trigger<> *prev_command_trigger_{nullptr}; Trigger *position_trigger_; bool has_position_{false}; From 0a188ad9d22a2b5a8060ec7ad3318cb9dba509e2 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 14 Dec 2023 17:33:04 +0900 Subject: [PATCH 86/89] Fix SplitDefault with variants (#5928) --- esphome/config_validation.py | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/esphome/config_validation.py b/esphome/config_validation.py index fdbe7d6320..7b94608509 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -1518,6 +1518,13 @@ class GenerateID(Optional): super().__init__(key, default=lambda: None) +def _get_priority_default(*args): + for arg in args: + if arg is not vol.UNDEFINED: + return arg + return vol.UNDEFINED + + class SplitDefault(Optional): """Mark this key to have a split default for ESP8266/ESP32.""" @@ -1542,30 +1549,22 @@ class SplitDefault(Optional): super().__init__(key) self._esp8266_default = vol.default_factory(esp8266) self._esp32_arduino_default = vol.default_factory( - esp32_arduino if esp32 is vol.UNDEFINED else esp32 + _get_priority_default(esp32, esp32_arduino) ) self._esp32_idf_default = vol.default_factory( - esp32_idf if esp32 is vol.UNDEFINED else esp32 + _get_priority_default(esp32, esp32_idf) ) self._esp32_s2_arduino_default = vol.default_factory( - (esp32_s2_arduino if esp32 is vol.UNDEFINED else esp32) - if esp32_s2 is vol.UNDEFINED - else esp32_s2 + _get_priority_default(esp32_s2, esp32, esp32_s2_arduino, esp32_arduino) ) self._esp32_s2_idf_default = vol.default_factory( - (esp32_s2_idf if esp32 is vol.UNDEFINED else esp32) - if esp32_s2 is vol.UNDEFINED - else esp32_s2 + _get_priority_default(esp32_s2, esp32, esp32_s2_idf, esp32_idf) ) self._esp32_s3_arduino_default = vol.default_factory( - (esp32_s3_arduino if esp32 is vol.UNDEFINED else esp32) - if esp32_s3 is vol.UNDEFINED - else esp32_s3 + _get_priority_default(esp32_s3, esp32, esp32_s3_arduino, esp32_arduino) ) self._esp32_s3_idf_default = vol.default_factory( - (esp32_s3_idf if esp32 is vol.UNDEFINED else esp32) - if esp32_s3 is vol.UNDEFINED - else esp32_s3 + _get_priority_default(esp32_s3, esp32, esp32_s3_idf, esp32_idf) ) self._rp2040_default = vol.default_factory(rp2040) self._bk72xx_default = vol.default_factory(bk72xx) From 300343ae2495ef66407d95254308517b3857ac40 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Fri, 15 Dec 2023 11:24:52 +1100 Subject: [PATCH 87/89] ESP32-S3 and ESP-IDF don't play well with USB_CDC and need USB_SERIAL_JTAG (#5929) Co-authored-by: Keith Burzinski --- esphome/components/logger/__init__.py | 4 +- esphome/config_validation.py | 27 +++++++--- tests/test8.1.yaml | 78 +++++++++++++++++++++++++++ tests/test8.2.yaml | 75 ++++++++++++++++++++++++++ 4 files changed, 177 insertions(+), 7 deletions(-) create mode 100644 tests/test8.1.yaml create mode 100644 tests/test8.2.yaml diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index be302bd489..6cad783db9 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -172,7 +172,9 @@ CONFIG_SCHEMA = cv.All( esp8266=UART0, esp32=UART0, esp32_s2=USB_CDC, - esp32_s3=USB_CDC, + esp32_s3_idf=USB_SERIAL_JTAG, + esp32_c3_idf=USB_SERIAL_JTAG, + esp32_s3_arduino=USB_CDC, rp2040=USB_CDC, bk72xx=DEFAULT, rtl87xx=DEFAULT, diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 7b94608509..8f2e080b46 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -1541,6 +1541,9 @@ class SplitDefault(Optional): esp32_s3=vol.UNDEFINED, esp32_s3_arduino=vol.UNDEFINED, esp32_s3_idf=vol.UNDEFINED, + esp32_c3=vol.UNDEFINED, + esp32_c3_arduino=vol.UNDEFINED, + esp32_c3_idf=vol.UNDEFINED, rp2040=vol.UNDEFINED, bk72xx=vol.UNDEFINED, rtl87xx=vol.UNDEFINED, @@ -1549,22 +1552,28 @@ class SplitDefault(Optional): super().__init__(key) self._esp8266_default = vol.default_factory(esp8266) self._esp32_arduino_default = vol.default_factory( - _get_priority_default(esp32, esp32_arduino) + _get_priority_default(esp32_arduino, esp32) ) self._esp32_idf_default = vol.default_factory( - _get_priority_default(esp32, esp32_idf) + _get_priority_default(esp32_idf, esp32) ) self._esp32_s2_arduino_default = vol.default_factory( - _get_priority_default(esp32_s2, esp32, esp32_s2_arduino, esp32_arduino) + _get_priority_default(esp32_s2_arduino, esp32_s2, esp32_arduino, esp32) ) self._esp32_s2_idf_default = vol.default_factory( - _get_priority_default(esp32_s2, esp32, esp32_s2_idf, esp32_idf) + _get_priority_default(esp32_s2_idf, esp32_s2, esp32_idf, esp32) ) self._esp32_s3_arduino_default = vol.default_factory( - _get_priority_default(esp32_s3, esp32, esp32_s3_arduino, esp32_arduino) + _get_priority_default(esp32_s3_arduino, esp32_s3, esp32_arduino, esp32) ) self._esp32_s3_idf_default = vol.default_factory( - _get_priority_default(esp32_s3, esp32, esp32_s3_idf, esp32_idf) + _get_priority_default(esp32_s3_idf, esp32_s3, esp32_idf, esp32) + ) + self._esp32_c3_arduino_default = vol.default_factory( + _get_priority_default(esp32_c3_arduino, esp32_c3, esp32_arduino, esp32) + ) + self._esp32_c3_idf_default = vol.default_factory( + _get_priority_default(esp32_c3_idf, esp32_c3, esp32_idf, esp32) ) self._rp2040_default = vol.default_factory(rp2040) self._bk72xx_default = vol.default_factory(bk72xx) @@ -1580,6 +1589,7 @@ class SplitDefault(Optional): from esphome.components.esp32.const import ( VARIANT_ESP32S2, VARIANT_ESP32S3, + VARIANT_ESP32C3, ) variant = get_esp32_variant() @@ -1593,6 +1603,11 @@ class SplitDefault(Optional): return self._esp32_s3_arduino_default if CORE.using_esp_idf: return self._esp32_s3_idf_default + elif variant == VARIANT_ESP32C3: + if CORE.using_arduino: + return self._esp32_c3_arduino_default + if CORE.using_esp_idf: + return self._esp32_c3_idf_default else: if CORE.using_arduino: return self._esp32_arduino_default diff --git a/tests/test8.1.yaml b/tests/test8.1.yaml new file mode 100644 index 0000000000..bc1d2e22a4 --- /dev/null +++ b/tests/test8.1.yaml @@ -0,0 +1,78 @@ +# Tests for ESP32-S3 boards - IDf +--- +wifi: + ssid: "ssid" + +network: + enable_ipv6: true + +esp32: + board: esp32s3box + variant: ESP32S3 + framework: + type: esp-idf + +esphome: + name: esp32-s3-test + +logger: + +debug: + +psram: + +spi: + - id: spi_id_1 + clk_pin: + number: GPIO7 + allow_other_uses: false + mosi_pin: GPIO6 + interface: any + +spi_device: + id: spidev + data_rate: 2MHz + spi_id: spi_id_1 + mode: 3 + bit_order: lsb_first + +display: + - platform: ili9xxx + id: displ8 + model: ili9342 + cs_pin: GPIO5 + dc_pin: GPIO4 + reset_pin: + number: GPIO48 + allow_other_uses: true + +i2c: + scl: GPIO18 + sda: GPIO8 + +touchscreen: + - platform: tt21100 + display: displ8 + interrupt_pin: + number: GPIO3 + ignore_strapping_warning: true + allow_other_uses: false + reset_pin: + number: GPIO48 + allow_other_uses: true + +binary_sensor: + - platform: tt21100 + name: Home Button + index: 1 + +sensor: + - platform: debug + free: + name: "Heap Free" + block: + name: "Max Block Free" + loop_time: + name: "Loop Time" + psram: + name: "PSRAM Free" diff --git a/tests/test8.2.yaml b/tests/test8.2.yaml new file mode 100644 index 0000000000..69525b333b --- /dev/null +++ b/tests/test8.2.yaml @@ -0,0 +1,75 @@ +# Tests for ESP32-C3 boards - IDf +--- +wifi: + ssid: "ssid" + +network: + enable_ipv6: true + +esp32: + board: lolin_c3_mini + variant: ESP32C3 + framework: + type: esp-idf + +esphome: + name: esp32-c3-test + +logger: + +debug: + +psram: + +spi: + - id: spi_id_1 + clk_pin: + number: GPIO7 + allow_other_uses: false + mosi_pin: GPIO6 + interface: any + +spi_device: + id: spidev + data_rate: 2MHz + spi_id: spi_id_1 + mode: 3 + bit_order: lsb_first + +display: + - platform: ili9xxx + id: displ8 + model: ili9342 + cs_pin: GPIO5 + dc_pin: GPIO4 + reset_pin: + number: GPIO21 + +i2c: + scl: GPIO18 + sda: GPIO8 + +touchscreen: + - platform: tt21100 + display: displ8 + interrupt_pin: + number: GPIO3 + allow_other_uses: false + reset_pin: + number: GPIO20 + +binary_sensor: + - platform: tt21100 + name: Home Button + index: 1 + +sensor: + - platform: debug + free: + name: "Heap Free" + block: + name: "Max Block Free" + loop_time: + name: "Loop Time" + psram: + name: "PSRAM Free" From 836a3db163b130a4aecf3a632868dc3a90c609c8 Mon Sep 17 00:00:00 2001 From: mrtoy-me <118446898+mrtoy-me@users.noreply.github.com> Date: Fri, 15 Dec 2023 15:39:05 +1000 Subject: [PATCH 88/89] Update ENS160 TVOC device_class and AQI units to match required by HA (#5939) --- esphome/components/ens160/sensor.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/esphome/components/ens160/sensor.py b/esphome/components/ens160/sensor.py index 55f0ff7b6f..393b63bae1 100644 --- a/esphome/components/ens160/sensor.py +++ b/esphome/components/ens160/sensor.py @@ -9,7 +9,7 @@ from esphome.const import ( CONF_TVOC, DEVICE_CLASS_AQI, DEVICE_CLASS_CARBON_DIOXIDE, - DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, + DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS, ICON_CHEMICAL_WEAPON, ICON_MOLECULE_CO2, ICON_RADIATOR, @@ -45,11 +45,10 @@ CONFIG_SCHEMA = ( unit_of_measurement=UNIT_PARTS_PER_BILLION, icon=ICON_RADIATOR, accuracy_decimals=0, - device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, + device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS, state_class=STATE_CLASS_MEASUREMENT, ), cv.Required(CONF_AQI): sensor.sensor_schema( - unit_of_measurement=UNIT_INDEX, icon=ICON_CHEMICAL_WEAPON, accuracy_decimals=0, device_class=DEVICE_CLASS_AQI, From ea4e618f2af48b25c813ca9f54eddae530077c29 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 17 Dec 2023 16:27:08 +0000 Subject: [PATCH 89/89] Bump zeroconf from 0.128.4 to 0.130.0 (#5950) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index f330ecbf3e..20a5514e71 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,7 +11,7 @@ esptool==4.6.2 click==8.1.7 esphome-dashboard==20231107.0 aioesphomeapi==21.0.0 -zeroconf==0.128.4 +zeroconf==0.130.0 python-magic==0.4.27 # esp-idf requires this, but doesn't bundle it by default