diff --git a/CODEOWNERS b/CODEOWNERS index 5a9bc92cdf..e3f9d2b2d4 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -168,9 +168,11 @@ esphome/components/homeassistant/* @OttoWinter esphome/components/honeywell_hih_i2c/* @Benichou34 esphome/components/honeywellabp/* @RubyBailey esphome/components/honeywellabp2_i2c/* @jpfaff -esphome/components/host/* @esphome/core +esphome/components/host/* @clydebarrow @esphome/core +esphome/components/host/time/* @clydebarrow esphome/components/hrxl_maxsonar_wr/* @netmikey esphome/components/hte501/* @Stock-M +esphome/components/http_request/ota/* @oarcher esphome/components/htu31d/* @betterengineering esphome/components/hydreon_rgxx/* @functionpointer esphome/components/hyt271/* @Philippe12 diff --git a/docker/Dockerfile b/docker/Dockerfile index 3c661b7e1c..f6234235f7 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -100,6 +100,9 @@ RUN --mount=type=tmpfs,target=/root/.cargo if [ "$TARGETARCH$TARGETVARIANT" = "a --break-system-packages --no-cache-dir -r /requirements.txt -r /requirements_optional.txt \ && /platformio_install_deps.py /platformio.ini --libraries +# Avoid unsafe git error when container user and file config volume permissions don't match +RUN git config --system --add safe.directory '/config/*' + # ======================= docker-type image ======================= FROM base AS docker diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index ade94cb9f5..697436415b 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -11,6 +11,7 @@ from esphome.components.esp32.const import ( from esphome.const import ( CONF_DOMAIN, CONF_ID, + CONF_VALUE, CONF_MANUAL_IP, CONF_STATIC_IP, CONF_TYPE, @@ -26,6 +27,8 @@ from esphome.const import ( CONF_INTERRUPT_PIN, CONF_RESET_PIN, CONF_SPI, + CONF_PAGE_ID, + CONF_ADDRESS, ) from esphome.core import CORE, coroutine_with_priority from esphome.components.network import IPAddress @@ -36,11 +39,13 @@ DEPENDENCIES = ["esp32"] AUTO_LOAD = ["network"] ethernet_ns = cg.esphome_ns.namespace("ethernet") +PHYRegister = ethernet_ns.struct("PHYRegister") CONF_PHY_ADDR = "phy_addr" CONF_MDC_PIN = "mdc_pin" CONF_MDIO_PIN = "mdio_pin" CONF_CLK_MODE = "clk_mode" CONF_POWER_PIN = "power_pin" +CONF_PHY_REGISTERS = "phy_registers" CONF_CLOCK_SPEED = "clock_speed" @@ -117,6 +122,13 @@ BASE_SCHEMA = cv.Schema( } ).extend(cv.COMPONENT_SCHEMA) +PHY_REGISTER_SCHEMA = cv.Schema( + { + cv.Required(CONF_ADDRESS): cv.hex_int, + cv.Required(CONF_VALUE): cv.hex_int, + cv.Optional(CONF_PAGE_ID): cv.hex_int, + } +) RMII_SCHEMA = BASE_SCHEMA.extend( cv.Schema( { @@ -127,6 +139,7 @@ RMII_SCHEMA = BASE_SCHEMA.extend( ), cv.Optional(CONF_PHY_ADDR, default=0): cv.int_range(min=0, max=31), cv.Optional(CONF_POWER_PIN): pins.internal_gpio_output_pin_number, + cv.Optional(CONF_PHY_REGISTERS): cv.ensure_list(PHY_REGISTER_SCHEMA), } ) ) @@ -198,6 +211,15 @@ def manual_ip(config): ) +def phy_register(address: int, value: int, page: int): + return cg.StructInitializer( + PHYRegister, + ("address", address), + ("value", value), + ("page", page), + ) + + @coroutine_with_priority(60.0) async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) @@ -225,6 +247,13 @@ async def to_code(config): cg.add(var.set_clk_mode(*CLK_MODES[config[CONF_CLK_MODE]])) if CONF_POWER_PIN in config: cg.add(var.set_power_pin(config[CONF_POWER_PIN])) + for register_value in config.get(CONF_PHY_REGISTERS, []): + reg = phy_register( + register_value.get(CONF_ADDRESS), + register_value.get(CONF_VALUE), + register_value.get(CONF_PAGE_ID), + ) + cg.add(var.add_phy_register(reg)) cg.add(var.set_type(ETHERNET_TYPES[config[CONF_TYPE]])) cg.add(var.set_use_address(config[CONF_USE_ADDRESS])) diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index 7672dc2dcb..75bdd29be7 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -28,6 +28,13 @@ EthernetComponent *global_eth_component; // NOLINT(cppcoreguidelines-avoid-non- return; \ } +#define ESPHL_ERROR_CHECK_RET(err, message, ret) \ + if ((err) != ESP_OK) { \ + ESP_LOGE(TAG, message ": (%d) %s", err, esp_err_to_name(err)); \ + this->mark_failed(); \ + return ret; \ + } + EthernetComponent::EthernetComponent() { global_eth_component = this; } void EthernetComponent::setup() { @@ -188,9 +195,9 @@ void EthernetComponent::setup() { // KSZ8081RNA default is incorrect. It expects a 25MHz clock instead of the 50MHz we provide. this->ksz8081_set_clock_reference_(mac); } - if (this->type_ == ETHERNET_TYPE_RTL8201 && this->clk_mode_ == EMAC_CLK_EXT_IN) { - // Change in default behavior of RTL8201FI may require register setting to enable external clock - this->rtl8201_set_rmii_mode_(mac); + + for (const auto &phy_register : this->phy_registers_) { + this->write_phy_register_(mac, phy_register); } #endif @@ -498,22 +505,9 @@ void EthernetComponent::dump_connect_params_() { } #endif /* USE_NETWORK_IPV6 */ - esp_err_t err; - - uint8_t mac[6]; - err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_G_MAC_ADDR, &mac); - ESPHL_ERROR_CHECK(err, "ETH_CMD_G_MAC error"); - ESP_LOGCONFIG(TAG, " MAC Address: %02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); - - eth_duplex_t duplex_mode; - err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_G_DUPLEX_MODE, &duplex_mode); - ESPHL_ERROR_CHECK(err, "ETH_CMD_G_DUPLEX_MODE error"); - ESP_LOGCONFIG(TAG, " Is Full Duplex: %s", YESNO(duplex_mode == ETH_DUPLEX_FULL)); - - eth_speed_t speed; - err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_G_SPEED, &speed); - ESPHL_ERROR_CHECK(err, "ETH_CMD_G_SPEED error"); - ESP_LOGCONFIG(TAG, " Link Speed: %u", speed == ETH_SPEED_100M ? 100 : 10); + ESP_LOGCONFIG(TAG, " MAC Address: %s", this->get_eth_mac_address_pretty().c_str()); + ESP_LOGCONFIG(TAG, " Is Full Duplex: %s", YESNO(this->get_duplex_mode() == ETH_DUPLEX_FULL)); + ESP_LOGCONFIG(TAG, " Link Speed: %u", this->get_link_speed() == ETH_SPEED_100M ? 100 : 10); } #ifdef USE_ETHERNET_SPI @@ -533,6 +527,7 @@ void EthernetComponent::set_clk_mode(emac_rmii_clock_mode_t clk_mode, emac_rmii_ this->clk_mode_ = clk_mode; this->clk_gpio_ = clk_gpio; } +void EthernetComponent::add_phy_register(PHYRegister register_value) { this->phy_registers_.push_back(register_value); } #endif void EthernetComponent::set_type(EthernetType type) { this->type_ = type; } void EthernetComponent::set_manual_ip(const ManualIP &manual_ip) { this->manual_ip_ = manual_ip; } @@ -546,6 +541,34 @@ std::string EthernetComponent::get_use_address() const { void EthernetComponent::set_use_address(const std::string &use_address) { this->use_address_ = use_address; } +void EthernetComponent::get_eth_mac_address_raw(uint8_t *mac) { + esp_err_t err; + err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_G_MAC_ADDR, mac); + ESPHL_ERROR_CHECK(err, "ETH_CMD_G_MAC error"); +} + +std::string EthernetComponent::get_eth_mac_address_pretty() { + uint8_t mac[6]; + get_mac_address_raw(mac); + return str_snprintf("%02X:%02X:%02X:%02X:%02X:%02X", 17, mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); +} + +eth_duplex_t EthernetComponent::get_duplex_mode() { + esp_err_t err; + eth_duplex_t duplex_mode; + err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_G_DUPLEX_MODE, &duplex_mode); + ESPHL_ERROR_CHECK_RET(err, "ETH_CMD_G_DUPLEX_MODE error", ETH_DUPLEX_HALF); + return duplex_mode; +} + +eth_speed_t EthernetComponent::get_link_speed() { + esp_err_t err; + eth_speed_t speed; + err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_G_SPEED, &speed); + ESPHL_ERROR_CHECK_RET(err, "ETH_CMD_G_SPEED error", ETH_SPEED_10M); + return speed; +} + bool EthernetComponent::powerdown() { ESP_LOGI(TAG, "Powering down ethernet PHY"); if (this->phy_ == nullptr) { @@ -591,44 +614,27 @@ void EthernetComponent::ksz8081_set_clock_reference_(esp_eth_mac_t *mac) { ESP_LOGVV(TAG, "KSZ8081 PHY Control 2: %s", format_hex_pretty((u_int8_t *) &phy_control_2, 2).c_str()); } } -constexpr uint8_t RTL8201_RMSR_REG_ADDR = 0x10; -void EthernetComponent::rtl8201_set_rmii_mode_(esp_eth_mac_t *mac) { + +void EthernetComponent::write_phy_register_(esp_eth_mac_t *mac, PHYRegister register_data) { esp_err_t err; - uint32_t phy_rmii_mode; - err = mac->write_phy_reg(mac, this->phy_addr_, 0x1f, 0x07); - ESPHL_ERROR_CHECK(err, "Setting Page 7 failed"); + constexpr uint8_t eth_phy_psr_reg_addr = 0x1F; - /* - * RTL8201 RMII Mode Setting Register (RMSR) - * Page 7 Register 16 - * - * bit 0 Reserved 0 - * bit 1 Rg_rmii_rxdsel 1 (default) - * bit 2 Rg_rmii_rxdv_sel: 0 (default) - * bit 3 RMII Mode: 1 (RMII Mode) - * bit 4~7 Rg_rmii_rx_offset: 1111 (default) - * bit 8~11 Rg_rmii_tx_offset: 1111 (default) - * bit 12 Rg_rmii_clkdir: 1 (Input) - * bit 13~15 Reserved 000 - * - * Binary: 0001 1111 1111 1010 - * Hex: 0x1FFA - * - */ + if (this->type_ == ETHERNET_TYPE_RTL8201 && register_data.page) { + ESP_LOGD(TAG, "Select PHY Register Page: 0x%02" PRIX32, register_data.page); + err = mac->write_phy_reg(mac, this->phy_addr_, eth_phy_psr_reg_addr, register_data.page); + ESPHL_ERROR_CHECK(err, "Select PHY Register page failed"); + } - err = mac->read_phy_reg(mac, this->phy_addr_, RTL8201_RMSR_REG_ADDR, &(phy_rmii_mode)); - ESPHL_ERROR_CHECK(err, "Read PHY RMSR Register failed"); - ESP_LOGV(TAG, "Hardware default RTL8201 RMII Mode Register is: 0x%04" PRIX32, phy_rmii_mode); + ESP_LOGD(TAG, "Writing to PHY Register Address: 0x%02" PRIX32, register_data.address); + ESP_LOGD(TAG, "Writing to PHY Register Value: 0x%04" PRIX32, register_data.value); + err = mac->write_phy_reg(mac, this->phy_addr_, register_data.address, register_data.value); + ESPHL_ERROR_CHECK(err, "Writing PHY Register failed"); - err = mac->write_phy_reg(mac, this->phy_addr_, RTL8201_RMSR_REG_ADDR, 0x1FFA); - ESPHL_ERROR_CHECK(err, "Setting Register 16 RMII Mode Setting failed"); - - err = mac->read_phy_reg(mac, this->phy_addr_, RTL8201_RMSR_REG_ADDR, &(phy_rmii_mode)); - ESPHL_ERROR_CHECK(err, "Read PHY RMSR Register failed"); - ESP_LOGV(TAG, "Setting RTL8201 RMII Mode Register to: 0x%04" PRIX32, phy_rmii_mode); - - err = mac->write_phy_reg(mac, this->phy_addr_, 0x1f, 0x0); - ESPHL_ERROR_CHECK(err, "Setting Page 0 failed"); + if (this->type_ == ETHERNET_TYPE_RTL8201 && register_data.page) { + ESP_LOGD(TAG, "Select PHY Register Page 0x%02" PRIX32, 0x0); + err = mac->write_phy_reg(mac, this->phy_addr_, eth_phy_psr_reg_addr, 0x0); + ESPHL_ERROR_CHECK(err, "Select PHY Register Page 0 failed"); + } } #endif diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h index 3705e3e06a..f0fe6cab87 100644 --- a/esphome/components/ethernet/ethernet_component.h +++ b/esphome/components/ethernet/ethernet_component.h @@ -35,6 +35,12 @@ struct ManualIP { network::IPAddress dns2; ///< The second DNS server. 0.0.0.0 for default. }; +struct PHYRegister { + uint32_t address; + uint32_t value; + uint32_t page; +}; + enum class EthernetComponentState { STOPPED, CONNECTING, @@ -66,6 +72,7 @@ class EthernetComponent : public Component { void set_mdc_pin(uint8_t mdc_pin); void set_mdio_pin(uint8_t mdio_pin); void set_clk_mode(emac_rmii_clock_mode_t clk_mode, emac_rmii_clock_gpio_t clk_gpio); + void add_phy_register(PHYRegister register_value); #endif void set_type(EthernetType type); void set_manual_ip(const ManualIP &manual_ip); @@ -74,6 +81,10 @@ class EthernetComponent : public Component { network::IPAddress get_dns_address(uint8_t num); std::string get_use_address() const; void set_use_address(const std::string &use_address); + void get_eth_mac_address_raw(uint8_t *mac); + std::string get_eth_mac_address_pretty(); + eth_duplex_t get_duplex_mode(); + eth_speed_t get_link_speed(); bool powerdown(); protected: @@ -87,8 +98,8 @@ class EthernetComponent : public Component { void dump_connect_params_(); /// @brief Set `RMII Reference Clock Select` bit for KSZ8081. void ksz8081_set_clock_reference_(esp_eth_mac_t *mac); - /// @brief Set `RMII Mode Setting Register` for RTL8201. - void rtl8201_set_rmii_mode_(esp_eth_mac_t *mac); + /// @brief Set arbitratry PHY registers from config. + void write_phy_register_(esp_eth_mac_t *mac, PHYRegister register_data); std::string use_address_; #ifdef USE_ETHERNET_SPI @@ -107,6 +118,7 @@ class EthernetComponent : public Component { uint8_t mdio_pin_{18}; emac_rmii_clock_mode_t clk_mode_{EMAC_CLK_EXT_IN}; emac_rmii_clock_gpio_t clk_gpio_{EMAC_CLK_IN_GPIO}; + std::vector phy_registers_{}; #endif EthernetType type_{ETHERNET_TYPE_UNKNOWN}; optional manual_ip_{}; diff --git a/esphome/components/ethernet_info/ethernet_info_text_sensor.cpp b/esphome/components/ethernet_info/ethernet_info_text_sensor.cpp index c8b2b5885b..329fb9113a 100644 --- a/esphome/components/ethernet_info/ethernet_info_text_sensor.cpp +++ b/esphome/components/ethernet_info/ethernet_info_text_sensor.cpp @@ -10,6 +10,7 @@ static const char *const TAG = "ethernet_info"; void IPAddressEthernetInfo::dump_config() { LOG_TEXT_SENSOR("", "EthernetInfo IPAddress", this); } void DNSAddressEthernetInfo::dump_config() { LOG_TEXT_SENSOR("", "EthernetInfo DNS Address", this); } +void MACAddressEthernetInfo::dump_config() { LOG_TEXT_SENSOR("", "EthernetInfo MAC Address", this); } } // namespace ethernet_info } // namespace esphome diff --git a/esphome/components/ethernet_info/ethernet_info_text_sensor.h b/esphome/components/ethernet_info/ethernet_info_text_sensor.h index 82a7dcf56e..94eed886e5 100644 --- a/esphome/components/ethernet_info/ethernet_info_text_sensor.h +++ b/esphome/components/ethernet_info/ethernet_info_text_sensor.h @@ -59,6 +59,13 @@ class DNSAddressEthernetInfo : public PollingComponent, public text_sensor::Text std::string last_results_; }; +class MACAddressEthernetInfo : public Component, public text_sensor::TextSensor { + public: + void setup() override { this->publish_state(ethernet::global_eth_component->get_eth_mac_address_pretty()); } + std::string unique_id() override { return get_mac_address() + "-ethernetinfo-mac"; } + void dump_config() override; +}; + } // namespace ethernet_info } // namespace esphome diff --git a/esphome/components/ethernet_info/text_sensor.py b/esphome/components/ethernet_info/text_sensor.py index 292673c182..a545475870 100644 --- a/esphome/components/ethernet_info/text_sensor.py +++ b/esphome/components/ethernet_info/text_sensor.py @@ -4,6 +4,7 @@ from esphome.components import text_sensor from esphome.const import ( CONF_IP_ADDRESS, CONF_DNS_ADDRESS, + CONF_MAC_ADDRESS, ENTITY_CATEGORY_DIAGNOSTIC, ) @@ -19,6 +20,10 @@ DNSAddressEthernetInfo = ethernet_info_ns.class_( "DNSAddressEthernetInfo", text_sensor.TextSensor, cg.PollingComponent ) +MACAddressEthernetInfo = ethernet_info_ns.class_( + "MACAddressEthernetInfo", text_sensor.TextSensor, cg.PollingComponent +) + CONFIG_SCHEMA = cv.Schema( { cv.Optional(CONF_IP_ADDRESS): text_sensor.text_sensor_schema( @@ -36,6 +41,9 @@ CONFIG_SCHEMA = cv.Schema( cv.Optional(CONF_DNS_ADDRESS): text_sensor.text_sensor_schema( DNSAddressEthernetInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC ).extend(cv.polling_component_schema("1s")), + cv.Optional(CONF_MAC_ADDRESS): text_sensor.text_sensor_schema( + MACAddressEthernetInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC + ), } ) @@ -51,3 +59,6 @@ async def to_code(config): if conf := config.get(CONF_DNS_ADDRESS): dns_info = await text_sensor.new_text_sensor(config[CONF_DNS_ADDRESS]) await cg.register_component(dns_info, config[CONF_DNS_ADDRESS]) + if conf := config.get(CONF_MAC_ADDRESS): + mac_info = await text_sensor.new_text_sensor(config[CONF_MAC_ADDRESS]) + await cg.register_component(mac_info, config[CONF_MAC_ADDRESS]) diff --git a/esphome/components/ft5x06/touchscreen/__init__.py b/esphome/components/ft5x06/touchscreen/__init__.py index adeeac0d1a..4ceb50c709 100644 --- a/esphome/components/ft5x06/touchscreen/__init__.py +++ b/esphome/components/ft5x06/touchscreen/__init__.py @@ -1,8 +1,9 @@ +from esphome import pins import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import i2c, touchscreen -from esphome.const import CONF_ID +from esphome.const import CONF_ID, CONF_INTERRUPT_PIN from .. import ft5x06_ns FT5x06ButtonListener = ft5x06_ns.class_("FT5x06ButtonListener") @@ -16,6 +17,7 @@ FT5x06Touchscreen = ft5x06_ns.class_( CONFIG_SCHEMA = touchscreen.TOUCHSCREEN_SCHEMA.extend( { cv.GenerateID(): cv.declare_id(FT5x06Touchscreen), + cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema, } ).extend(i2c.i2c_device_schema(0x48)) @@ -24,3 +26,7 @@ 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) + + if interrupt_pin := config.get(CONF_INTERRUPT_PIN): + pin = await cg.gpio_pin_expression(interrupt_pin) + cg.add(var.set_interrupt_pin(pin)) diff --git a/esphome/components/ft5x06/touchscreen/ft5x06_touchscreen.cpp b/esphome/components/ft5x06/touchscreen/ft5x06_touchscreen.cpp new file mode 100644 index 0000000000..bd603fdc10 --- /dev/null +++ b/esphome/components/ft5x06/touchscreen/ft5x06_touchscreen.cpp @@ -0,0 +1,102 @@ +#include "ft5x06_touchscreen.h" + +#include "esphome/core/log.h" + +namespace esphome { +namespace ft5x06 { + +static const char *const TAG = "ft5x06.touchscreen"; + +void FT5x06Touchscreen::setup() { + ESP_LOGCONFIG(TAG, "Setting up FT5x06 Touchscreen..."); + if (this->interrupt_pin_ != nullptr) { + this->interrupt_pin_->setup(); + 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); + } + + // wait 200ms after reset. + this->set_timeout(200, [this] { this->continue_setup_(); }); +} + +void FT5x06Touchscreen::continue_setup_() { + 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]; + ESP_LOGD(TAG, "Read vendor ID 0x%X", data[0]); + break; + + default: + ESP_LOGE(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. + if (this->display_ != nullptr) { + if (this->x_raw_max_ == this->x_raw_min_) { + this->x_raw_max_ = this->display_->get_native_width(); + } + if (this->y_raw_max_ == this->y_raw_min_) { + this->y_raw_max_ = this->display_->get_native_height(); + } + } + ESP_LOGCONFIG(TAG, "FT5x06 Touchscreen setup complete"); +} + +void FT5x06Touchscreen::update_touches() { + uint8_t touch_cnt; + uint8_t data[MAX_TOUCHES][6]; + + if (!this->read_byte(FT5X06_TD_STATUS, &touch_cnt) || touch_cnt > MAX_TOUCHES) { + ESP_LOGW(TAG, "Failed to read status"); + return; + } + if (touch_cnt == 0) + return; + + if (!this->read_bytes(FT5X06_TOUCH_DATA, (uint8_t *) data, touch_cnt * 6)) { + ESP_LOGW(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]); + + ESP_LOGD(TAG, "Read %X status, id: %d, pos %d/%d", status, id, x, y); + if (status == 0 || status == 2) { + this->add_raw_touch_position_(id, x, y); + } + } +} + +void FT5x06Touchscreen::dump_config() { + ESP_LOGCONFIG(TAG, "FT5x06 Touchscreen:"); + ESP_LOGCONFIG(TAG, " Address: 0x%02X", this->address_); + ESP_LOGCONFIG(TAG, " Vendor ID: 0x%X", (int) this->vendor_id_); +} + +bool FT5x06Touchscreen::err_check_(i2c::ErrorCode err, const char *msg) { + if (err != i2c::ERROR_OK) { + this->mark_failed(); + ESP_LOGE(TAG, "%s failed - err 0x%X", msg, err); + return false; + } + return true; +} +bool FT5x06Touchscreen::set_mode_(FTMode mode) { + return this->err_check_(this->write_register(FT5X06_MODE_REG, (uint8_t *) &mode, 1), "Set mode"); +} + +} // namespace ft5x06 +} // namespace esphome diff --git a/esphome/components/ft5x06/touchscreen/ft5x06_touchscreen.h b/esphome/components/ft5x06/touchscreen/ft5x06_touchscreen.h index 7ddd2e44d7..23e5a0c49f 100644 --- a/esphome/components/ft5x06/touchscreen/ft5x06_touchscreen.h +++ b/esphome/components/ft5x06/touchscreen/ft5x06_touchscreen.h @@ -3,14 +3,12 @@ #include "esphome/components/i2c/i2c.h" #include "esphome/components/touchscreen/touchscreen.h" #include "esphome/core/component.h" +#include "esphome/core/gpio.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, @@ -39,91 +37,19 @@ 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 setup() override; + void dump_config() override; + void update_touches() override; - 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. - if (this->display_ != nullptr) { - if (this->x_raw_max_ == this->x_raw_min_) { - this->x_raw_max_ = this->display_->get_native_width(); - } - if (this->y_raw_max_ == this->y_raw_min_) { - this->y_raw_max_ = this->display_->get_native_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->add_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_); - } + void set_interrupt_pin(InternalGPIOPin *interrupt_pin) { this->interrupt_pin_ = interrupt_pin; } 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"); - } + void continue_setup_(); + bool err_check_(i2c::ErrorCode err, const char *msg); + bool set_mode_(FTMode mode); VendorId vendor_id_{FT5X06_ID_UNKNOWN}; + + InternalGPIOPin *interrupt_pin_{nullptr}; }; } // namespace ft5x06 diff --git a/esphome/components/host/__init__.py b/esphome/components/host/__init__.py index 3bd3b6b172..39e418c9ea 100644 --- a/esphome/components/host/__init__.py +++ b/esphome/components/host/__init__.py @@ -16,7 +16,7 @@ from .const import KEY_HOST # force import gpio to register pin schema from .gpio import host_pin_to_code # noqa -CODEOWNERS = ["@esphome/core"] +CODEOWNERS = ["@esphome/core", "@clydebarrow"] AUTO_LOAD = ["network"] diff --git a/esphome/components/host/time/__init__.py b/esphome/components/host/time/__init__.py new file mode 100644 index 0000000000..76a88d98a1 --- /dev/null +++ b/esphome/components/host/time/__init__.py @@ -0,0 +1,20 @@ +import esphome.codegen as cg +from esphome.const import CONF_ID +import esphome.config_validation as cv +from esphome.components import time as time_ + +CODEOWNERS = ["@clydebarrow"] + +time_ns = cg.esphome_ns.namespace("host") +HostTime = time_ns.class_("HostTime", time_.RealTimeClock) +CONFIG_SCHEMA = time_.TIME_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(HostTime), + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await time_.register_time(var, config) diff --git a/esphome/components/host/time/host_time.h b/esphome/components/host/time/host_time.h new file mode 100644 index 0000000000..4f1473b809 --- /dev/null +++ b/esphome/components/host/time/host_time.h @@ -0,0 +1,15 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/time/real_time_clock.h" + +namespace esphome { +namespace host { + +class HostTime : public time::RealTimeClock { + public: + void update() override {} +}; + +} // namespace host +} // namespace esphome diff --git a/esphome/components/http_request/ota/__init__.py b/esphome/components/http_request/ota/__init__.py new file mode 100644 index 0000000000..6a56fac83a --- /dev/null +++ b/esphome/components/http_request/ota/__init__.py @@ -0,0 +1,189 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import automation +from esphome.const import ( + CONF_ESP8266_DISABLE_SSL_SUPPORT, + CONF_ID, + CONF_PASSWORD, + CONF_TIMEOUT, + CONF_URL, + CONF_USERNAME, +) +from esphome.components import esp32 +from esphome.components.ota import BASE_OTA_SCHEMA, ota_to_code, OTAComponent +from esphome.core import CORE, coroutine_with_priority +from .. import http_request_ns + +CODEOWNERS = ["@oarcher"] + +AUTO_LOAD = ["md5"] +DEPENDENCIES = ["network"] + +CONF_MD5 = "md5" +CONF_MD5_URL = "md5_url" +CONF_VERIFY_SSL = "verify_ssl" +CONF_WATCHDOG_TIMEOUT = "watchdog_timeout" + +OtaHttpRequestComponent = http_request_ns.class_( + "OtaHttpRequestComponent", OTAComponent +) +OtaHttpRequestComponentArduino = http_request_ns.class_( + "OtaHttpRequestComponentArduino", OtaHttpRequestComponent +) +OtaHttpRequestComponentIDF = http_request_ns.class_( + "OtaHttpRequestComponentIDF", OtaHttpRequestComponent +) +OtaHttpRequestComponentFlashAction = http_request_ns.class_( + "OtaHttpRequestComponentFlashAction", automation.Action +) + + +def validate_ssl_verification(config): + error_message = "" + + if CORE.is_esp32: + if not CORE.using_esp_idf and config[CONF_VERIFY_SSL]: + error_message = "ESPHome supports certificate verification only via ESP-IDF" + + if CORE.is_rp2040 and config[CONF_VERIFY_SSL]: + error_message = "ESPHome does not support certificate verification in Arduino" + + if ( + CORE.is_esp8266 + and not config[CONF_ESP8266_DISABLE_SSL_SUPPORT] + and config[CONF_VERIFY_SSL] + ): + error_message = "ESPHome does not support certificate verification in Arduino" + + if len(error_message) > 0: + raise cv.Invalid( + f"{error_message}. Set '{CONF_VERIFY_SSL}: false' to skip certificate validation and allow less secure HTTPS connections." + ) + + return config + + +def _declare_request_class(value): + if CORE.using_esp_idf: + return cv.declare_id(OtaHttpRequestComponentIDF)(value) + + if CORE.is_esp8266 or CORE.is_esp32 or CORE.is_rp2040: + return cv.declare_id(OtaHttpRequestComponentArduino)(value) + return NotImplementedError + + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): _declare_request_class, + cv.SplitDefault(CONF_ESP8266_DISABLE_SSL_SUPPORT, esp8266=False): cv.All( + cv.only_on_esp8266, cv.boolean + ), + cv.Optional(CONF_VERIFY_SSL, default=True): cv.boolean, + cv.Optional( + CONF_TIMEOUT, default="5min" + ): cv.positive_time_period_milliseconds, + cv.Optional(CONF_WATCHDOG_TIMEOUT): cv.All( + cv.Any(cv.only_on_esp32, cv.only_on_rp2040), + cv.positive_not_null_time_period, + cv.positive_time_period_milliseconds, + ), + } + ) + .extend(BASE_OTA_SCHEMA) + .extend(cv.COMPONENT_SCHEMA), + cv.require_framework_version( + esp8266_arduino=cv.Version(2, 5, 1), + esp32_arduino=cv.Version(0, 0, 0), + esp_idf=cv.Version(0, 0, 0), + rp2040_arduino=cv.Version(0, 0, 0), + ), + validate_ssl_verification, +) + + +@coroutine_with_priority(52.0) +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await ota_to_code(var, config) + + cg.add(var.set_timeout(config[CONF_TIMEOUT])) + + if timeout_ms := config.get(CONF_WATCHDOG_TIMEOUT): + cg.add_define( + "USE_HTTP_REQUEST_OTA_WATCHDOG_TIMEOUT", + timeout_ms, + ) + + if CORE.is_esp8266 and not config[CONF_ESP8266_DISABLE_SSL_SUPPORT]: + cg.add_define("USE_HTTP_REQUEST_ESP8266_HTTPS") + + if CORE.is_esp32: + if CORE.using_esp_idf: + esp32.add_idf_sdkconfig_option( + "CONFIG_MBEDTLS_CERTIFICATE_BUNDLE", + config.get(CONF_VERIFY_SSL), + ) + esp32.add_idf_sdkconfig_option( + "CONFIG_ESP_TLS_INSECURE", + not config.get(CONF_VERIFY_SSL), + ) + esp32.add_idf_sdkconfig_option( + "CONFIG_ESP_TLS_SKIP_SERVER_CERT_VERIFY", + not config.get(CONF_VERIFY_SSL), + ) + else: + cg.add_library("WiFiClientSecure", None) + cg.add_library("HTTPClient", None) + if CORE.is_esp8266: + cg.add_library("ESP8266HTTPClient", None) + if CORE.is_rp2040 and CORE.using_arduino: + cg.add_library("HTTPClient", None) + + await cg.register_component(var, config) + + +OTA_HTTP_REQUEST_FLASH_ACTION_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.use_id(OtaHttpRequestComponent), + cv.Optional(CONF_MD5_URL): cv.templatable(cv.url), + cv.Optional(CONF_MD5): cv.templatable(cv.string), + cv.Optional(CONF_PASSWORD): cv.templatable(cv.string), + cv.Optional(CONF_USERNAME): cv.templatable(cv.string), + cv.Required(CONF_URL): cv.templatable(cv.url), + } + ), + cv.has_exactly_one_key(CONF_MD5, CONF_MD5_URL), +) + + +@automation.register_action( + "ota_http_request.flash", + OtaHttpRequestComponentFlashAction, + OTA_HTTP_REQUEST_FLASH_ACTION_SCHEMA, +) +async def ota_http_request_action_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + + if md5_url := config.get(CONF_MD5_URL): + template_ = await cg.templatable(md5_url, args, cg.std_string) + cg.add(var.set_md5_url(template_)) + + if md5_str := config.get(CONF_MD5): + template_ = await cg.templatable(md5_str, args, cg.std_string) + cg.add(var.set_md5(template_)) + + if password_str := config.get(CONF_PASSWORD): + template_ = await cg.templatable(password_str, args, cg.std_string) + cg.add(var.set_password(template_)) + + if username_str := config.get(CONF_USERNAME): + template_ = await cg.templatable(username_str, args, cg.std_string) + cg.add(var.set_username(template_)) + + template_ = await cg.templatable(config[CONF_URL], args, cg.std_string) + cg.add(var.set_url(template_)) + + return var diff --git a/esphome/components/http_request/ota/automation.h b/esphome/components/http_request/ota/automation.h new file mode 100644 index 0000000000..d4c21f1c72 --- /dev/null +++ b/esphome/components/http_request/ota/automation.h @@ -0,0 +1,42 @@ +#pragma once +#include "ota_http_request.h" + +#include "esphome/core/automation.h" + +namespace esphome { +namespace http_request { + +template class OtaHttpRequestComponentFlashAction : public Action { + public: + OtaHttpRequestComponentFlashAction(OtaHttpRequestComponent *parent) : parent_(parent) {} + TEMPLATABLE_VALUE(std::string, md5_url) + TEMPLATABLE_VALUE(std::string, md5) + TEMPLATABLE_VALUE(std::string, password) + TEMPLATABLE_VALUE(std::string, url) + TEMPLATABLE_VALUE(std::string, username) + + void play(Ts... x) override { + if (this->md5_url_.has_value()) { + this->parent_->set_md5_url(this->md5_url_.value(x...)); + } + if (this->md5_.has_value()) { + this->parent_->set_md5(this->md5_.value(x...)); + } + if (this->password_.has_value()) { + this->parent_->set_password(this->password_.value(x...)); + } + if (this->username_.has_value()) { + this->parent_->set_username(this->username_.value(x...)); + } + this->parent_->set_url(this->url_.value(x...)); + + this->parent_->flash(); + // Normally never reached due to reboot + } + + protected: + OtaHttpRequestComponent *parent_; +}; + +} // namespace http_request +} // namespace esphome diff --git a/esphome/components/http_request/ota/ota_http_request.cpp b/esphome/components/http_request/ota/ota_http_request.cpp new file mode 100644 index 0000000000..cf0816c858 --- /dev/null +++ b/esphome/components/http_request/ota/ota_http_request.cpp @@ -0,0 +1,293 @@ +#include "ota_http_request.h" +#include "watchdog.h" + +#include "esphome/core/application.h" +#include "esphome/core/defines.h" +#include "esphome/core/log.h" + +#include "esphome/components/md5/md5.h" +#include "esphome/components/ota/ota_backend_arduino_esp32.h" +#include "esphome/components/ota/ota_backend_arduino_esp8266.h" +#include "esphome/components/ota/ota_backend_arduino_rp2040.h" +#include "esphome/components/ota/ota_backend_esp_idf.h" +#include "esphome/components/ota/ota_backend.h" + +namespace esphome { +namespace http_request { + +void OtaHttpRequestComponent::setup() { +#ifdef USE_OTA_STATE_CALLBACK + ota::register_ota_platform(this); +#endif +} + +void OtaHttpRequestComponent::dump_config() { + ESP_LOGCONFIG(TAG, "Over-The-Air updates via HTTP request:"); + ESP_LOGCONFIG(TAG, " Timeout: %llus", this->timeout_ / 1000); +#ifdef USE_ESP8266 +#ifdef USE_HTTP_REQUEST_ESP8266_HTTPS + ESP_LOGCONFIG(TAG, " ESP8266 SSL support: No"); +#else + ESP_LOGCONFIG(TAG, " ESP8266 SSL support: Yes"); +#endif +#endif +#ifdef CONFIG_MBEDTLS_CERTIFICATE_BUNDLE + ESP_LOGCONFIG(TAG, " TLS server verification: Yes"); +#else + ESP_LOGCONFIG(TAG, " TLS server verification: No"); +#endif +#ifdef USE_HTTP_REQUEST_OTA_WATCHDOG_TIMEOUT + ESP_LOGCONFIG(TAG, " Watchdog timeout: %ds", USE_HTTP_REQUEST_OTA_WATCHDOG_TIMEOUT / 1000); +#endif +}; + +void OtaHttpRequestComponent::set_md5_url(const std::string &url) { + if (!this->validate_url_(url)) { + this->md5_url_.clear(); // URL was not valid; prevent flashing until it is + return; + } + this->md5_url_ = url; + this->md5_expected_.clear(); // to be retrieved later +} + +void OtaHttpRequestComponent::set_url(const std::string &url) { + if (!this->validate_url_(url)) { + this->url_.clear(); // URL was not valid; prevent flashing until it is + return; + } + this->url_ = url; +} + +bool OtaHttpRequestComponent::check_status() { + // status can be -1, or HTTP status code + if (this->status_ < 100) { + ESP_LOGE(TAG, "HTTP server did not respond (error %d)", this->status_); + return false; + } + if (this->status_ >= 310) { + ESP_LOGE(TAG, "HTTP error %d", this->status_); + return false; + } + ESP_LOGV(TAG, "HTTP status %d", this->status_); + return true; +} + +void OtaHttpRequestComponent::flash() { + if (this->url_.empty()) { + ESP_LOGE(TAG, "URL not set; cannot start update"); + return; + } + + ESP_LOGI(TAG, "Starting update..."); +#ifdef USE_OTA_STATE_CALLBACK + this->state_callback_.call(ota::OTA_STARTED, 0.0f, 0); +#endif + + auto ota_status = this->do_ota_(); + + switch (ota_status) { + case ota::OTA_RESPONSE_OK: +#ifdef USE_OTA_STATE_CALLBACK + this->state_callback_.call(ota::OTA_COMPLETED, 100.0f, ota_status); +#endif + delay(10); + App.safe_reboot(); + break; + + default: +#ifdef USE_OTA_STATE_CALLBACK + this->state_callback_.call(ota::OTA_ERROR, 0.0f, ota_status); +#endif + this->md5_computed_.clear(); // will be reset at next attempt + this->md5_expected_.clear(); // will be reset at next attempt + break; + } +} + +void OtaHttpRequestComponent::cleanup_(std::unique_ptr backend) { + if (this->update_started_) { + ESP_LOGV(TAG, "Aborting OTA backend"); + backend->abort(); + } + ESP_LOGV(TAG, "Aborting HTTP connection"); + this->http_end(); +}; + +uint8_t OtaHttpRequestComponent::do_ota_() { + uint8_t buf[this->http_recv_buffer_ + 1]; + uint32_t last_progress = 0; + uint32_t update_start_time = millis(); + md5::MD5Digest md5_receive; + std::unique_ptr md5_receive_str(new char[33]); + + if (this->md5_expected_.empty() && !this->http_get_md5_()) { + return OTA_MD5_INVALID; + } + + ESP_LOGD(TAG, "MD5 expected: %s", this->md5_expected_.c_str()); + + auto url_with_auth = this->get_url_with_auth_(this->url_); + if (url_with_auth.empty()) { + return OTA_BAD_URL; + } + ESP_LOGVV(TAG, "url_with_auth: %s", url_with_auth.c_str()); + ESP_LOGI(TAG, "Connecting to: %s", this->url_.c_str()); + this->http_init(url_with_auth); + if (!this->check_status()) { + this->http_end(); + return OTA_CONNECTION_ERROR; + } + + // we will compute MD5 on the fly for verification -- Arduino OTA seems to ignore it + md5_receive.init(); + ESP_LOGV(TAG, "MD5Digest initialized"); + + ESP_LOGV(TAG, "OTA backend begin"); + auto backend = ota::make_ota_backend(); + auto error_code = backend->begin(this->body_length_); + if (error_code != ota::OTA_RESPONSE_OK) { + ESP_LOGW(TAG, "backend->begin error: %d", error_code); + this->cleanup_(std::move(backend)); + return error_code; + } + + this->bytes_read_ = 0; + while (this->bytes_read_ < this->body_length_) { + // read a maximum of chunk_size bytes into buf. (real read size returned) + int bufsize = this->http_read(buf, this->http_recv_buffer_); + ESP_LOGVV(TAG, "bytes_read_ = %u, body_length_ = %u, bufsize = %i", this->bytes_read_, this->body_length_, bufsize); + + // feed watchdog and give other tasks a chance to run + App.feed_wdt(); + yield(); + + if (bufsize < 0) { + ESP_LOGE(TAG, "Stream closed"); + this->cleanup_(std::move(backend)); + return OTA_CONNECTION_ERROR; + } else if (bufsize > 0 && bufsize <= this->http_recv_buffer_) { + // add read bytes to MD5 + md5_receive.add(buf, bufsize); + + // write bytes to OTA backend + this->update_started_ = true; + error_code = backend->write(buf, bufsize); + if (error_code != ota::OTA_RESPONSE_OK) { + // error code explanation available at + // https://github.com/esphome/esphome/blob/dev/esphome/components/ota/ota_backend.h + ESP_LOGE(TAG, "Error code (%02X) writing binary data to flash at offset %d and size %d", error_code, + this->bytes_read_ - bufsize, this->body_length_); + this->cleanup_(std::move(backend)); + return error_code; + } + } + + uint32_t now = millis(); + if ((now - last_progress > 1000) or (this->bytes_read_ == this->body_length_)) { + last_progress = now; + float percentage = this->bytes_read_ * 100.0f / this->body_length_; + ESP_LOGD(TAG, "Progress: %0.1f%%", percentage); +#ifdef USE_OTA_STATE_CALLBACK + this->state_callback_.call(ota::OTA_IN_PROGRESS, percentage, 0); +#endif + } + } // while + + ESP_LOGI(TAG, "Done in %.0f seconds", float(millis() - update_start_time) / 1000); + + // verify MD5 is as expected and act accordingly + md5_receive.calculate(); + md5_receive.get_hex(md5_receive_str.get()); + this->md5_computed_ = md5_receive_str.get(); + if (strncmp(this->md5_computed_.c_str(), this->md5_expected_.c_str(), MD5_SIZE) != 0) { + ESP_LOGE(TAG, "MD5 computed: %s - Aborting due to MD5 mismatch", this->md5_computed_.c_str()); + this->cleanup_(std::move(backend)); + return ota::OTA_RESPONSE_ERROR_MD5_MISMATCH; + } else { + backend->set_update_md5(md5_receive_str.get()); + } + + this->http_end(); + + // feed watchdog and give other tasks a chance to run + App.feed_wdt(); + yield(); + delay(100); // NOLINT + + error_code = backend->end(); + if (error_code != ota::OTA_RESPONSE_OK) { + ESP_LOGW(TAG, "Error ending update! error_code: %d", error_code); + this->cleanup_(std::move(backend)); + return error_code; + } + + ESP_LOGI(TAG, "Update complete"); + return ota::OTA_RESPONSE_OK; +} + +std::string OtaHttpRequestComponent::get_url_with_auth_(const std::string &url) { + if (this->username_.empty() || this->password_.empty()) { + return url; + } + + auto start_char = url.find("://"); + if ((start_char == std::string::npos) || (start_char < 4)) { + ESP_LOGE(TAG, "Incorrect URL prefix"); + return {}; + } + + ESP_LOGD(TAG, "Using basic HTTP authentication"); + + start_char += 3; // skip '://' characters + auto url_with_auth = + url.substr(0, start_char) + this->username_ + ":" + this->password_ + "@" + url.substr(start_char); + return url_with_auth; +} + +bool OtaHttpRequestComponent::http_get_md5_() { + if (this->md5_url_.empty()) { + return false; + } + + auto url_with_auth = this->get_url_with_auth_(this->md5_url_); + if (url_with_auth.empty()) { + return false; + } + + ESP_LOGVV(TAG, "url_with_auth: %s", url_with_auth.c_str()); + ESP_LOGI(TAG, "Connecting to: %s", this->md5_url_.c_str()); + this->http_init(url_with_auth); + if (!this->check_status()) { + this->http_end(); + return false; + } + int length = this->body_length_; + if (length < 0) { + this->http_end(); + return false; + } + if (length < MD5_SIZE) { + ESP_LOGE(TAG, "MD5 file must be %u bytes; %u bytes reported by HTTP server. Aborting", MD5_SIZE, + this->body_length_); + this->http_end(); + return false; + } + + this->bytes_read_ = 0; + this->md5_expected_.resize(MD5_SIZE); + auto read_len = this->http_read((uint8_t *) this->md5_expected_.data(), MD5_SIZE); + this->http_end(); + + return read_len == MD5_SIZE; +} + +bool OtaHttpRequestComponent::validate_url_(const std::string &url) { + if ((url.length() < 8) || (url.find("http") != 0) || (url.find("://") == std::string::npos)) { + ESP_LOGE(TAG, "URL is invalid and/or must be prefixed with 'http://' or 'https://'"); + return false; + } + return true; +} + +} // namespace http_request +} // namespace esphome diff --git a/esphome/components/http_request/ota/ota_http_request.h b/esphome/components/http_request/ota/ota_http_request.h new file mode 100644 index 0000000000..9fbdf2ec25 --- /dev/null +++ b/esphome/components/http_request/ota/ota_http_request.h @@ -0,0 +1,72 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/defines.h" +#include "esphome/components/ota/ota_backend.h" + +#include +#include +#include + +namespace esphome { +namespace http_request { + +static const char *const TAG = "http_request.ota"; +static const uint8_t MD5_SIZE = 32; + +enum OtaHttpRequestError : uint8_t { + OTA_MD5_INVALID = 0x10, + OTA_BAD_URL = 0x11, + OTA_CONNECTION_ERROR = 0x12, +}; + +class OtaHttpRequestComponent : public ota::OTAComponent { + public: + void setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } + + void set_md5_url(const std::string &md5_url); + void set_md5(const std::string &md5) { this->md5_expected_ = md5; } + void set_password(const std::string &password) { this->password_ = password; } + void set_timeout(const uint64_t timeout) { this->timeout_ = timeout; } + void set_url(const std::string &url); + void set_username(const std::string &username) { this->username_ = username; } + + std::string md5_computed() { return this->md5_computed_; } + std::string md5_expected() { return this->md5_expected_; } + + bool check_status(); + + void flash(); + + virtual void http_init(const std::string &url){}; + virtual int http_read(uint8_t *buf, size_t len) { return 0; }; + virtual void http_end(){}; + + protected: + void cleanup_(std::unique_ptr backend); + uint8_t do_ota_(); + std::string get_url_with_auth_(const std::string &url); + bool http_get_md5_(); + bool secure_() { return this->url_.find("https:") != std::string::npos; }; + bool validate_url_(const std::string &url); + + std::string md5_computed_{}; + std::string md5_expected_{}; + std::string md5_url_{}; + std::string password_{}; + std::string username_{}; + std::string url_{}; + size_t body_length_ = 0; + size_t bytes_read_ = 0; + int status_ = -1; + uint64_t timeout_ = 0; + bool update_started_ = false; + const uint16_t http_recv_buffer_ = 256; // the firmware GET chunk size + const uint16_t max_http_recv_buffer_ = 512; // internal max http buffer size must be > HTTP_RECV_BUFFER_ (TLS + // overhead) and must be a power of two from 512 to 4096 +}; + +} // namespace http_request +} // namespace esphome diff --git a/esphome/components/http_request/ota/ota_http_request_arduino.cpp b/esphome/components/http_request/ota/ota_http_request_arduino.cpp new file mode 100644 index 0000000000..d1dc638d5e --- /dev/null +++ b/esphome/components/http_request/ota/ota_http_request_arduino.cpp @@ -0,0 +1,134 @@ +#include "ota_http_request.h" +#include "watchdog.h" + +#ifdef USE_ARDUINO +#include "ota_http_request_arduino.h" +#include "esphome/core/defines.h" +#include "esphome/core/log.h" +#include "esphome/core/application.h" +#include "esphome/components/network/util.h" +#include "esphome/components/md5/md5.h" + +namespace esphome { +namespace http_request { + +struct Header { + const char *name; + const char *value; +}; + +void OtaHttpRequestComponentArduino::http_init(const std::string &url) { + const char *header_keys[] = {"Content-Length", "Content-Type"}; + const size_t header_count = sizeof(header_keys) / sizeof(header_keys[0]); + watchdog::WatchdogManager wdts; + +#ifdef USE_ESP8266 + if (this->stream_ptr_ == nullptr && this->set_stream_ptr_()) { + ESP_LOGE(TAG, "Unable to set client"); + return; + } +#endif // USE_ESP8266 + +#ifdef USE_RP2040 + this->client_.setInsecure(); +#endif + + App.feed_wdt(); + +#if defined(USE_ESP32) || defined(USE_RP2040) + this->status_ = this->client_.begin(url.c_str()); +#endif +#ifdef USE_ESP8266 + this->client_.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS); + this->status_ = this->client_.begin(*this->stream_ptr_, url.c_str()); +#endif + + if (!this->status_) { + this->client_.end(); + return; + } + + this->client_.setReuse(true); + + // returned needed headers must be collected before the requests + this->client_.collectHeaders(header_keys, header_count); + + // HTTP GET + this->status_ = this->client_.GET(); + + this->body_length_ = (size_t) this->client_.getSize(); + +#if defined(USE_ESP32) || defined(USE_RP2040) + if (this->stream_ptr_ == nullptr) { + this->set_stream_ptr_(); + } +#endif +} + +int OtaHttpRequestComponentArduino::http_read(uint8_t *buf, const size_t max_len) { +#ifdef USE_ESP8266 +#if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(3, 1, 0) // && USE_ARDUINO_VERSION_CODE < VERSION_CODE(?, ?, ?) + if (!this->secure_()) { + ESP_LOGW(TAG, "Using HTTP on Arduino version >= 3.1 is **very** slow. Consider setting framework version to 3.0.2 " + "in your YAML, or use HTTPS"); + } +#endif // USE_ARDUINO_VERSION_CODE +#endif // USE_ESP8266 + + watchdog::WatchdogManager wdts; + + // Since arduino8266 >= 3.1 using this->stream_ptr_ is broken (https://github.com/esp8266/Arduino/issues/9035) + WiFiClient *stream_ptr = this->client_.getStreamPtr(); + if (stream_ptr == nullptr) { + ESP_LOGE(TAG, "Stream pointer vanished!"); + return -1; + } + + int available_data = stream_ptr->available(); + int bufsize = std::min((int) max_len, available_data); + if (bufsize > 0) { + stream_ptr->readBytes(buf, bufsize); + this->bytes_read_ += bufsize; + buf[bufsize] = '\0'; // not fed to ota + } + + return bufsize; +} + +void OtaHttpRequestComponentArduino::http_end() { + watchdog::WatchdogManager wdts; + this->client_.end(); +} + +int OtaHttpRequestComponentArduino::set_stream_ptr_() { +#ifdef USE_ESP8266 +#ifdef USE_HTTP_REQUEST_ESP8266_HTTPS + if (this->secure_()) { + ESP_LOGV(TAG, "ESP8266 HTTPS connection with WiFiClientSecure"); + this->stream_ptr_ = std::make_unique(); + WiFiClientSecure *secure_client = static_cast(this->stream_ptr_.get()); + secure_client->setBufferSizes(this->max_http_recv_buffer_, 512); + secure_client->setInsecure(); + } else { + this->stream_ptr_ = std::make_unique(); + } +#else + ESP_LOGV(TAG, "ESP8266 HTTP connection with WiFiClient"); + if (this->secure_()) { + ESP_LOGE(TAG, "Can't use HTTPS connection with esp8266_disable_ssl_support"); + return -1; + } + this->stream_ptr_ = std::make_unique(); +#endif // USE_HTTP_REQUEST_ESP8266_HTTPS +#endif // USE_ESP8266 + +#if defined(USE_ESP32) || defined(USE_RP2040) + this->stream_ptr_ = std::unique_ptr(this->client_.getStreamPtr()); +#endif + return 0; +} + +} // namespace http_request +} // namespace esphome + +#endif // USE_ARDUINO diff --git a/esphome/components/http_request/ota/ota_http_request_arduino.h b/esphome/components/http_request/ota/ota_http_request_arduino.h new file mode 100644 index 0000000000..02bc046520 --- /dev/null +++ b/esphome/components/http_request/ota/ota_http_request_arduino.h @@ -0,0 +1,42 @@ +#pragma once + +#include "ota_http_request.h" + +#ifdef USE_ARDUINO +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/core/defines.h" + +#include +#include +#include + +#if defined(USE_ESP32) || defined(USE_RP2040) +#include +#endif +#ifdef USE_ESP8266 +#include +#ifdef USE_HTTP_REQUEST_ESP8266_HTTPS +#include +#endif +#endif + +namespace esphome { +namespace http_request { + +class OtaHttpRequestComponentArduino : public OtaHttpRequestComponent { + public: + void http_init(const std::string &url) override; + int http_read(uint8_t *buf, size_t len) override; + void http_end() override; + + protected: + int set_stream_ptr_(); + HTTPClient client_{}; + std::unique_ptr stream_ptr_; +}; + +} // namespace http_request +} // namespace esphome + +#endif // USE_ARDUINO diff --git a/esphome/components/http_request/ota/ota_http_request_idf.cpp b/esphome/components/http_request/ota/ota_http_request_idf.cpp new file mode 100644 index 0000000000..9fa565d9bb --- /dev/null +++ b/esphome/components/http_request/ota/ota_http_request_idf.cpp @@ -0,0 +1,86 @@ +#include "ota_http_request_idf.h" +#include "watchdog.h" + +#ifdef USE_ESP_IDF +#include "esphome/core/application.h" +#include "esphome/core/defines.h" +#include "esphome/core/log.h" +#include "esphome/components/md5/md5.h" +#include "esphome/components/network/util.h" + +#include "esp_event.h" +#include "esp_http_client.h" +#include "esp_idf_version.h" +#include "esp_log.h" +#include "esp_netif.h" +#include "esp_system.h" +#include "esp_task_wdt.h" +#include "esp_tls.h" + +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "nvs_flash.h" + +#include +#include +#include +#include +#include +#if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE +#include "esp_crt_bundle.h" +#endif + +namespace esphome { +namespace http_request { + +void OtaHttpRequestComponentIDF::http_init(const std::string &url) { + App.feed_wdt(); +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wmissing-field-initializers" + esp_http_client_config_t config = {nullptr}; + config.url = url.c_str(); + config.method = HTTP_METHOD_GET; + config.timeout_ms = (int) this->timeout_; + config.buffer_size = this->max_http_recv_buffer_; + config.auth_type = HTTP_AUTH_TYPE_BASIC; + config.max_authorization_retries = -1; +#if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE + if (this->secure_()) { + config.crt_bundle_attach = esp_crt_bundle_attach; + } +#endif +#pragma GCC diagnostic pop + + watchdog::WatchdogManager wdts; + this->client_ = esp_http_client_init(&config); + if ((this->status_ = esp_http_client_open(this->client_, 0)) == ESP_OK) { + this->body_length_ = esp_http_client_fetch_headers(this->client_); + this->status_ = esp_http_client_get_status_code(this->client_); + } +} + +int OtaHttpRequestComponentIDF::http_read(uint8_t *buf, const size_t max_len) { + watchdog::WatchdogManager wdts; + int bufsize = std::min(max_len, this->body_length_ - this->bytes_read_); + + App.feed_wdt(); + int read_len = esp_http_client_read(this->client_, (char *) buf, bufsize); + if (read_len > 0) { + this->bytes_read_ += bufsize; + buf[bufsize] = '\0'; // not fed to ota + } + + return read_len; +} + +void OtaHttpRequestComponentIDF::http_end() { + watchdog::WatchdogManager wdts; + + esp_http_client_close(this->client_); + esp_http_client_cleanup(this->client_); +} + +} // namespace http_request +} // namespace esphome + +#endif // USE_ESP_IDF diff --git a/esphome/components/http_request/ota/ota_http_request_idf.h b/esphome/components/http_request/ota/ota_http_request_idf.h new file mode 100644 index 0000000000..9783b2a3e1 --- /dev/null +++ b/esphome/components/http_request/ota/ota_http_request_idf.h @@ -0,0 +1,24 @@ +#pragma once + +#include "ota_http_request.h" + +#ifdef USE_ESP_IDF +#include "esp_http_client.h" + +namespace esphome { +namespace http_request { + +class OtaHttpRequestComponentIDF : public OtaHttpRequestComponent { + public: + void http_init(const std::string &url) override; + int http_read(uint8_t *buf, size_t len) override; + void http_end() override; + + protected: + esp_http_client_handle_t client_{}; +}; + +} // namespace http_request +} // namespace esphome + +#endif // USE_ESP_IDF diff --git a/esphome/components/http_request/ota/watchdog.cpp b/esphome/components/http_request/ota/watchdog.cpp new file mode 100644 index 0000000000..663c9afaac --- /dev/null +++ b/esphome/components/http_request/ota/watchdog.cpp @@ -0,0 +1,71 @@ +#include "watchdog.h" + +#ifdef USE_HTTP_REQUEST_OTA_WATCHDOG_TIMEOUT + +#include "esphome/core/application.h" +#include "esphome/core/log.h" + +#include +#include +#ifdef USE_ESP32 +#include "esp_idf_version.h" +#include "esp_task_wdt.h" +#endif +#ifdef USE_RP2040 +#include "hardware/watchdog.h" +#include "pico/stdlib.h" +#endif + +namespace esphome { +namespace http_request { +namespace watchdog { + +static const char *const TAG = "watchdog.http_request.ota"; + +WatchdogManager::WatchdogManager() { + this->saved_timeout_ms_ = this->get_timeout_(); + this->set_timeout_(USE_HTTP_REQUEST_OTA_WATCHDOG_TIMEOUT); +} + +WatchdogManager::~WatchdogManager() { this->set_timeout_(this->saved_timeout_ms_); } + +void WatchdogManager::set_timeout_(uint32_t timeout_ms) { + ESP_LOGV(TAG, "Adjusting WDT to %" PRIu32 "ms", timeout_ms); +#ifdef USE_ESP32 +#if ESP_IDF_VERSION_MAJOR >= 5 + esp_task_wdt_config_t wdt_config = { + .timeout_ms = timeout_ms, + .idle_core_mask = 0x03, + .trigger_panic = true, + }; + esp_task_wdt_reconfigure(&wdt_config); +#else + esp_task_wdt_init(timeout_ms, true); +#endif // ESP_IDF_VERSION_MAJOR +#endif // USE_ESP32 + +#ifdef USE_RP2040 + watchdog_enable(timeout_ms, true); +#endif +} + +uint32_t WatchdogManager::get_timeout_() { + uint32_t timeout_ms = 0; + +#ifdef USE_ESP32 + timeout_ms = (uint32_t) CONFIG_ESP_TASK_WDT_TIMEOUT_S * 1000; +#endif // USE_ESP32 + +#ifdef USE_RP2040 + timeout_ms = watchdog_get_count() / 1000; +#endif + + ESP_LOGVV(TAG, "get_timeout: %" PRIu32 "ms", timeout_ms); + + return timeout_ms; +} + +} // namespace watchdog +} // namespace http_request +} // namespace esphome +#endif diff --git a/esphome/components/http_request/ota/watchdog.h b/esphome/components/http_request/ota/watchdog.h new file mode 100644 index 0000000000..0a09dcd6fa --- /dev/null +++ b/esphome/components/http_request/ota/watchdog.h @@ -0,0 +1,27 @@ +#pragma once + +#include "esphome/core/defines.h" + +#include + +namespace esphome { +namespace http_request { +namespace watchdog { + +class WatchdogManager { +#ifdef USE_HTTP_REQUEST_OTA_WATCHDOG_TIMEOUT + public: + WatchdogManager(); + ~WatchdogManager(); + + private: + uint32_t get_timeout_(); + void set_timeout_(uint32_t timeout_ms); + + uint32_t saved_timeout_ms_{0}; +#endif +}; + +} // namespace watchdog +} // namespace http_request +} // namespace esphome diff --git a/esphome/components/improv_serial/improv_serial_component.cpp b/esphome/components/improv_serial/improv_serial_component.cpp index 40297bee68..2937720496 100644 --- a/esphome/components/improv_serial/improv_serial_component.cpp +++ b/esphome/components/improv_serial/improv_serial_component.cpp @@ -109,6 +109,7 @@ void ImprovSerialComponent::write_data_(std::vector &data) { #if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32S3) case logger::UART_SELECTION_USB_SERIAL_JTAG: usb_serial_jtag_write_bytes((char *) data.data(), data.size(), 20 / portTICK_PERIOD_MS); + usb_serial_jtag_ll_txfifo_flush(); // fixes for issue in IDF 4.4.7 break; #endif // USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32S3 default: diff --git a/esphome/components/improv_serial/improv_serial_component.h b/esphome/components/improv_serial/improv_serial_component.h index 8583d0762b..f737f93d86 100644 --- a/esphome/components/improv_serial/improv_serial_component.h +++ b/esphome/components/improv_serial/improv_serial_component.h @@ -17,6 +17,7 @@ #if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32S3) || \ defined(USE_ESP32_VARIANT_ESP32H2) #include +#include #endif #if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) #include diff --git a/esphome/components/remote_base/__init__.py b/esphome/components/remote_base/__init__.py index 9d49026aa3..3d1c10a092 100644 --- a/esphome/components/remote_base/__init__.py +++ b/esphome/components/remote_base/__init__.py @@ -1769,7 +1769,17 @@ def aeha_dumper(var, config): pass -@register_action("aeha", AEHAAction, AEHA_SCHEMA) +@register_action( + "aeha", + AEHAAction, + AEHA_SCHEMA.extend( + { + cv.Optional(CONF_CARRIER_FREQUENCY, default="38000Hz"): cv.All( + cv.frequency, cv.int_ + ), + } + ), +) async def aeha_action(var, config, args): template_ = await cg.templatable(config[CONF_ADDRESS], args, cg.uint16) cg.add(var.set_address(template_)) @@ -1777,6 +1787,8 @@ async def aeha_action(var, config, args): config[CONF_DATA], args, cg.std_vector.template(cg.uint8) ) cg.add(var.set_data(template_)) + templ = await cg.templatable(config[CONF_CARRIER_FREQUENCY], args, cg.uint32) + cg.add(var.set_carrier_frequency(templ)) # Haier diff --git a/esphome/components/remote_base/aeha_protocol.cpp b/esphome/components/remote_base/aeha_protocol.cpp index 40bdadf634..04fe731817 100644 --- a/esphome/components/remote_base/aeha_protocol.cpp +++ b/esphome/components/remote_base/aeha_protocol.cpp @@ -16,7 +16,6 @@ static const uint16_t BIT_ZERO_LOW_US = BITWISE; static const uint16_t TRAILER = BITWISE; void AEHAProtocol::encode(RemoteTransmitData *dst, const AEHAData &data) { - dst->set_carrier_frequency(38000); dst->reserve(2 + 32 + (data.data.size() * 2) + 1); dst->item(HEADER_HIGH_US, HEADER_LOW_US); diff --git a/esphome/components/remote_base/aeha_protocol.h b/esphome/components/remote_base/aeha_protocol.h index c41f3f8df1..51718eefcb 100644 --- a/esphome/components/remote_base/aeha_protocol.h +++ b/esphome/components/remote_base/aeha_protocol.h @@ -30,12 +30,14 @@ template class AEHAAction : public RemoteTransmitterActionBase, data) + TEMPLATABLE_VALUE(uint32_t, carrier_frequency); void set_data(const std::vector &data) { data_ = data; } void encode(RemoteTransmitData *dst, Ts... x) override { AEHAData data{}; data.address = this->address_.value(x...); data.data = this->data_.value(x...); + dst->set_carrier_frequency(this->carrier_frequency_.value(x...)); AEHAProtocol().encode(dst, data); } }; diff --git a/esphome/components/sntp/sntp_component.cpp b/esphome/components/sntp/sntp_component.cpp index a0e791be75..4ded98d483 100644 --- a/esphome/components/sntp/sntp_component.cpp +++ b/esphome/components/sntp/sntp_component.cpp @@ -20,7 +20,6 @@ namespace sntp { static const char *const TAG = "sntp"; void SNTPComponent::setup() { -#ifndef USE_HOST ESP_LOGCONFIG(TAG, "Setting up SNTP..."); #if defined(USE_ESP_IDF) if (esp_sntp_enabled()) { @@ -44,7 +43,6 @@ void SNTPComponent::setup() { #endif sntp_init(); -#endif } void SNTPComponent::dump_config() { ESP_LOGCONFIG(TAG, "SNTP Time:"); @@ -54,7 +52,7 @@ void SNTPComponent::dump_config() { ESP_LOGCONFIG(TAG, " Timezone: '%s'", this->timezone_.c_str()); } void SNTPComponent::update() { -#if !defined(USE_ESP_IDF) && !defined(USE_HOST) +#if !defined(USE_ESP_IDF) // force resync if (sntp_enabled()) { sntp_stop(); diff --git a/esphome/components/sntp/time.py b/esphome/components/sntp/time.py index b1362f5421..7cc82e3dff 100644 --- a/esphome/components/sntp/time.py +++ b/esphome/components/sntp/time.py @@ -2,24 +2,41 @@ from esphome.components import time as time_ import esphome.config_validation as cv import esphome.codegen as cg from esphome.core import CORE -from esphome.const import CONF_ID, CONF_SERVERS - +from esphome.const import ( + CONF_ID, + CONF_SERVERS, + PLATFORM_ESP32, + PLATFORM_ESP8266, + PLATFORM_RP2040, + PLATFORM_RTL87XX, + PLATFORM_BK72XX, +) DEPENDENCIES = ["network"] sntp_ns = cg.esphome_ns.namespace("sntp") SNTPComponent = sntp_ns.class_("SNTPComponent", time_.RealTimeClock) - DEFAULT_SERVERS = ["0.pool.ntp.org", "1.pool.ntp.org", "2.pool.ntp.org"] -CONFIG_SCHEMA = time_.TIME_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(SNTPComponent), - cv.Optional(CONF_SERVERS, default=DEFAULT_SERVERS): cv.All( - cv.ensure_list(cv.Any(cv.domain, cv.hostname)), cv.Length(min=1, max=3) - ), - } -).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = cv.All( + time_.TIME_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(SNTPComponent), + cv.Optional(CONF_SERVERS, default=DEFAULT_SERVERS): cv.All( + cv.ensure_list(cv.Any(cv.domain, cv.hostname)), cv.Length(min=1, max=3) + ), + } + ).extend(cv.COMPONENT_SCHEMA), + cv.only_on( + [ + PLATFORM_ESP32, + PLATFORM_ESP8266, + PLATFORM_RP2040, + PLATFORM_BK72XX, + PLATFORM_RTL87XX, + ] + ), +) async def to_code(config): diff --git a/esphome/components/tuya/climate/__init__.py b/esphome/components/tuya/climate/__init__.py index 56eb377ed7..363e7c764b 100644 --- a/esphome/components/tuya/climate/__init__.py +++ b/esphome/components/tuya/climate/__init__.py @@ -189,8 +189,6 @@ CONFIG_SCHEMA = cv.All( cv.has_at_least_one_key(CONF_TARGET_TEMPERATURE_DATAPOINT, CONF_SWITCH_DATAPOINT), validate_temperature_multipliers, validate_cooling_values, - cv.has_at_most_one_key(CONF_ACTIVE_STATE, CONF_HEATING_STATE_PIN), - cv.has_at_most_one_key(CONF_ACTIVE_STATE, CONF_COOLING_STATE_PIN), ) @@ -207,6 +205,12 @@ async def to_code(config): if switch_datapoint := config.get(CONF_SWITCH_DATAPOINT): cg.add(var.set_switch_id(switch_datapoint)) + if heating_state_pin_config := config.get(CONF_HEATING_STATE_PIN): + heating_state_pin = await cg.gpio_pin_expression(heating_state_pin_config) + cg.add(var.set_heating_state_pin(heating_state_pin)) + if cooling_state_pin_config := config.get(CONF_COOLING_STATE_PIN): + cooling_state_pin = await cg.gpio_pin_expression(cooling_state_pin_config) + cg.add(var.set_cooling_state_pin(cooling_state_pin)) if active_state_config := config.get(CONF_ACTIVE_STATE): cg.add(var.set_active_state_id(active_state_config.get(CONF_DATAPOINT))) if (heating_value := active_state_config.get(CONF_HEATING_VALUE)) is not None: @@ -217,13 +221,6 @@ async def to_code(config): cg.add(var.set_active_state_drying_value(drying_value)) if (fanonly_value := active_state_config.get(CONF_FANONLY_VALUE)) is not None: cg.add(var.set_active_state_fanonly_value(fanonly_value)) - else: - if heating_state_pin_config := config.get(CONF_HEATING_STATE_PIN): - heating_state_pin = await cg.gpio_pin_expression(heating_state_pin_config) - cg.add(var.set_heating_state_pin(heating_state_pin)) - if cooling_state_pin_config := config.get(CONF_COOLING_STATE_PIN): - cooling_state_pin = await cg.gpio_pin_expression(cooling_state_pin_config) - cg.add(var.set_cooling_state_pin(cooling_state_pin)) if target_temperature_datapoint := config.get(CONF_TARGET_TEMPERATURE_DATAPOINT): cg.add(var.set_target_temperature_id(target_temperature_datapoint)) diff --git a/esphome/components/tuya/climate/tuya_climate.cpp b/esphome/components/tuya/climate/tuya_climate.cpp index 274e19a69e..7827a4e3ab 100644 --- a/esphome/components/tuya/climate/tuya_climate.cpp +++ b/esphome/components/tuya/climate/tuya_climate.cpp @@ -24,6 +24,14 @@ void TuyaClimate::setup() { this->publish_state(); }); } + if (this->heating_state_pin_ != nullptr) { + this->heating_state_pin_->setup(); + this->heating_state_ = this->heating_state_pin_->digital_read(); + } + if (this->cooling_state_pin_ != nullptr) { + this->cooling_state_pin_->setup(); + this->cooling_state_ = this->cooling_state_pin_->digital_read(); + } if (this->active_state_id_.has_value()) { this->parent_->register_listener(*this->active_state_id_, [this](const TuyaDatapoint &datapoint) { ESP_LOGV(TAG, "MCU reported active state is: %u", datapoint.value_enum); @@ -31,15 +39,6 @@ void TuyaClimate::setup() { this->compute_state_(); this->publish_state(); }); - } else { - if (this->heating_state_pin_ != nullptr) { - this->heating_state_pin_->setup(); - this->heating_state_ = this->heating_state_pin_->digital_read(); - } - if (this->cooling_state_pin_ != nullptr) { - this->cooling_state_pin_->setup(); - this->cooling_state_ = this->cooling_state_pin_->digital_read(); - } } if (this->target_temperature_id_.has_value()) { this->parent_->register_listener(*this->target_temperature_id_, [this](const TuyaDatapoint &datapoint) { @@ -113,9 +112,6 @@ void TuyaClimate::setup() { } void TuyaClimate::loop() { - if (this->active_state_id_.has_value()) - return; - bool state_changed = false; if (this->heating_state_pin_ != nullptr) { bool heating_state = this->heating_state_pin_->digital_read(); @@ -147,14 +143,18 @@ void TuyaClimate::control(const climate::ClimateCall &call) { this->parent_->set_boolean_datapoint_value(*this->switch_id_, switch_state); const climate::ClimateMode new_mode = *call.get_mode(); - if (new_mode == climate::CLIMATE_MODE_HEAT && this->supports_heat_) { - this->parent_->set_enum_datapoint_value(*this->active_state_id_, *this->active_state_heating_value_); - } else if (new_mode == climate::CLIMATE_MODE_COOL && this->supports_cool_) { - this->parent_->set_enum_datapoint_value(*this->active_state_id_, *this->active_state_cooling_value_); - } else if (new_mode == climate::CLIMATE_MODE_DRY && this->active_state_drying_value_.has_value()) { - this->parent_->set_enum_datapoint_value(*this->active_state_id_, *this->active_state_drying_value_); - } else if (new_mode == climate::CLIMATE_MODE_FAN_ONLY && this->active_state_fanonly_value_.has_value()) { - this->parent_->set_enum_datapoint_value(*this->active_state_id_, *this->active_state_fanonly_value_); + if (this->active_state_id_.has_value()) { + if (new_mode == climate::CLIMATE_MODE_HEAT && this->supports_heat_) { + this->parent_->set_enum_datapoint_value(*this->active_state_id_, *this->active_state_heating_value_); + } else if (new_mode == climate::CLIMATE_MODE_COOL && this->supports_cool_) { + this->parent_->set_enum_datapoint_value(*this->active_state_id_, *this->active_state_cooling_value_); + } else if (new_mode == climate::CLIMATE_MODE_DRY && this->active_state_drying_value_.has_value()) { + this->parent_->set_enum_datapoint_value(*this->active_state_id_, *this->active_state_drying_value_); + } else if (new_mode == climate::CLIMATE_MODE_FAN_ONLY && this->active_state_fanonly_value_.has_value()) { + this->parent_->set_enum_datapoint_value(*this->active_state_id_, *this->active_state_fanonly_value_); + } + } else { + ESP_LOGW(TAG, "Active state (mode) datapoint not configured"); } } @@ -422,7 +422,32 @@ void TuyaClimate::compute_state_() { } climate::ClimateAction target_action = climate::CLIMATE_ACTION_IDLE; - if (this->active_state_id_.has_value()) { + if (this->heating_state_pin_ != nullptr || this->cooling_state_pin_ != nullptr) { + // Use state from input pins + if (this->heating_state_) { + target_action = climate::CLIMATE_ACTION_HEATING; + this->mode = climate::CLIMATE_MODE_HEAT; + } else if (this->cooling_state_) { + target_action = climate::CLIMATE_ACTION_COOLING; + this->mode = climate::CLIMATE_MODE_COOL; + } + if (this->active_state_id_.has_value()) { + // Both are available, use MCU datapoint as mode + if (this->supports_heat_ && this->active_state_heating_value_.has_value() && + this->active_state_ == this->active_state_heating_value_) { + this->mode = climate::CLIMATE_MODE_HEAT; + } else if (this->supports_cool_ && this->active_state_cooling_value_.has_value() && + this->active_state_ == this->active_state_cooling_value_) { + this->mode = climate::CLIMATE_MODE_COOL; + } else if (this->active_state_drying_value_.has_value() && + this->active_state_ == this->active_state_drying_value_) { + this->mode = climate::CLIMATE_MODE_DRY; + } else if (this->active_state_fanonly_value_.has_value() && + this->active_state_ == this->active_state_fanonly_value_) { + this->mode = climate::CLIMATE_MODE_FAN_ONLY; + } + } + } else if (this->active_state_id_.has_value()) { // Use state from MCU datapoint if (this->supports_heat_ && this->active_state_heating_value_.has_value() && this->active_state_ == this->active_state_heating_value_) { @@ -441,15 +466,6 @@ void TuyaClimate::compute_state_() { target_action = climate::CLIMATE_ACTION_FAN; this->mode = climate::CLIMATE_MODE_FAN_ONLY; } - } else if (this->heating_state_pin_ != nullptr || this->cooling_state_pin_ != nullptr) { - // Use state from input pins - if (this->heating_state_) { - target_action = climate::CLIMATE_ACTION_HEATING; - this->mode = climate::CLIMATE_MODE_HEAT; - } else if (this->cooling_state_) { - target_action = climate::CLIMATE_ACTION_COOLING; - this->mode = climate::CLIMATE_MODE_COOL; - } } else { // Fallback to active state calc based on temp and hysteresis const float temp_diff = this->target_temperature - this->current_temperature; diff --git a/esphome/components/tuya/cover/__init__.py b/esphome/components/tuya/cover/__init__.py index f886c7030f..2dd66f814d 100644 --- a/esphome/components/tuya/cover/__init__.py +++ b/esphome/components/tuya/cover/__init__.py @@ -16,6 +16,7 @@ CONF_DIRECTION_DATAPOINT = "direction_datapoint" CONF_POSITION_DATAPOINT = "position_datapoint" CONF_POSITION_REPORT_DATAPOINT = "position_report_datapoint" CONF_INVERT_POSITION = "invert_position" +CONF_INVERT_POSITION_REPORT = "invert_position_report" TuyaCover = tuya_ns.class_("TuyaCover", cover.Cover, cg.Component) @@ -47,6 +48,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_MIN_VALUE, default=0): cv.int_, cv.Optional(CONF_MAX_VALUE, default=100): cv.int_, cv.Optional(CONF_INVERT_POSITION, default=False): cv.boolean, + cv.Optional(CONF_INVERT_POSITION_REPORT, default=False): cv.boolean, cv.Optional(CONF_RESTORE_MODE, default="RESTORE"): cv.enum( RESTORE_MODES, upper=True ), @@ -71,6 +73,7 @@ async def to_code(config): cg.add(var.set_min_value(config[CONF_MIN_VALUE])) cg.add(var.set_max_value(config[CONF_MAX_VALUE])) cg.add(var.set_invert_position(config[CONF_INVERT_POSITION])) + cg.add(var.set_invert_position_report(config[CONF_INVERT_POSITION_REPORT])) cg.add(var.set_restore_mode(config[CONF_RESTORE_MODE])) paren = await cg.get_variable(config[CONF_TUYA_ID]) cg.add(var.set_tuya_parent(paren)) diff --git a/esphome/components/tuya/cover/tuya_cover.cpp b/esphome/components/tuya/cover/tuya_cover.cpp index fcb961f45e..14bf937cf7 100644 --- a/esphome/components/tuya/cover/tuya_cover.cpp +++ b/esphome/components/tuya/cover/tuya_cover.cpp @@ -51,7 +51,7 @@ void TuyaCover::setup() { return; } auto pos = float(datapoint.value_uint - this->min_value_) / this->value_range_; - this->position = 1.0f - pos; + this->position = this->invert_position_report_ ? pos : 1.0f - pos; this->publish_state(); }); } @@ -62,7 +62,7 @@ void TuyaCover::control(const cover::CoverCall &call) { this->parent_->force_set_enum_datapoint_value(*this->control_id_, COMMAND_STOP); } else { auto pos = this->position; - pos = 1.0f - pos; + pos = this->invert_position_report_ ? pos : 1.0f - pos; auto position_int = static_cast(pos * this->value_range_); position_int = position_int + this->min_value_; @@ -78,7 +78,7 @@ void TuyaCover::control(const cover::CoverCall &call) { this->parent_->force_set_enum_datapoint_value(*this->control_id_, COMMAND_CLOSE); } } else { - pos = 1.0f - pos; + pos = this->invert_position_report_ ? pos : 1.0f - pos; auto position_int = static_cast(pos * this->value_range_); position_int = position_int + this->min_value_; @@ -112,6 +112,9 @@ void TuyaCover::dump_config() { ESP_LOGCONFIG(TAG, " Configured as Inverted, but direction_datapoint isn't configured"); } } + if (this->invert_position_report_) { + ESP_LOGCONFIG(TAG, " Position Reporting Inverted"); + } if (this->control_id_.has_value()) { ESP_LOGCONFIG(TAG, " Control has datapoint ID %u", *this->control_id_); } diff --git a/esphome/components/tuya/cover/tuya_cover.h b/esphome/components/tuya/cover/tuya_cover.h index 87c72b0e66..bb5a00bc59 100644 --- a/esphome/components/tuya/cover/tuya_cover.h +++ b/esphome/components/tuya/cover/tuya_cover.h @@ -25,6 +25,7 @@ class TuyaCover : public cover::Cover, public Component { void set_min_value(uint32_t min_value) { min_value_ = min_value; } void set_max_value(uint32_t max_value) { max_value_ = max_value; } void set_invert_position(bool invert_position) { invert_position_ = invert_position; } + void set_invert_position_report(bool invert_position_report) { invert_position_report_ = invert_position_report; } void set_restore_mode(TuyaCoverRestoreMode restore_mode) { restore_mode_ = restore_mode; } protected: @@ -42,6 +43,7 @@ class TuyaCover : public cover::Cover, public Component { uint32_t max_value_; uint32_t value_range_; bool invert_position_; + bool invert_position_report_; }; } // namespace tuya diff --git a/esphome/components/wireguard/__init__.py b/esphome/components/wireguard/__init__.py index 177ad6d356..5d5f620b51 100644 --- a/esphome/components/wireguard/__init__.py +++ b/esphome/components/wireguard/__init__.py @@ -7,7 +7,10 @@ from esphome.const import ( CONF_TIME_ID, CONF_ADDRESS, CONF_REBOOT_TIMEOUT, + KEY_CORE, + KEY_FRAMEWORK_VERSION, ) +from esphome.components.esp32 import CORE, add_idf_sdkconfig_option from esphome.components import time from esphome.core import TimePeriod from esphome import automation @@ -117,6 +120,13 @@ async def to_code(config): if config[CONF_REQUIRE_CONNECTION_TO_PROCEED]: cg.add(var.disable_auto_proceed()) + # Workaround for crash on IDF 5+ + # See https://github.com/trombik/esp_wireguard/issues/33#issuecomment-1568503651 + if CORE.using_esp_idf and CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] >= cv.Version( + 5, 0, 0 + ): + add_idf_sdkconfig_option("CONFIG_LWIP_PPP_SUPPORT", True) + # This flag is added here because the esp_wireguard library statically # set the size of its allowed_ips list at compile time using this value; # the '+1' modifier is relative to the device's own address that will diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 5fc72921e1..7259e3c062 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -1949,13 +1949,13 @@ def url(value): except ValueError as e: raise Invalid("Not a valid URL") from e - if not parsed.scheme or not parsed.netloc: - raise Invalid("Expected a URL scheme and host") - return parsed.geturl() + if parsed.scheme and parsed.netloc or parsed.scheme == "file": + return parsed.geturl() + raise Invalid("Expected a file scheme or a URL scheme with host") def git_ref(value): - if re.match(r"[a-zA-Z0-9\-_.\./]+", value) is None: + if re.match(r"[a-zA-Z0-9_./-]+", value) is None: raise Invalid("Not a valid git ref") return value diff --git a/esphome/core/defines.h b/esphome/core/defines.h index c2ad0f641c..76b6af6592 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -33,6 +33,7 @@ #define USE_GRAPH #define USE_GRAPHICAL_DISPLAY_MENU #define USE_HOMEASSISTANT_TIME +#define USE_HTTP_REQUEST_OTA_WATCHDOG_TIMEOUT 8000 // NOLINT #define USE_JSON #define USE_LIGHT #define USE_LOCK diff --git a/script/setup b/script/setup index 1a18a6a9ea..aeb1b39bc1 100755 --- a/script/setup +++ b/script/setup @@ -13,11 +13,6 @@ if [ ! -n "$DEVCONTAINER" ] && [ ! -n "$VIRTUAL_ENV" ] && [ ! "$ESPHOME_NO_VENV" source $location fi -# Avoid unsafe git error when running inside devcontainer -if [ -n "$DEVCONTAINER" ]; then - git config --global --add safe.directory "$PWD" -fi - pip3 install -r requirements.txt -r requirements_optional.txt -r requirements_test.txt -r requirements_dev.txt pip3 install setuptools wheel pip3 install -e ".[dev,test,displays]" --config-settings editable_mode=compat diff --git a/tests/components/ethernet_info/common.yaml b/tests/components/ethernet_info/common.yaml index dade4d7ca5..d9a6f515b1 100644 --- a/tests/components/ethernet_info/common.yaml +++ b/tests/components/ethernet_info/common.yaml @@ -17,3 +17,5 @@ text_sensor: name: IP Address dns_address: name: DNS Address + mac_address: + name: MAC Address diff --git a/tests/components/host/common.yaml b/tests/components/host/common.yaml index 3d14c190a6..fca0c5d597 100644 --- a/tests/components/host/common.yaml +++ b/tests/components/host/common.yaml @@ -1,15 +1,10 @@ time: - - platform: sntp + - platform: host id: esptime timezone: Australia/Sydney logger: level: VERBOSE - logs: - lvgl: INFO - display: DEBUG - sensor: INFO - vnc: DEBUG host: mac_address: "62:23:45:AF:B3:DD" diff --git a/tests/components/http_request/common.yaml b/tests/components/http_request/common_http_request.yaml similarity index 95% rename from tests/components/http_request/common.yaml rename to tests/components/http_request/common_http_request.yaml index 848fe3f509..b00768c736 100644 --- a/tests/components/http_request/common.yaml +++ b/tests/components/http_request/common_http_request.yaml @@ -28,10 +28,6 @@ esphome: body: "Some data" verify_ssl: false -wifi: - ssid: MySSID - password: password1 - http_request: useragent: esphome/tagreader timeout: 10s diff --git a/tests/components/http_request/common_ota.yaml b/tests/components/http_request/common_ota.yaml new file mode 100644 index 0000000000..10e7d54c3f --- /dev/null +++ b/tests/components/http_request/common_ota.yaml @@ -0,0 +1,36 @@ +wifi: + ssid: MySSID + password: password1 + +ota: + - platform: http_request + verify_ssl: ${verify_ssl} + on_begin: + then: + - logger.log: "OTA start" + on_progress: + then: + - logger.log: + format: "OTA progress %0.1f%%" + args: ["x"] + on_end: + then: + - logger.log: "OTA end" + on_error: + then: + - logger.log: + format: "OTA update error %d" + args: ["x"] + on_state_change: + then: + lambda: 'ESP_LOGD("ota", "State %d", state);' + +button: + - platform: template + name: Firmware update + on_press: + then: + - ota_http_request.flash: + md5_url: http://my.ha.net:8123/local/esphome/firmware.md5 + url: http://my.ha.net:8123/local/esphome/firmware.bin + - logger.log: "This message should be not displayed (reboot)" diff --git a/tests/components/http_request/test-nossl.esp8266.yaml b/tests/components/http_request/test-nossl.esp8266.yaml new file mode 100644 index 0000000000..65116d5550 --- /dev/null +++ b/tests/components/http_request/test-nossl.esp8266.yaml @@ -0,0 +1,38 @@ +<<: !include common_http_request.yaml + +wifi: + ssid: MySSID + password: password1 + +ota: + - platform: http_request + esp8266_disable_ssl_support: true + on_begin: + then: + - logger.log: "OTA start" + on_progress: + then: + - logger.log: + format: "OTA progress %0.1f%%" + args: ["x"] + on_end: + then: + - logger.log: "OTA end" + on_error: + then: + - logger.log: + format: "OTA update error %d" + args: ["x"] + on_state_change: + then: + lambda: 'ESP_LOGD("ota", "State %d", state);' + +button: + - platform: template + name: Firmware update + on_press: + then: + - ota_http_request.flash: + md5_url: http://my.ha.net:8123/local/esphome/firmware.md5 + url: http://my.ha.net:8123/local/esphome/firmware.bin + - logger.log: "This message should be not displayed (reboot)" diff --git a/tests/components/http_request/test.esp32-c3-idf.yaml b/tests/components/http_request/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..da629e83a9 --- /dev/null +++ b/tests/components/http_request/test.esp32-c3-idf.yaml @@ -0,0 +1,4 @@ +substitutions: + verify_ssl: "true" + +<<: !include common_ota.yaml diff --git a/tests/components/http_request/test.esp32-c3.yaml b/tests/components/http_request/test.esp32-c3.yaml index 25cb37a0b4..1f597bb500 100644 --- a/tests/components/http_request/test.esp32-c3.yaml +++ b/tests/components/http_request/test.esp32-c3.yaml @@ -1,2 +1,5 @@ -packages: - common: !include common.yaml +substitutions: + verify_ssl: "false" + +<<: !include common_http_request.yaml +<<: !include common_ota.yaml diff --git a/tests/components/http_request/test.esp32-idf.yaml b/tests/components/http_request/test.esp32-idf.yaml new file mode 100644 index 0000000000..da629e83a9 --- /dev/null +++ b/tests/components/http_request/test.esp32-idf.yaml @@ -0,0 +1,4 @@ +substitutions: + verify_ssl: "true" + +<<: !include common_ota.yaml diff --git a/tests/components/http_request/test.esp32.yaml b/tests/components/http_request/test.esp32.yaml index 25cb37a0b4..1f597bb500 100644 --- a/tests/components/http_request/test.esp32.yaml +++ b/tests/components/http_request/test.esp32.yaml @@ -1,2 +1,5 @@ -packages: - common: !include common.yaml +substitutions: + verify_ssl: "false" + +<<: !include common_http_request.yaml +<<: !include common_ota.yaml diff --git a/tests/components/http_request/test.esp8266.yaml b/tests/components/http_request/test.esp8266.yaml index 25cb37a0b4..1f597bb500 100644 --- a/tests/components/http_request/test.esp8266.yaml +++ b/tests/components/http_request/test.esp8266.yaml @@ -1,2 +1,5 @@ -packages: - common: !include common.yaml +substitutions: + verify_ssl: "false" + +<<: !include common_http_request.yaml +<<: !include common_ota.yaml diff --git a/tests/components/http_request/test.rp2040.yaml b/tests/components/http_request/test.rp2040.yaml new file mode 100644 index 0000000000..077e4d82da --- /dev/null +++ b/tests/components/http_request/test.rp2040.yaml @@ -0,0 +1,4 @@ +substitutions: + verify_ssl: "false" + +<<: !include common_ota.yaml diff --git a/tests/components/remote_transmitter/common-buttons.yaml b/tests/components/remote_transmitter/common-buttons.yaml index e727017e85..c6a2453b20 100644 --- a/tests/components/remote_transmitter/common-buttons.yaml +++ b/tests/components/remote_transmitter/common-buttons.yaml @@ -118,6 +118,7 @@ button: on_press: remote_transmitter.transmit_aeha: address: 0x8008 + carrier_frequency: 36700Hz data: [ 0x00, diff --git a/tests/components/sntp/test.bk72xx.yaml b/tests/components/sntp/test.bk72xx.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/sntp/test.bk72xx.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/test1.yaml b/tests/test1.yaml index 2a20a1bb45..c49ff307e5 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -2906,6 +2906,7 @@ switch: turn_on_action: remote_transmitter.transmit_aeha: address: 0x8008 + carrier_frequency: 36700Hz data: [ 0x00,