From f8a1bd4e79449a07420f09b5a88321dbf1ca6d35 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 11 May 2022 12:50:42 +1200 Subject: [PATCH 01/60] Bump version to 2022.6.0-dev --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index fc928dc530..9a2e41d69f 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2022.5.0-dev" +__version__ = "2022.6.0-dev" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From 3f678e218d22194de8f2c19dd2a9fa5b009d5d77 Mon Sep 17 00:00:00 2001 From: Maurice Makaay Date: Wed, 11 May 2022 23:25:00 +0200 Subject: [PATCH 02/60] On epoch sync, restore local TZ (#3462) Co-authored-by: Maurice Makaay --- esphome/components/time/real_time_clock.cpp | 12 ++++++++++-- esphome/components/time/real_time_clock.h | 1 + 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/esphome/components/time/real_time_clock.cpp b/esphome/components/time/real_time_clock.cpp index 36c5f4161d..7b5f0aa49b 100644 --- a/esphome/components/time/real_time_clock.cpp +++ b/esphome/components/time/real_time_clock.cpp @@ -13,11 +13,11 @@ static const char *const TAG = "time"; RealTimeClock::RealTimeClock() = default; void RealTimeClock::call_setup() { - setenv("TZ", this->timezone_.c_str(), 1); - tzset(); + this->apply_timezone_(); PollingComponent::call_setup(); } void RealTimeClock::synchronize_epoch_(uint32_t epoch) { + // Update UTC epoch time. struct timeval timev { .tv_sec = static_cast(epoch), .tv_usec = 0, }; @@ -30,6 +30,9 @@ void RealTimeClock::synchronize_epoch_(uint32_t epoch) { ret = settimeofday(&timev, nullptr); } + // Move timezone back to local timezone. + this->apply_timezone_(); + if (ret != 0) { ESP_LOGW(TAG, "setimeofday() failed with code %d", ret); } @@ -41,6 +44,11 @@ void RealTimeClock::synchronize_epoch_(uint32_t epoch) { this->time_sync_callback_.call(); } +void RealTimeClock::apply_timezone_() { + setenv("TZ", this->timezone_.c_str(), 1); + tzset(); +} + size_t ESPTime::strftime(char *buffer, size_t buffer_len, const char *format) { struct tm c_tm = this->to_c_tm(); return ::strftime(buffer, buffer_len, format, &c_tm); diff --git a/esphome/components/time/real_time_clock.h b/esphome/components/time/real_time_clock.h index b22c6f04d7..7f4afee306 100644 --- a/esphome/components/time/real_time_clock.h +++ b/esphome/components/time/real_time_clock.h @@ -137,6 +137,7 @@ class RealTimeClock : public PollingComponent { void synchronize_epoch_(uint32_t epoch); std::string timezone_{}; + void apply_timezone_(); CallbackManager time_sync_callback_; }; From c2aaae4818266bf82cfb8650650e9929ad954331 Mon Sep 17 00:00:00 2001 From: Niclas Larsson Date: Thu, 12 May 2022 00:26:51 +0200 Subject: [PATCH 03/60] Shelly dimmer: Use unique_ptr to handle the lifetime of stm32_t (#3400) Co-authored-by: Martin <25747549+martgras@users.noreply.github.com> Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- .../shelly_dimmer/shelly_dimmer.cpp | 9 +- .../components/shelly_dimmer/stm32flash.cpp | 119 +++++++++--------- esphome/components/shelly_dimmer/stm32flash.h | 28 +++-- 3 files changed, 79 insertions(+), 77 deletions(-) diff --git a/esphome/components/shelly_dimmer/shelly_dimmer.cpp b/esphome/components/shelly_dimmer/shelly_dimmer.cpp index 3b79d0bf57..32c556da5e 100644 --- a/esphome/components/shelly_dimmer/shelly_dimmer.cpp +++ b/esphome/components/shelly_dimmer/shelly_dimmer.cpp @@ -158,11 +158,8 @@ bool ShellyDimmer::upgrade_firmware_() { ESP_LOGW(TAG, "Starting STM32 firmware upgrade"); this->reset_dfu_boot_(); - // Could be constexpr in c++17 - static const auto CLOSE = [](stm32_t *stm32) { stm32_close(stm32); }; - // Cleanup with RAII - std::unique_ptr stm32{stm32_init(this, STREAM_SERIAL, 1), CLOSE}; + auto stm32 = stm32_init(this, STREAM_SERIAL, 1); if (!stm32) { ESP_LOGW(TAG, "Failed to initialize STM32"); @@ -170,7 +167,7 @@ bool ShellyDimmer::upgrade_firmware_() { } // Erase STM32 flash. - if (stm32_erase_memory(stm32.get(), 0, STM32_MASS_ERASE) != STM32_ERR_OK) { + if (stm32_erase_memory(stm32, 0, STM32_MASS_ERASE) != STM32_ERR_OK) { ESP_LOGW(TAG, "Failed to erase STM32 flash memory"); return false; } @@ -196,7 +193,7 @@ bool ShellyDimmer::upgrade_firmware_() { std::memcpy(buffer, p, BUFFER_SIZE); p += BUFFER_SIZE; - if (stm32_write_memory(stm32.get(), addr, buffer, len) != STM32_ERR_OK) { + if (stm32_write_memory(stm32, addr, buffer, len) != STM32_ERR_OK) { ESP_LOGW(TAG, "Failed to write to STM32 flash memory"); return false; } diff --git a/esphome/components/shelly_dimmer/stm32flash.cpp b/esphome/components/shelly_dimmer/stm32flash.cpp index 4c777776fb..e688f2de36 100644 --- a/esphome/components/shelly_dimmer/stm32flash.cpp +++ b/esphome/components/shelly_dimmer/stm32flash.cpp @@ -117,7 +117,7 @@ namespace shelly_dimmer { namespace { -int flash_addr_to_page_ceil(const stm32_t *stm, uint32_t addr) { +int flash_addr_to_page_ceil(const stm32_unique_ptr &stm, uint32_t addr) { if (!(addr >= stm->dev->fl_start && addr <= stm->dev->fl_end)) return 0; @@ -135,7 +135,7 @@ int flash_addr_to_page_ceil(const stm32_t *stm, uint32_t addr) { return addr ? page + 1 : page; } -stm32_err_t stm32_get_ack_timeout(const stm32_t *stm, uint32_t timeout) { +stm32_err_t stm32_get_ack_timeout(const stm32_unique_ptr &stm, uint32_t timeout) { auto *stream = stm->stream; uint8_t rxbyte; @@ -168,9 +168,9 @@ stm32_err_t stm32_get_ack_timeout(const stm32_t *stm, uint32_t timeout) { } while (true); } -stm32_err_t stm32_get_ack(const stm32_t *stm) { return stm32_get_ack_timeout(stm, 0); } +stm32_err_t stm32_get_ack(const stm32_unique_ptr &stm) { return stm32_get_ack_timeout(stm, 0); } -stm32_err_t stm32_send_command_timeout(const stm32_t *stm, const uint8_t cmd, const uint32_t timeout) { +stm32_err_t stm32_send_command_timeout(const stm32_unique_ptr &stm, const uint8_t cmd, const uint32_t timeout) { auto *const stream = stm->stream; static constexpr auto BUFFER_SIZE = 2; @@ -194,12 +194,12 @@ stm32_err_t stm32_send_command_timeout(const stm32_t *stm, const uint8_t cmd, co return STM32_ERR_UNKNOWN; } -stm32_err_t stm32_send_command(const stm32_t *stm, const uint8_t cmd) { +stm32_err_t stm32_send_command(const stm32_unique_ptr &stm, const uint8_t cmd) { return stm32_send_command_timeout(stm, cmd, 0); } /* if we have lost sync, send a wrong command and expect a NACK */ -stm32_err_t stm32_resync(const stm32_t *stm) { +stm32_err_t stm32_resync(const stm32_unique_ptr &stm) { auto *const stream = stm->stream; uint32_t t0 = millis(); auto t1 = t0; @@ -238,7 +238,7 @@ stm32_err_t stm32_resync(const stm32_t *stm) { * * len is value of the first byte in the frame. */ -stm32_err_t stm32_guess_len_cmd(const stm32_t *stm, const uint8_t cmd, uint8_t *const data, unsigned int len) { +stm32_err_t stm32_guess_len_cmd(const stm32_unique_ptr &stm, const uint8_t cmd, uint8_t *const data, unsigned int len) { auto *const stream = stm->stream; if (stm32_send_command(stm, cmd) != STM32_ERR_OK) @@ -286,7 +286,7 @@ stm32_err_t stm32_guess_len_cmd(const stm32_t *stm, const uint8_t cmd, uint8_t * * This function sends the init sequence and, in case of timeout, recovers * the interface. */ -stm32_err_t stm32_send_init_seq(const stm32_t *stm) { +stm32_err_t stm32_send_init_seq(const stm32_unique_ptr &stm) { auto *const stream = stm->stream; stream->write_array(&STM32_CMD_INIT, 1); @@ -320,7 +320,7 @@ stm32_err_t stm32_send_init_seq(const stm32_t *stm) { return STM32_ERR_UNKNOWN; } -stm32_err_t stm32_mass_erase(const stm32_t *stm) { +stm32_err_t stm32_mass_erase(const stm32_unique_ptr &stm) { auto *const stream = stm->stream; if (stm32_send_command(stm, stm->cmd->er) != STM32_ERR_OK) { @@ -364,7 +364,7 @@ template std::unique_ptr malloc_array_raii DELETOR}; } -stm32_err_t stm32_pages_erase(const stm32_t *stm, const uint32_t spage, const uint32_t pages) { +stm32_err_t stm32_pages_erase(const stm32_unique_ptr &stm, const uint32_t spage, const uint32_t pages) { auto *const stream = stm->stream; uint8_t cs = 0; int i = 0; @@ -474,6 +474,18 @@ template void populate_buffer_with_address(uint8_t (&buffer)[N], uint3 buffer[4] = static_cast(buffer[0] ^ buffer[1] ^ buffer[2] ^ buffer[3]); } +template stm32_unique_ptr make_stm32_with_deletor(T ptr) { + static const auto CLOSE = [](stm32_t *stm32) { + if (stm32) { + free(stm32->cmd); // NOLINT + } + free(stm32); // NOLINT + }; + + // Cleanup with RAII + return std::unique_ptr{ptr, CLOSE}; +} + } // Anonymous namespace } // namespace shelly_dimmer @@ -485,48 +497,44 @@ namespace shelly_dimmer { /* find newer command by higher code */ #define newer(prev, a) (((prev) == STM32_CMD_ERR) ? (a) : (((prev) > (a)) ? (prev) : (a))) -stm32_t *stm32_init(uart::UARTDevice *stream, const uint8_t flags, const char init) { +stm32_unique_ptr stm32_init(uart::UARTDevice *stream, const uint8_t flags, const char init) { uint8_t buf[257]; - // Could be constexpr in c++17 - static const auto CLOSE = [](stm32_t *stm32) { stm32_close(stm32); }; - - // Cleanup with RAII - std::unique_ptr stm{static_cast(calloc(sizeof(stm32_t), 1)), // NOLINT - CLOSE}; + auto stm = make_stm32_with_deletor(static_cast(calloc(sizeof(stm32_t), 1))); // NOLINT if (!stm) { - return nullptr; + return make_stm32_with_deletor(nullptr); } stm->stream = stream; stm->flags = flags; stm->cmd = static_cast(malloc(sizeof(stm32_cmd_t))); // NOLINT if (!stm->cmd) { - return nullptr; + return make_stm32_with_deletor(nullptr); } memset(stm->cmd, STM32_CMD_ERR, sizeof(stm32_cmd_t)); if ((stm->flags & STREAM_OPT_CMD_INIT) && init) { - if (stm32_send_init_seq(stm.get()) != STM32_ERR_OK) - return nullptr; // NOLINT + if (stm32_send_init_seq(stm) != STM32_ERR_OK) + return make_stm32_with_deletor(nullptr); } /* get the version and read protection status */ - if (stm32_send_command(stm.get(), STM32_CMD_GVR) != STM32_ERR_OK) { - return nullptr; // NOLINT + if (stm32_send_command(stm, STM32_CMD_GVR) != STM32_ERR_OK) { + return make_stm32_with_deletor(nullptr); } /* From AN, only UART bootloader returns 3 bytes */ { const auto len = (stm->flags & STREAM_OPT_GVR_ETX) ? 3 : 1; if (!stream->read_array(buf, len)) - return nullptr; // NOLINT + return make_stm32_with_deletor(nullptr); + stm->version = buf[0]; stm->option1 = (stm->flags & STREAM_OPT_GVR_ETX) ? buf[1] : 0; stm->option2 = (stm->flags & STREAM_OPT_GVR_ETX) ? buf[2] : 0; - if (stm32_get_ack(stm.get()) != STM32_ERR_OK) { - return nullptr; + if (stm32_get_ack(stm) != STM32_ERR_OK) { + return make_stm32_with_deletor(nullptr); } } @@ -544,8 +552,8 @@ stm32_t *stm32_init(uart::UARTDevice *stream, const uint8_t flags, const char in return STM32_CMD_GET_LENGTH; })(); - if (stm32_guess_len_cmd(stm.get(), STM32_CMD_GET, buf, len) != STM32_ERR_OK) - return nullptr; + if (stm32_guess_len_cmd(stm, STM32_CMD_GET, buf, len) != STM32_ERR_OK) + return make_stm32_with_deletor(nullptr); } const auto stop = buf[0] + 1; @@ -607,23 +615,23 @@ stm32_t *stm32_init(uart::UARTDevice *stream, const uint8_t flags, const char in } if (new_cmds) ESP_LOGD(TAG, ")"); - if (stm32_get_ack(stm.get()) != STM32_ERR_OK) { - return nullptr; + if (stm32_get_ack(stm) != STM32_ERR_OK) { + return make_stm32_with_deletor(nullptr); } if (stm->cmd->get == STM32_CMD_ERR || stm->cmd->gvr == STM32_CMD_ERR || stm->cmd->gid == STM32_CMD_ERR) { ESP_LOGD(TAG, "Error: bootloader did not returned correct information from GET command"); - return nullptr; + return make_stm32_with_deletor(nullptr); } /* get the device ID */ - if (stm32_guess_len_cmd(stm.get(), stm->cmd->gid, buf, 1) != STM32_ERR_OK) { - return nullptr; + if (stm32_guess_len_cmd(stm, stm->cmd->gid, buf, 1) != STM32_ERR_OK) { + return make_stm32_with_deletor(nullptr); } const auto returned = buf[0] + 1; if (returned < 2) { ESP_LOGD(TAG, "Only %d bytes sent in the PID, unknown/unsupported device", returned); - return nullptr; + return make_stm32_with_deletor(nullptr); } stm->pid = (buf[1] << 8) | buf[2]; if (returned > 2) { @@ -631,8 +639,8 @@ stm32_t *stm32_init(uart::UARTDevice *stream, const uint8_t flags, const char in for (auto i = 2; i <= returned; i++) ESP_LOGD(TAG, " %02x", buf[i]); } - if (stm32_get_ack(stm.get()) != STM32_ERR_OK) { - return nullptr; + if (stm32_get_ack(stm) != STM32_ERR_OK) { + return make_stm32_with_deletor(nullptr); } stm->dev = DEVICES; @@ -641,21 +649,14 @@ stm32_t *stm32_init(uart::UARTDevice *stream, const uint8_t flags, const char in if (!stm->dev->id) { ESP_LOGD(TAG, "Unknown/unsupported device (Device ID: 0x%03x)", stm->pid); - return nullptr; + return make_stm32_with_deletor(nullptr); } - // TODO: Would be much better if the unique_ptr was returned from this function - // Release ownership of unique_ptr - return stm.release(); // NOLINT + return stm; } -void stm32_close(stm32_t *stm) { - if (stm) - free(stm->cmd); // NOLINT - free(stm); // NOLINT -} - -stm32_err_t stm32_read_memory(const stm32_t *stm, const uint32_t address, uint8_t *data, const unsigned int len) { +stm32_err_t stm32_read_memory(const stm32_unique_ptr &stm, const uint32_t address, uint8_t *data, + const unsigned int len) { auto *const stream = stm->stream; if (!len) @@ -693,7 +694,8 @@ stm32_err_t stm32_read_memory(const stm32_t *stm, const uint32_t address, uint8_ return STM32_ERR_OK; } -stm32_err_t stm32_write_memory(const stm32_t *stm, uint32_t address, const uint8_t *data, const unsigned int len) { +stm32_err_t stm32_write_memory(const stm32_unique_ptr &stm, uint32_t address, const uint8_t *data, + const unsigned int len) { auto *const stream = stm->stream; if (!len) @@ -753,7 +755,7 @@ stm32_err_t stm32_write_memory(const stm32_t *stm, uint32_t address, const uint8 return STM32_ERR_OK; } -stm32_err_t stm32_wunprot_memory(const stm32_t *stm) { +stm32_err_t stm32_wunprot_memory(const stm32_unique_ptr &stm) { if (stm->cmd->uw == STM32_CMD_ERR) { ESP_LOGD(TAG, "Error: WRITE UNPROTECT command not implemented in bootloader."); return STM32_ERR_NO_CMD; @@ -766,7 +768,7 @@ stm32_err_t stm32_wunprot_memory(const stm32_t *stm) { []() { ESP_LOGD(TAG, "Error: Failed to WRITE UNPROTECT"); }); } -stm32_err_t stm32_wprot_memory(const stm32_t *stm) { +stm32_err_t stm32_wprot_memory(const stm32_unique_ptr &stm) { if (stm->cmd->wp == STM32_CMD_ERR) { ESP_LOGD(TAG, "Error: WRITE PROTECT command not implemented in bootloader."); return STM32_ERR_NO_CMD; @@ -779,7 +781,7 @@ stm32_err_t stm32_wprot_memory(const stm32_t *stm) { []() { ESP_LOGD(TAG, "Error: Failed to WRITE PROTECT"); }); } -stm32_err_t stm32_runprot_memory(const stm32_t *stm) { +stm32_err_t stm32_runprot_memory(const stm32_unique_ptr &stm) { if (stm->cmd->ur == STM32_CMD_ERR) { ESP_LOGD(TAG, "Error: READOUT UNPROTECT command not implemented in bootloader."); return STM32_ERR_NO_CMD; @@ -792,7 +794,7 @@ stm32_err_t stm32_runprot_memory(const stm32_t *stm) { []() { ESP_LOGD(TAG, "Error: Failed to READOUT UNPROTECT"); }); } -stm32_err_t stm32_readprot_memory(const stm32_t *stm) { +stm32_err_t stm32_readprot_memory(const stm32_unique_ptr &stm) { if (stm->cmd->rp == STM32_CMD_ERR) { ESP_LOGD(TAG, "Error: READOUT PROTECT command not implemented in bootloader."); return STM32_ERR_NO_CMD; @@ -805,7 +807,7 @@ stm32_err_t stm32_readprot_memory(const stm32_t *stm) { []() { ESP_LOGD(TAG, "Error: Failed to READOUT PROTECT"); }); } -stm32_err_t stm32_erase_memory(const stm32_t *stm, uint32_t spage, uint32_t pages) { +stm32_err_t stm32_erase_memory(const stm32_unique_ptr &stm, uint32_t spage, uint32_t pages) { if (!pages || spage > STM32_MAX_PAGES || ((pages != STM32_MASS_ERASE) && ((spage + pages) > STM32_MAX_PAGES))) return STM32_ERR_OK; @@ -847,7 +849,7 @@ stm32_err_t stm32_erase_memory(const stm32_t *stm, uint32_t spage, uint32_t page return STM32_ERR_OK; } -static stm32_err_t stm32_run_raw_code(const stm32_t *stm, uint32_t target_address, const uint8_t *code, +static stm32_err_t stm32_run_raw_code(const stm32_unique_ptr &stm, uint32_t target_address, const uint8_t *code, uint32_t code_size) { static constexpr uint32_t BUFFER_SIZE = 256; @@ -893,7 +895,7 @@ static stm32_err_t stm32_run_raw_code(const stm32_t *stm, uint32_t target_addres return stm32_go(stm, target_address); } -stm32_err_t stm32_go(const stm32_t *stm, const uint32_t address) { +stm32_err_t stm32_go(const stm32_unique_ptr &stm, const uint32_t address) { auto *const stream = stm->stream; if (stm->cmd->go == STM32_CMD_ERR) { @@ -916,7 +918,7 @@ stm32_err_t stm32_go(const stm32_t *stm, const uint32_t address) { return STM32_ERR_OK; } -stm32_err_t stm32_reset_device(const stm32_t *stm) { +stm32_err_t stm32_reset_device(const stm32_unique_ptr &stm) { const auto target_address = stm->dev->ram_start; if (stm->dev->flags & F_OBLL) { @@ -927,7 +929,8 @@ stm32_err_t stm32_reset_device(const stm32_t *stm) { } } -stm32_err_t stm32_crc_memory(const stm32_t *stm, const uint32_t address, const uint32_t length, uint32_t *const crc) { +stm32_err_t stm32_crc_memory(const stm32_unique_ptr &stm, const uint32_t address, const uint32_t length, + uint32_t *const crc) { static constexpr auto BUFFER_SIZE = 5; auto *const stream = stm->stream; @@ -1022,7 +1025,7 @@ uint32_t stm32_sw_crc(uint32_t crc, uint8_t *buf, unsigned int len) { return crc; } -stm32_err_t stm32_crc_wrapper(const stm32_t *stm, uint32_t address, uint32_t length, uint32_t *crc) { +stm32_err_t stm32_crc_wrapper(const stm32_unique_ptr &stm, uint32_t address, uint32_t length, uint32_t *crc) { static constexpr uint32_t CRC_INIT_VALUE = 0xFFFFFFFF; static constexpr uint32_t BUFFER_SIZE = 256; diff --git a/esphome/components/shelly_dimmer/stm32flash.h b/esphome/components/shelly_dimmer/stm32flash.h index c561375c38..d973b35222 100644 --- a/esphome/components/shelly_dimmer/stm32flash.h +++ b/esphome/components/shelly_dimmer/stm32flash.h @@ -23,6 +23,7 @@ #ifdef USE_SHD_FIRMWARE_DATA #include +#include #include "esphome/components/uart/uart.h" namespace esphome { @@ -108,19 +109,20 @@ struct VarlenCmd { uint8_t length; }; -stm32_t *stm32_init(uart::UARTDevice *stream, uint8_t flags, char init); -void stm32_close(stm32_t *stm); -stm32_err_t stm32_read_memory(const stm32_t *stm, uint32_t address, uint8_t *data, unsigned int len); -stm32_err_t stm32_write_memory(const stm32_t *stm, uint32_t address, const uint8_t *data, unsigned int len); -stm32_err_t stm32_wunprot_memory(const stm32_t *stm); -stm32_err_t stm32_wprot_memory(const stm32_t *stm); -stm32_err_t stm32_erase_memory(const stm32_t *stm, uint32_t spage, uint32_t pages); -stm32_err_t stm32_go(const stm32_t *stm, uint32_t address); -stm32_err_t stm32_reset_device(const stm32_t *stm); -stm32_err_t stm32_readprot_memory(const stm32_t *stm); -stm32_err_t stm32_runprot_memory(const stm32_t *stm); -stm32_err_t stm32_crc_memory(const stm32_t *stm, uint32_t address, uint32_t length, uint32_t *crc); -stm32_err_t stm32_crc_wrapper(const stm32_t *stm, uint32_t address, uint32_t length, uint32_t *crc); +using stm32_unique_ptr = std::unique_ptr; + +stm32_unique_ptr stm32_init(uart::UARTDevice *stream, uint8_t flags, char init); +stm32_err_t stm32_read_memory(const stm32_unique_ptr &stm, uint32_t address, uint8_t *data, unsigned int len); +stm32_err_t stm32_write_memory(const stm32_unique_ptr &stm, uint32_t address, const uint8_t *data, unsigned int len); +stm32_err_t stm32_wunprot_memory(const stm32_unique_ptr &stm); +stm32_err_t stm32_wprot_memory(const stm32_unique_ptr &stm); +stm32_err_t stm32_erase_memory(const stm32_unique_ptr &stm, uint32_t spage, uint32_t pages); +stm32_err_t stm32_go(const stm32_unique_ptr &stm, uint32_t address); +stm32_err_t stm32_reset_device(const stm32_unique_ptr &stm); +stm32_err_t stm32_readprot_memory(const stm32_unique_ptr &stm); +stm32_err_t stm32_runprot_memory(const stm32_unique_ptr &stm); +stm32_err_t stm32_crc_memory(const stm32_unique_ptr &stm, uint32_t address, uint32_t length, uint32_t *crc); +stm32_err_t stm32_crc_wrapper(const stm32_unique_ptr &stm, uint32_t address, uint32_t length, uint32_t *crc); uint32_t stm32_sw_crc(uint32_t crc, uint8_t *buf, unsigned int len); } // namespace shelly_dimmer From 01c4d3c225fca32bc62a39ec7126637262e8957d Mon Sep 17 00:00:00 2001 From: James Szalay Date: Wed, 11 May 2022 23:26:14 -0400 Subject: [PATCH 04/60] Use heat mode for heat. Move EXT HT to custom presets. (#3437) * Use heat mode for heat. Move EXT HT to custom presets. * Fix syntax error. --- esphome/components/bedjet/bedjet.cpp | 6 ++++-- esphome/components/bedjet/bedjet.h | 2 ++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/esphome/components/bedjet/bedjet.cpp b/esphome/components/bedjet/bedjet.cpp index 1a932da0c5..38ed6206a8 100644 --- a/esphome/components/bedjet/bedjet.cpp +++ b/esphome/components/bedjet/bedjet.cpp @@ -117,7 +117,7 @@ void Bedjet::control(const ClimateCall &call) { pkt = this->codec_->get_button_request(BTN_OFF); break; case climate::CLIMATE_MODE_HEAT: - pkt = this->codec_->get_button_request(BTN_EXTHT); + pkt = this->codec_->get_button_request(BTN_HEAT); break; case climate::CLIMATE_MODE_FAN_ONLY: pkt = this->codec_->get_button_request(BTN_COOL); @@ -137,7 +137,7 @@ void Bedjet::control(const ClimateCall &call) { } else { this->force_refresh_ = true; this->mode = mode; - // We're using (custom) preset for Turbo & M1-3 presets, so changing climate mode will clear those + // We're using (custom) preset for Turbo, EXT HT, & M1-3 presets, so changing climate mode will clear those this->custom_preset.reset(); this->preset.reset(); } @@ -186,6 +186,8 @@ void Bedjet::control(const ClimateCall &call) { pkt = this->codec_->get_button_request(BTN_M2); } else if (preset == "M3") { pkt = this->codec_->get_button_request(BTN_M3); + } else if (preset == "EXT HT") { + pkt = this->codec_->get_button_request(BTN_EXTHT); } else { ESP_LOGW(TAG, "Unsupported preset: %s", preset.c_str()); return; diff --git a/esphome/components/bedjet/bedjet.h b/esphome/components/bedjet/bedjet.h index b061d2b5ec..0565be6045 100644 --- a/esphome/components/bedjet/bedjet.h +++ b/esphome/components/bedjet/bedjet.h @@ -67,6 +67,8 @@ class Bedjet : public climate::Climate, public esphome::ble_client::BLEClientNod // We could fetch biodata from bedjet and set these names that way. // But then we have to invert the lookup in order to send the right preset. // For now, we can leave them as M1-3 to match the remote buttons. + // EXT HT added to match remote button. + "EXT HT", "M1", "M2", "M3", From bcb47c306c996340fd19a2e66cbca5985fe059e2 Mon Sep 17 00:00:00 2001 From: swifty99 Date: Thu, 12 May 2022 06:53:33 +0200 Subject: [PATCH 05/60] Tcs34725 automatic sampling settings for improved dynamics and accuracy (#3258) Co-authored-by: Daniel Cousens <413395+dcousens@users.noreply.github.com> Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/tcs34725/sensor.py | 3 +- esphome/components/tcs34725/tcs34725.cpp | 109 ++++++++++++++++++++--- esphome/components/tcs34725/tcs34725.h | 4 +- 3 files changed, 103 insertions(+), 13 deletions(-) diff --git a/esphome/components/tcs34725/sensor.py b/esphome/components/tcs34725/sensor.py index fcc56e395f..d47e9a34c8 100644 --- a/esphome/components/tcs34725/sensor.py +++ b/esphome/components/tcs34725/sensor.py @@ -31,6 +31,7 @@ TCS34725Component = tcs34725_ns.class_( TCS34725IntegrationTime = tcs34725_ns.enum("TCS34725IntegrationTime") TCS34725_INTEGRATION_TIMES = { + "auto": TCS34725IntegrationTime.TCS34725_INTEGRATION_TIME_AUTO, "2.4ms": TCS34725IntegrationTime.TCS34725_INTEGRATION_TIME_2_4MS, "24ms": TCS34725IntegrationTime.TCS34725_INTEGRATION_TIME_24MS, "50ms": TCS34725IntegrationTime.TCS34725_INTEGRATION_TIME_50MS, @@ -88,7 +89,7 @@ CONFIG_SCHEMA = ( cv.Optional(CONF_CLEAR_CHANNEL): color_channel_schema, cv.Optional(CONF_ILLUMINANCE): illuminance_schema, cv.Optional(CONF_COLOR_TEMPERATURE): color_temperature_schema, - cv.Optional(CONF_INTEGRATION_TIME, default="2.4ms"): cv.enum( + cv.Optional(CONF_INTEGRATION_TIME, default="auto"): cv.enum( TCS34725_INTEGRATION_TIMES, lower=True ), cv.Optional(CONF_GAIN, default="1X"): cv.enum(TCS34725_GAINS, upper=True), diff --git a/esphome/components/tcs34725/tcs34725.cpp b/esphome/components/tcs34725/tcs34725.cpp index 825f7da4cc..276bf65ebf 100644 --- a/esphome/components/tcs34725/tcs34725.cpp +++ b/esphome/components/tcs34725/tcs34725.cpp @@ -136,8 +136,14 @@ void TCS34725Component::calculate_temperature_and_lux_(uint16_t r, uint16_t g, u } /* Check for saturation and mark the sample as invalid if true */ if (c >= sat) { - ESP_LOGW(TAG, "Saturation too high, discarding sample with saturation %.1f and clear %d", sat, c); - return; + if (this->integration_time_auto_) { + ESP_LOGI(TAG, "Saturation too high, sample discarded, autogain ongoing"); + } else { + ESP_LOGW( + TAG, + "Saturation too high, sample with saturation %.1f and clear %d treat values carefully or use grey filter", + sat, c); + } } /* AMS RGB sensors have no IR channel, so the IR content must be */ @@ -149,8 +155,14 @@ void TCS34725Component::calculate_temperature_and_lux_(uint16_t r, uint16_t g, u g2 = g - ir; b2 = b - ir; + // discarding super low values? not recemmonded, and avoided by using auto gain. if (r2 == 0) { - return; + // legacy code + if (!this->integration_time_auto_) { + ESP_LOGW(TAG, + "No light detected on red channel, switch to auto gain or adjust timing, values will be unreliable"); + return; + } } // Lux Calculation (DN40 3.2) @@ -189,7 +201,7 @@ void TCS34725Component::update() { this->status_set_warning(); return; } - ESP_LOGV(TAG, "Raw values clear=%x red=%x green=%x blue=%x", raw_c, raw_r, raw_g, raw_b); + ESP_LOGV(TAG, "Raw values clear=%d red=%d green=%d blue=%d", raw_c, raw_r, raw_g, raw_b); float channel_c; float channel_r; @@ -220,20 +232,95 @@ void TCS34725Component::update() { calculate_temperature_and_lux_(raw_r, raw_g, raw_b, raw_c); } - if (this->illuminance_sensor_ != nullptr) - this->illuminance_sensor_->publish_state(this->illuminance_); + // do not publish values if auto gain finding ongoing, and oversaturated + // so: publish when: + // - not auto mode + // - clear not oversaturated + // - clear oversaturated but gain and timing cannot go lower + if (!this->integration_time_auto_ || raw_c < 65530 || (this->gain_reg_ == 0 && this->integration_time_ < 200)) { + if (this->illuminance_sensor_ != nullptr) + this->illuminance_sensor_->publish_state(this->illuminance_); - if (this->color_temperature_sensor_ != nullptr) - this->color_temperature_sensor_->publish_state(this->color_temperature_); + if (this->color_temperature_sensor_ != nullptr) + this->color_temperature_sensor_->publish_state(this->color_temperature_); + } - ESP_LOGD(TAG, "Got Red=%.1f%%,Green=%.1f%%,Blue=%.1f%%,Clear=%.1f%% Illuminance=%.1flx Color Temperature=%.1fK", + ESP_LOGD(TAG, + "Got Red=%.1f%%,Green=%.1f%%,Blue=%.1f%%,Clear=%.1f%% Illuminance=%.1flx Color " + "Temperature=%.1fK", channel_r, channel_g, channel_b, channel_c, this->illuminance_, this->color_temperature_); + if (this->integration_time_auto_) { + // change integration time an gain to achieve maximum resolution an dynamic range + // calculate optimal integration time to achieve 70% satuaration + float integration_time_ideal; + integration_time_ideal = 60 / ((float) raw_c / 655.35) * this->integration_time_; + + uint8_t gain_reg_val_new = this->gain_reg_; + // increase gain if less than 20% of white channel used and high integration time + // increase only if not already maximum + // do not use max gain, as ist will not get better + if (this->gain_reg_ < 3) { + if (((float) raw_c / 655.35 < 20.f) && (this->integration_time_ > 600.f)) { + gain_reg_val_new = this->gain_reg_ + 1; + // update integration time to new situation + integration_time_ideal = integration_time_ideal / 4; + } + } + + // decrease gain, if very high clear values and integration times alreadey low + if (this->gain_reg_ > 0) { + if (70 < ((float) raw_c / 655.35) && (this->integration_time_ < 200)) { + gain_reg_val_new = this->gain_reg_ - 1; + // update integration time to new situation + integration_time_ideal = integration_time_ideal * 4; + } + } + + // saturate integration times + float integration_time_next = integration_time_ideal; + if (integration_time_ideal > 2.4f * 256) { + integration_time_next = 2.4f * 256; + } + if (integration_time_ideal < 154) { + integration_time_next = 154; + } + + // calculate register value from timing + uint8_t regval_atime = (uint8_t)(256.f - integration_time_next / 2.4f); + ESP_LOGD(TAG, "Integration time: %.1fms, ideal: %.1fms regval_new %d Gain: %.f Clear channel raw: %d gain reg: %d", + this->integration_time_, integration_time_next, regval_atime, this->gain_, raw_c, this->gain_reg_); + + if (this->integration_reg_ != regval_atime || gain_reg_val_new != this->gain_reg_) { + this->integration_reg_ = regval_atime; + this->gain_reg_ = gain_reg_val_new; + set_gain((TCS34725Gain) gain_reg_val_new); + if (this->write_config_register_(TCS34725_REGISTER_ATIME, this->integration_reg_) != i2c::ERROR_OK || + this->write_config_register_(TCS34725_REGISTER_CONTROL, this->gain_reg_) != i2c::ERROR_OK) { + this->mark_failed(); + ESP_LOGW(TAG, "TCS34725I update timing failed!"); + } else { + this->integration_time_ = integration_time_next; + } + } + } this->status_clear_warning(); } void TCS34725Component::set_integration_time(TCS34725IntegrationTime integration_time) { - this->integration_reg_ = integration_time; - this->integration_time_ = (256.f - integration_time) * 2.4f; + // if an integration time is 0x100, this is auto start with 154ms as this gives best starting point + TCS34725IntegrationTime my_integration_time_regval; + + if (integration_time == TCS34725_INTEGRATION_TIME_AUTO) { + this->integration_time_auto_ = true; + this->integration_reg_ = TCS34725_INTEGRATION_TIME_154MS; + my_integration_time_regval = TCS34725_INTEGRATION_TIME_154MS; + } else { + this->integration_reg_ = integration_time; + my_integration_time_regval = integration_time; + this->integration_time_auto_ = false; + } + this->integration_time_ = (256.f - my_integration_time_regval) * 2.4f; + ESP_LOGI(TAG, "TCS34725I Integration time set to: %.1fms", this->integration_time_); } void TCS34725Component::set_gain(TCS34725Gain gain) { this->gain_reg_ = gain; diff --git a/esphome/components/tcs34725/tcs34725.h b/esphome/components/tcs34725/tcs34725.h index 04565d948e..23985e8221 100644 --- a/esphome/components/tcs34725/tcs34725.h +++ b/esphome/components/tcs34725/tcs34725.h @@ -26,6 +26,7 @@ enum TCS34725IntegrationTime { TCS34725_INTEGRATION_TIME_540MS = 0x1F, TCS34725_INTEGRATION_TIME_600MS = 0x06, TCS34725_INTEGRATION_TIME_614MS = 0x00, + TCS34725_INTEGRATION_TIME_AUTO = 0x100, }; enum TCS34725Gain { @@ -77,10 +78,11 @@ class TCS34725Component : public PollingComponent, public i2c::I2CDevice { float glass_attenuation_{1.0}; float illuminance_; float color_temperature_; + bool integration_time_auto_{true}; private: void calculate_temperature_and_lux_(uint16_t r, uint16_t g, uint16_t b, uint16_t c); - uint8_t integration_reg_{TCS34725_INTEGRATION_TIME_2_4MS}; + uint16_t integration_reg_; uint8_t gain_reg_{TCS34725_GAIN_1X}; }; From 1c873e003431562737d45271ab2fc5fe3409f5b8 Mon Sep 17 00:00:00 2001 From: Michael Davidson Date: Thu, 12 May 2022 14:54:45 +1000 Subject: [PATCH 06/60] Make custom_fan and custom_preset templatable as per documentation (#3330) --- esphome/components/climate/__init__.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/esphome/components/climate/__init__.py b/esphome/components/climate/__init__.py index 87b9a4b3e2..1de9aa3f3a 100644 --- a/esphome/components/climate/__init__.py +++ b/esphome/components/climate/__init__.py @@ -287,9 +287,11 @@ CLIMATE_CONTROL_ACTION_SCHEMA = cv.Schema( cv.Exclusive(CONF_FAN_MODE, "fan_mode"): cv.templatable( validate_climate_fan_mode ), - cv.Exclusive(CONF_CUSTOM_FAN_MODE, "fan_mode"): cv.string_strict, + cv.Exclusive(CONF_CUSTOM_FAN_MODE, "fan_mode"): cv.templatable( + cv.string_strict + ), cv.Exclusive(CONF_PRESET, "preset"): cv.templatable(validate_climate_preset), - cv.Exclusive(CONF_CUSTOM_PRESET, "preset"): cv.string_strict, + cv.Exclusive(CONF_CUSTOM_PRESET, "preset"): cv.templatable(cv.string_strict), cv.Optional(CONF_SWING_MODE): cv.templatable(validate_climate_swing_mode), } ) @@ -324,13 +326,17 @@ async def climate_control_to_code(config, action_id, template_arg, args): template_ = await cg.templatable(config[CONF_FAN_MODE], args, ClimateFanMode) cg.add(var.set_fan_mode(template_)) if CONF_CUSTOM_FAN_MODE in config: - template_ = await cg.templatable(config[CONF_CUSTOM_FAN_MODE], args, str) + template_ = await cg.templatable( + config[CONF_CUSTOM_FAN_MODE], args, cg.std_string + ) cg.add(var.set_custom_fan_mode(template_)) if CONF_PRESET in config: template_ = await cg.templatable(config[CONF_PRESET], args, ClimatePreset) cg.add(var.set_preset(template_)) if CONF_CUSTOM_PRESET in config: - template_ = await cg.templatable(config[CONF_CUSTOM_PRESET], args, str) + template_ = await cg.templatable( + config[CONF_CUSTOM_PRESET], args, cg.std_string + ) cg.add(var.set_custom_preset(template_)) if CONF_SWING_MODE in config: template_ = await cg.templatable( From 03d5a0ec1d1d251db9ae8871a80e6960591827fc Mon Sep 17 00:00:00 2001 From: Brian Kaufman Date: Wed, 11 May 2022 21:57:50 -0700 Subject: [PATCH 07/60] Update captive portal canHandle function (#3360) --- esphome/components/captive_portal/captive_portal.h | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/esphome/components/captive_portal/captive_portal.h b/esphome/components/captive_portal/captive_portal.h index 0e68bc9cef..c2aada171f 100644 --- a/esphome/components/captive_portal/captive_portal.h +++ b/esphome/components/captive_portal/captive_portal.h @@ -39,17 +39,7 @@ class CaptivePortal : public AsyncWebHandler, public Component { if (request->method() == HTTP_GET) { if (request->url() == "/") return true; - if (request->url() == "/stylesheet.css") - return true; - if (request->url() == "/wifi-strength-1.svg") - return true; - if (request->url() == "/wifi-strength-2.svg") - return true; - if (request->url() == "/wifi-strength-3.svg") - return true; - if (request->url() == "/wifi-strength-4.svg") - return true; - if (request->url() == "/lock.svg") + if (request->url() == "/config.json") return true; if (request->url() == "/wifisave") return true; From 39c6c2417ac2b3b20f4dbc2457aa380b330a7fb9 Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Thu, 12 May 2022 11:18:51 +0100 Subject: [PATCH 08/60] Remove duplicate convert_to_8bit_color_ function. (#2469) Co-authored-by: Oxan van Leeuwen --- .../components/display/display_color_utils.h | 4 ++- .../components/ili9341/ili9341_display.cpp | 30 ++++--------------- esphome/components/ili9341/ili9341_display.h | 2 -- 3 files changed, 8 insertions(+), 28 deletions(-) diff --git a/esphome/components/display/display_color_utils.h b/esphome/components/display/display_color_utils.h index 202de912de..7f29586932 100644 --- a/esphome/components/display/display_color_utils.h +++ b/esphome/components/display/display_color_utils.h @@ -66,6 +66,9 @@ class ColorUtil { } return color_return; } + static inline Color rgb332_to_color(uint8_t rgb332_color) { + return to_color((uint32_t) rgb332_color, COLOR_ORDER_RGB, COLOR_BITNESS_332); + } static uint8_t color_to_332(Color color, ColorOrder color_order = ColorOrder::COLOR_ORDER_RGB) { uint16_t red_color, green_color, blue_color; @@ -100,7 +103,6 @@ class ColorUtil { } return 0; } - static uint32_t color_to_grayscale4(Color color) { uint32_t gs4 = esp_scale8(color.white, 15); return gs4; diff --git a/esphome/components/ili9341/ili9341_display.cpp b/esphome/components/ili9341/ili9341_display.cpp index a24f0bbb64..09524ba787 100644 --- a/esphome/components/ili9341/ili9341_display.cpp +++ b/esphome/components/ili9341/ili9341_display.cpp @@ -112,29 +112,9 @@ void ILI9341Display::display_() { this->y_high_ = 0; } -uint16_t ILI9341Display::convert_to_16bit_color_(uint8_t color_8bit) { - int r = color_8bit >> 5; - int g = (color_8bit >> 2) & 0x07; - int b = color_8bit & 0x03; - uint16_t color = (r * 0x04) << 11; - color |= (g * 0x09) << 5; - color |= (b * 0x0A); - - return color; -} - -uint8_t ILI9341Display::convert_to_8bit_color_(uint16_t color_16bit) { - // convert 16bit color to 8 bit buffer - uint8_t r = color_16bit >> 11; - uint8_t g = (color_16bit >> 5) & 0x3F; - uint8_t b = color_16bit & 0x1F; - - return ((b / 0x0A) | ((g / 0x09) << 2) | ((r / 0x04) << 5)); -} - void ILI9341Display::fill(Color color) { - auto color565 = display::ColorUtil::color_to_565(color); - memset(this->buffer_, convert_to_8bit_color_(color565), this->get_buffer_length_()); + uint8_t color332 = display::ColorUtil::color_to_332(color, display::ColorOrder::COLOR_ORDER_RGB); + memset(this->buffer_, color332, this->get_buffer_length_()); this->x_low_ = 0; this->y_low_ = 0; this->x_high_ = this->get_width_internal() - 1; @@ -181,8 +161,8 @@ void HOT ILI9341Display::draw_absolute_pixel_internal(int x, int y, Color color) this->y_high_ = (y > this->y_high_) ? y : this->y_high_; uint32_t pos = (y * width_) + x; - auto color565 = display::ColorUtil::color_to_565(color); - buffer_[pos] = convert_to_8bit_color_(color565); + uint8_t color332 = display::ColorUtil::color_to_332(color, display::ColorOrder::COLOR_ORDER_RGB); + buffer_[pos] = color332; } // should return the total size: return this->get_width_internal() * this->get_height_internal() * 2 // 16bit color @@ -247,7 +227,7 @@ uint32_t ILI9341Display::buffer_to_transfer_(uint32_t pos, uint32_t sz) { } for (uint32_t i = 0; i < sz; ++i) { - uint16_t color = convert_to_16bit_color_(*src++); + uint16_t color = display::ColorUtil::color_to_565(display::ColorUtil::rgb332_to_color(*src++)); *dst++ = (uint8_t)(color >> 8); *dst++ = (uint8_t) color; } diff --git a/esphome/components/ili9341/ili9341_display.h b/esphome/components/ili9341/ili9341_display.h index d8c90c9d33..eeff688f4f 100644 --- a/esphome/components/ili9341/ili9341_display.h +++ b/esphome/components/ili9341/ili9341_display.h @@ -51,8 +51,6 @@ class ILI9341Display : public PollingComponent, void reset_(); void fill_internal_(Color color); void display_(); - uint16_t convert_to_16bit_color_(uint8_t color_8bit); - uint8_t convert_to_8bit_color_(uint16_t color_16bit); ILI9341Model model_; int16_t width_{320}; ///< Display width as modified by current rotation From 2dc2aec954b6c5dbc65d5898bc65dcce2efd0e1a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 13 May 2022 13:44:24 +1200 Subject: [PATCH 09/60] Bump esptool from 3.3 to 3.3.1 (#3468) 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 e62ef86765..dfe69cd33a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,7 @@ tzlocal==4.2 # from time tzdata>=2021.1 # from time pyserial==3.5 platformio==5.2.5 # When updating platformio, also update Dockerfile -esptool==3.3 +esptool==3.3.1 click==8.1.3 esphome-dashboard==20220508.0 aioesphomeapi==10.8.2 From 7a03c7d56fd0b4075c6d3494e404fb4201452872 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 15 May 2022 19:46:36 +1200 Subject: [PATCH 10/60] Bump pylint from 2.13.8 to 2.13.9 (#3470) Bumps [pylint](https://github.com/PyCQA/pylint) from 2.13.8 to 2.13.9. - [Release notes](https://github.com/PyCQA/pylint/releases) - [Changelog](https://github.com/PyCQA/pylint/blob/main/ChangeLog) - [Commits](https://github.com/PyCQA/pylint/compare/v2.13.8...v2.13.9) --- updated-dependencies: - dependency-name: pylint dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] 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 4b5db8ce87..7c21979647 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,4 +1,4 @@ -pylint==2.13.8 +pylint==2.13.9 flake8==4.0.1 black==22.3.0 pyupgrade==2.32.1 From fea05e9d33f13a5435f035bcf10d0ea0ef23abc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=5Bp=CA=B2=C9=B5s=5D?= Date: Sun, 15 May 2022 09:53:43 +0200 Subject: [PATCH 11/60] Increase JSON buffer size on overflow (#3475) --- esphome/components/json/json_util.cpp | 40 +++++++++++++++++---------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/esphome/components/json/json_util.cpp b/esphome/components/json/json_util.cpp index 2bd8112255..7e701af48b 100644 --- a/esphome/components/json/json_util.cpp +++ b/esphome/components/json/json_util.cpp @@ -26,21 +26,33 @@ std::string build_json(const json_build_t &f) { const size_t free_heap = heap_caps_get_largest_free_block(MALLOC_CAP_8BIT); #endif - const size_t request_size = std::min(free_heap, (size_t) 512); - - DynamicJsonDocument json_document(request_size); - if (json_document.capacity() == 0) { - ESP_LOGE(TAG, "Could not allocate memory for JSON document! Requested %u bytes, largest free heap block: %u bytes", - request_size, free_heap); - return "{}"; + size_t request_size = std::min(free_heap, (size_t) 512); + while (true) { + ESP_LOGV(TAG, "Attempting to allocate %u bytes for JSON serialization", request_size); + DynamicJsonDocument json_document(request_size); + if (json_document.capacity() == 0) { + ESP_LOGE(TAG, + "Could not allocate memory for JSON document! Requested %u bytes, largest free heap block: %u bytes", + request_size, free_heap); + return "{}"; + } + JsonObject root = json_document.to(); + f(root); + if (json_document.overflowed()) { + if (request_size == free_heap) { + ESP_LOGE(TAG, "Could not allocate memory for JSON document! Overflowed largest free heap block: %u bytes", + free_heap); + return "{}"; + } + request_size = std::min(request_size * 2, free_heap); + continue; + } + json_document.shrinkToFit(); + ESP_LOGV(TAG, "Size after shrink %u bytes", json_document.capacity()); + std::string output; + serializeJson(json_document, output); + return output; } - JsonObject root = json_document.to(); - f(root); - json_document.shrinkToFit(); - ESP_LOGV(TAG, "Size after shrink %u bytes", json_document.capacity()); - std::string output; - serializeJson(json_document, output); - return output; } void parse_json(const std::string &data, const json_parse_t &f) { From 0665acd1901f8ea1b644d89ab9933d032b288ba7 Mon Sep 17 00:00:00 2001 From: Maxim Ocheretianko Date: Sun, 15 May 2022 22:44:14 +0300 Subject: [PATCH 12/60] Tuya status gpio support (#3466) --- esphome/components/tuya/__init__.py | 6 +++++ esphome/components/tuya/tuya.cpp | 41 ++++++++++++++++++++++------- esphome/components/tuya/tuya.h | 7 +++-- tests/test4.yaml | 3 +++ 4 files changed, 45 insertions(+), 12 deletions(-) diff --git a/esphome/components/tuya/__init__.py b/esphome/components/tuya/__init__.py index 965893e012..2eaaa2a625 100644 --- a/esphome/components/tuya/__init__.py +++ b/esphome/components/tuya/__init__.py @@ -1,5 +1,6 @@ from esphome.components import time from esphome import automation +from esphome import pins import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import uart @@ -11,6 +12,7 @@ CONF_IGNORE_MCU_UPDATE_ON_DATAPOINTS = "ignore_mcu_update_on_datapoints" CONF_ON_DATAPOINT_UPDATE = "on_datapoint_update" CONF_DATAPOINT_TYPE = "datapoint_type" +CONF_STATUS_PIN = "status_pin" tuya_ns = cg.esphome_ns.namespace("tuya") Tuya = tuya_ns.class_("Tuya", cg.Component, uart.UARTDevice) @@ -88,6 +90,7 @@ CONFIG_SCHEMA = ( cv.Optional(CONF_IGNORE_MCU_UPDATE_ON_DATAPOINTS): cv.ensure_list( cv.uint8_t ), + cv.Optional(CONF_STATUS_PIN): pins.gpio_output_pin_schema, cv.Optional(CONF_ON_DATAPOINT_UPDATE): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( @@ -114,6 +117,9 @@ async def to_code(config): if CONF_TIME_ID in config: time_ = await cg.get_variable(config[CONF_TIME_ID]) cg.add(var.set_time_id(time_)) + if CONF_STATUS_PIN in config: + status_pin_ = await cg.gpio_pin_expression(config[CONF_STATUS_PIN]) + cg.add(var.set_status_pin(status_pin_)) if CONF_IGNORE_MCU_UPDATE_ON_DATAPOINTS in config: for dp in config[CONF_IGNORE_MCU_UPDATE_ON_DATAPOINTS]: cg.add(var.add_ignore_mcu_update_on_datapoints(dp)) diff --git a/esphome/components/tuya/tuya.cpp b/esphome/components/tuya/tuya.cpp index 1fbca7796d..1b35121c57 100644 --- a/esphome/components/tuya/tuya.cpp +++ b/esphome/components/tuya/tuya.cpp @@ -3,6 +3,7 @@ #include "esphome/components/network/util.h" #include "esphome/core/helpers.h" #include "esphome/core/util.h" +#include "esphome/core/gpio.h" namespace esphome { namespace tuya { @@ -13,6 +14,9 @@ static const int RECEIVE_TIMEOUT = 300; void Tuya::setup() { this->set_interval("heartbeat", 15000, [this] { this->send_empty_command_(TuyaCommandType::HEARTBEAT); }); + if (this->status_pin_.has_value()) { + this->status_pin_.value()->digital_write(false); + } } void Tuya::loop() { @@ -49,9 +53,12 @@ void Tuya::dump_config() { ESP_LOGCONFIG(TAG, " Datapoint %u: unknown", info.id); } } - if ((this->gpio_status_ != -1) || (this->gpio_reset_ != -1)) { - ESP_LOGCONFIG(TAG, " GPIO Configuration: status: pin %d, reset: pin %d (not supported)", this->gpio_status_, - this->gpio_reset_); + if ((this->status_pin_reported_ != -1) || (this->reset_pin_reported_ != -1)) { + ESP_LOGCONFIG(TAG, " GPIO Configuration: status: pin %d, reset: pin %d (not supported)", + this->status_pin_reported_, this->reset_pin_reported_); + } + if (this->status_pin_.has_value()) { + LOG_PIN(" Status Pin: ", this->status_pin_.value()); } ESP_LOGCONFIG(TAG, " Product: '%s'", this->product_.c_str()); this->check_uart_settings(9600); @@ -164,16 +171,27 @@ void Tuya::handle_command_(uint8_t command, uint8_t version, const uint8_t *buff } case TuyaCommandType::CONF_QUERY: { if (len >= 2) { - this->gpio_status_ = buffer[0]; - this->gpio_reset_ = buffer[1]; + this->status_pin_reported_ = buffer[0]; + this->reset_pin_reported_ = buffer[1]; } if (this->init_state_ == TuyaInitState::INIT_CONF) { // If mcu returned status gpio, then we can omit sending wifi state - if (this->gpio_status_ != -1) { + if (this->status_pin_reported_ != -1) { this->init_state_ = TuyaInitState::INIT_DATAPOINT; this->send_empty_command_(TuyaCommandType::DATAPOINT_QUERY); + bool is_pin_equals = + this->status_pin_.has_value() && this->status_pin_.value()->get_pin() == this->status_pin_reported_; + // Configure status pin toggling (if reported and configured) or WIFI_STATE periodic send + if (is_pin_equals) { + ESP_LOGV(TAG, "Configured status pin %i", this->status_pin_reported_); + this->set_interval("wifi", 1000, [this] { this->set_status_pin_(); }); + } else { + ESP_LOGW(TAG, "Supplied status_pin does not equals the reported pin %i. TuyaMcu will work in limited mode.", + this->status_pin_reported_); + } } else { this->init_state_ = TuyaInitState::INIT_WIFI; + ESP_LOGV(TAG, "Configured WIFI_STATE periodic send"); this->set_interval("wifi", 1000, [this] { this->send_wifi_status_(); }); } } @@ -397,16 +415,19 @@ void Tuya::send_empty_command_(TuyaCommandType command) { send_command_(TuyaCommand{.cmd = command, .payload = std::vector{}}); } +void Tuya::set_status_pin_() { + bool is_network_ready = network::is_connected() && remote_is_connected(); + this->status_pin_.value()->digital_write(is_network_ready); +} + void Tuya::send_wifi_status_() { uint8_t status = 0x02; if (network::is_connected()) { status = 0x03; // Protocol version 3 also supports specifying when connected to "the cloud" - if (this->protocol_version_ >= 0x03) { - if (remote_is_connected()) { - status = 0x04; - } + if (this->protocol_version_ >= 0x03 && remote_is_connected()) { + status = 0x04; } } diff --git a/esphome/components/tuya/tuya.h b/esphome/components/tuya/tuya.h index 3828c49b48..3a267d75a7 100644 --- a/esphome/components/tuya/tuya.h +++ b/esphome/components/tuya/tuya.h @@ -79,6 +79,7 @@ class Tuya : public Component, public uart::UARTDevice { void set_raw_datapoint_value(uint8_t datapoint_id, const std::vector &value); void set_boolean_datapoint_value(uint8_t datapoint_id, bool value); void set_integer_datapoint_value(uint8_t datapoint_id, uint32_t value); + void set_status_pin(InternalGPIOPin *status_pin) { this->status_pin_ = status_pin; } void set_string_datapoint_value(uint8_t datapoint_id, const std::string &value); void set_enum_datapoint_value(uint8_t datapoint_id, uint8_t value); void set_bitmask_datapoint_value(uint8_t datapoint_id, uint32_t value, uint8_t length); @@ -115,6 +116,7 @@ class Tuya : public Component, public uart::UARTDevice { void set_string_datapoint_value_(uint8_t datapoint_id, const std::string &value, bool forced); void set_raw_datapoint_value_(uint8_t datapoint_id, const std::vector &value, bool forced); void send_datapoint_command_(uint8_t datapoint_id, TuyaDatapointType datapoint_type, std::vector data); + void set_status_pin_(); void send_wifi_status_(); #ifdef USE_TIME @@ -123,8 +125,9 @@ class Tuya : public Component, public uart::UARTDevice { #endif TuyaInitState init_state_ = TuyaInitState::INIT_HEARTBEAT; uint8_t protocol_version_ = -1; - int gpio_status_ = -1; - int gpio_reset_ = -1; + optional status_pin_{}; + int status_pin_reported_ = -1; + int reset_pin_reported_ = -1; uint32_t last_command_timestamp_ = 0; uint32_t last_rx_char_timestamp_ = 0; std::string product_ = ""; diff --git a/tests/test4.yaml b/tests/test4.yaml index 54412222b5..82bb9e2f85 100644 --- a/tests/test4.yaml +++ b/tests/test4.yaml @@ -57,6 +57,9 @@ time: tuya: time_id: sntp_time + status_pin: + number: 14 + inverted: true pipsolar: id: inverter0 From f62d5d3b9d150891a4f80ed9ea24b3a65e6c2981 Mon Sep 17 00:00:00 2001 From: Maxim Ocheretianko Date: Sun, 15 May 2022 22:49:40 +0300 Subject: [PATCH 13/60] Add Tuya select (#3469) --- CODEOWNERS | 1 + esphome/components/tuya/select/__init__.py | 47 +++++++++++++++++ .../components/tuya/select/tuya_select.cpp | 52 +++++++++++++++++++ esphome/components/tuya/select/tuya_select.h | 30 +++++++++++ esphome/const.py | 1 + tests/test4.yaml | 9 ++++ 6 files changed, 140 insertions(+) create mode 100644 esphome/components/tuya/select/__init__.py create mode 100644 esphome/components/tuya/select/tuya_select.cpp create mode 100644 esphome/components/tuya/select/tuya_select.h diff --git a/CODEOWNERS b/CODEOWNERS index e2b29547cb..be6e8be3f7 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -222,6 +222,7 @@ esphome/components/tsl2591/* @wjcarpenter esphome/components/tuya/binary_sensor/* @jesserockz esphome/components/tuya/climate/* @jesserockz esphome/components/tuya/number/* @frankiboy1 +esphome/components/tuya/select/* @bearpawmaxim esphome/components/tuya/sensor/* @jesserockz esphome/components/tuya/switch/* @jesserockz esphome/components/tuya/text_sensor/* @dentra diff --git a/esphome/components/tuya/select/__init__.py b/esphome/components/tuya/select/__init__.py new file mode 100644 index 0000000000..3d65eda301 --- /dev/null +++ b/esphome/components/tuya/select/__init__.py @@ -0,0 +1,47 @@ +from esphome.components import select +import esphome.config_validation as cv +import esphome.codegen as cg +from esphome.const import CONF_OPTIONS, CONF_OPTIMISTIC, CONF_ENUM_DATAPOINT +from .. import tuya_ns, CONF_TUYA_ID, Tuya + +DEPENDENCIES = ["tuya"] +CODEOWNERS = ["@bearpawmaxim"] + +TuyaSelect = tuya_ns.class_("TuyaSelect", select.Select, cg.Component) + + +def ensure_option_map(value): + cv.check_not_templatable(value) + option = cv.All(cv.int_range(0, 2**8 - 1)) + mapping = cv.All(cv.string_strict) + options_map_schema = cv.Schema({option: mapping}) + value = options_map_schema(value) + + all_values = list(value.keys()) + unique_values = set(value.keys()) + if len(all_values) != len(unique_values): + raise cv.Invalid("Mapping values must be unique.") + + return value + + +CONFIG_SCHEMA = select.SELECT_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(TuyaSelect), + cv.GenerateID(CONF_TUYA_ID): cv.use_id(Tuya), + cv.Required(CONF_ENUM_DATAPOINT): cv.uint8_t, + cv.Required(CONF_OPTIONS): ensure_option_map, + cv.Optional(CONF_OPTIMISTIC, default=False): cv.boolean, + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + options_map = config[CONF_OPTIONS] + var = await select.new_select(config, options=list(options_map.values())) + await cg.register_component(var, config) + cg.add(var.set_select_mappings(list(options_map.keys()))) + parent = await cg.get_variable(config[CONF_TUYA_ID]) + cg.add(var.set_tuya_parent(parent)) + cg.add(var.set_select_id(config[CONF_ENUM_DATAPOINT])) + cg.add(var.set_optimistic(config[CONF_OPTIMISTIC])) diff --git a/esphome/components/tuya/select/tuya_select.cpp b/esphome/components/tuya/select/tuya_select.cpp new file mode 100644 index 0000000000..a4df0873b0 --- /dev/null +++ b/esphome/components/tuya/select/tuya_select.cpp @@ -0,0 +1,52 @@ +#include "esphome/core/log.h" +#include "tuya_select.h" + +namespace esphome { +namespace tuya { + +static const char *const TAG = "tuya.select"; + +void TuyaSelect::setup() { + this->parent_->register_listener(this->select_id_, [this](const TuyaDatapoint &datapoint) { + uint8_t enum_value = datapoint.value_enum; + ESP_LOGV(TAG, "MCU reported select %u value %u", this->select_id_, enum_value); + auto options = this->traits.get_options(); + auto mappings = this->mappings_; + auto it = std::find(mappings.cbegin(), mappings.cend(), enum_value); + if (it == mappings.end()) { + ESP_LOGW(TAG, "Invalid value %u", enum_value); + return; + } + size_t mapping_idx = std::distance(mappings.cbegin(), it); + auto value = this->at(mapping_idx); + this->publish_state(value.value()); + }); +} + +void TuyaSelect::control(const std::string &value) { + if (this->optimistic_) + this->publish_state(value); + + auto idx = this->index_of(value); + if (idx.has_value()) { + uint8_t mapping = this->mappings_.at(idx.value()); + ESP_LOGV(TAG, "Setting %u datapoint value to %u:%s", this->select_id_, mapping, value.c_str()); + this->parent_->set_enum_datapoint_value(this->select_id_, mapping); + return; + } + + ESP_LOGW(TAG, "Invalid value %s", value.c_str()); +} + +void TuyaSelect::dump_config() { + LOG_SELECT("", "Tuya Select", this); + ESP_LOGCONFIG(TAG, " Select has datapoint ID %u", this->select_id_); + ESP_LOGCONFIG(TAG, " Options are:"); + auto options = this->traits.get_options(); + for (auto i = 0; i < this->mappings_.size(); i++) { + ESP_LOGCONFIG(TAG, " %i: %s", this->mappings_.at(i), options.at(i).c_str()); + } +} + +} // namespace tuya +} // namespace esphome diff --git a/esphome/components/tuya/select/tuya_select.h b/esphome/components/tuya/select/tuya_select.h new file mode 100644 index 0000000000..ab233dc501 --- /dev/null +++ b/esphome/components/tuya/select/tuya_select.h @@ -0,0 +1,30 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/tuya/tuya.h" +#include "esphome/components/select/select.h" + +namespace esphome { +namespace tuya { + +class TuyaSelect : public select::Select, public Component { + public: + void setup() override; + void dump_config() override; + + void set_tuya_parent(Tuya *parent) { this->parent_ = parent; } + void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } + void set_select_id(uint8_t select_id) { this->select_id_ = select_id; } + void set_select_mappings(std::vector mappings) { this->mappings_ = std::move(mappings); } + + protected: + void control(const std::string &value) override; + + Tuya *parent_; + bool optimistic_ = false; + uint8_t select_id_; + std::vector mappings_; +}; + +} // namespace tuya +} // namespace esphome diff --git a/esphome/const.py b/esphome/const.py index 9a2e41d69f..c2aa53be70 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -198,6 +198,7 @@ CONF_ENABLE_TIME = "enable_time" CONF_ENERGY = "energy" CONF_ENTITY_CATEGORY = "entity_category" CONF_ENTITY_ID = "entity_id" +CONF_ENUM_DATAPOINT = "enum_datapoint" CONF_ESP8266_DISABLE_SSL_SUPPORT = "esp8266_disable_ssl_support" CONF_ESPHOME = "esphome" CONF_ETHERNET = "ethernet" diff --git a/tests/test4.yaml b/tests/test4.yaml index 82bb9e2f85..0e9f14e0d6 100644 --- a/tests/test4.yaml +++ b/tests/test4.yaml @@ -61,6 +61,15 @@ tuya: number: 14 inverted: true +select: + - platform: tuya + id: tuya_select + enum_datapoint: 42 + options: + 0: Internal + 1: Floor + 2: Both + pipsolar: id: inverter0 From 93e2506279239a3d09511bcfc0db44ee00fa3489 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 16 May 2022 13:05:20 +1200 Subject: [PATCH 14/60] Mark improv_serial and ESP-IDF usb based serial on c3/s2/s3 unsupported (#3477) --- esphome/components/improv_serial/__init__.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/esphome/components/improv_serial/__init__.py b/esphome/components/improv_serial/__init__.py index 21073a8ab3..67a0f7f4ed 100644 --- a/esphome/components/improv_serial/__init__.py +++ b/esphome/components/improv_serial/__init__.py @@ -1,6 +1,8 @@ -from esphome.const import CONF_BAUD_RATE, CONF_ID, CONF_LOGGER +from esphome.components.logger import USB_CDC, USB_SERIAL_JTAG +from esphome.const import CONF_BAUD_RATE, CONF_HARDWARE_UART, CONF_ID, CONF_LOGGER import esphome.codegen as cg import esphome.config_validation as cv +from esphome.core import CORE import esphome.final_validate as fv CODEOWNERS = ["@esphome/core"] @@ -17,14 +19,19 @@ CONFIG_SCHEMA = cv.Schema( ).extend(cv.COMPONENT_SCHEMA) -def validate_logger_baud_rate(config): +def validate_logger(config): logger_conf = fv.full_config.get()[CONF_LOGGER] if logger_conf[CONF_BAUD_RATE] == 0: raise cv.Invalid("improv_serial requires the logger baud_rate to be not 0") + if CORE.using_esp_idf: + if logger_conf[CONF_HARDWARE_UART] in [USB_SERIAL_JTAG, USB_CDC]: + raise cv.Invalid( + "improv_serial does not support the selected logger hardware_uart" + ) return config -FINAL_VALIDATE_SCHEMA = validate_logger_baud_rate +FINAL_VALIDATE_SCHEMA = validate_logger async def to_code(config): From 6dabf24bf35681e98fe01950dd9c77bd28fe4c81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=5Bp=CA=B2=C9=B5s=5D?= Date: Mon, 16 May 2022 05:35:27 +0200 Subject: [PATCH 15/60] MQTT cover: send state even if position is available (#3473) --- esphome/components/mqtt/mqtt_cover.cpp | 28 +++++++++++--------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/esphome/components/mqtt/mqtt_cover.cpp b/esphome/components/mqtt/mqtt_cover.cpp index e5525bc0f7..0718a24828 100644 --- a/esphome/components/mqtt/mqtt_cover.cpp +++ b/esphome/components/mqtt/mqtt_cover.cpp @@ -51,10 +51,9 @@ void MQTTCoverComponent::setup() { void MQTTCoverComponent::dump_config() { ESP_LOGCONFIG(TAG, "MQTT cover '%s':", this->cover_->get_name().c_str()); auto traits = this->cover_->get_traits(); - // no state topic for position - bool state_topic = !traits.get_supports_position(); - LOG_MQTT_COMPONENT(state_topic, true) - if (!state_topic) { + bool has_command_topic = traits.get_supports_position() || !traits.get_supports_tilt(); + LOG_MQTT_COMPONENT(true, has_command_topic) + if (traits.get_supports_position()) { ESP_LOGCONFIG(TAG, " Position State Topic: '%s'", this->get_position_state_topic().c_str()); ESP_LOGCONFIG(TAG, " Position Command Topic: '%s'", this->get_position_command_topic().c_str()); } @@ -72,7 +71,6 @@ void MQTTCoverComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConf root[MQTT_OPTIMISTIC] = true; } if (traits.get_supports_position()) { - config.state_topic = false; root[MQTT_POSITION_TOPIC] = this->get_position_state_topic(); root[MQTT_SET_POSITION_TOPIC] = this->get_position_command_topic(); } @@ -92,17 +90,7 @@ bool MQTTCoverComponent::send_initial_state() { return this->publish_state(); } bool MQTTCoverComponent::publish_state() { auto traits = this->cover_->get_traits(); bool success = true; - if (!traits.get_supports_position()) { - const char *state_s = "unknown"; - if (this->cover_->position == COVER_OPEN) { - state_s = "open"; - } else if (this->cover_->position == COVER_CLOSED) { - state_s = "closed"; - } - - if (!this->publish(this->get_state_topic_(), state_s)) - success = false; - } else { + if (traits.get_supports_position()) { std::string pos = value_accuracy_to_string(roundf(this->cover_->position * 100), 0); if (!this->publish(this->get_position_state_topic(), pos)) success = false; @@ -112,6 +100,14 @@ bool MQTTCoverComponent::publish_state() { if (!this->publish(this->get_tilt_state_topic(), pos)) success = false; } + const char *state_s = this->cover_->current_operation == COVER_OPERATION_OPENING ? "opening" + : this->cover_->current_operation == COVER_OPERATION_CLOSING ? "closing" + : this->cover_->position == COVER_CLOSED ? "closed" + : this->cover_->position == COVER_OPEN ? "open" + : traits.get_supports_position() ? "open" + : "unknown"; + if (!this->publish(this->get_state_topic_(), state_s)) + success = false; return success; } From 609a2ca5926b1213f242cac02a324821530612c6 Mon Sep 17 00:00:00 2001 From: Martin <25747549+martgras@users.noreply.github.com> Date: Tue, 17 May 2022 00:59:36 +0200 Subject: [PATCH 16/60] ESP32: Only save to NVS if data was changed (#3479) --- esphome/components/esp32/preferences.cpp | 33 +++++++++++++++++++----- 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/esphome/components/esp32/preferences.cpp b/esphome/components/esp32/preferences.cpp index 8c2b67a942..a78159825e 100644 --- a/esphome/components/esp32/preferences.cpp +++ b/esphome/components/esp32/preferences.cpp @@ -118,12 +118,17 @@ class ESP32Preferences : public ESPPreferences { // go through vector from back to front (makes erase easier/more efficient) for (ssize_t i = s_pending_save.size() - 1; i >= 0; i--) { const auto &save = s_pending_save[i]; - esp_err_t err = nvs_set_blob(nvs_handle, save.key.c_str(), save.data.data(), save.data.size()); - if (err != 0) { - ESP_LOGV(TAG, "nvs_set_blob('%s', len=%u) failed: %s", save.key.c_str(), save.data.size(), - esp_err_to_name(err)); - any_failed = true; - continue; + ESP_LOGVV(TAG, "Checking if NVS data %s has changed", save.key.c_str()); + if (is_changed(nvs_handle, save)) { + esp_err_t err = nvs_set_blob(nvs_handle, save.key.c_str(), save.data.data(), save.data.size()); + if (err != 0) { + ESP_LOGV(TAG, "nvs_set_blob('%s', len=%u) failed: %s", save.key.c_str(), save.data.size(), + esp_err_to_name(err)); + any_failed = true; + continue; + } + } else { + ESP_LOGD(TAG, "NVS data not changed skipping %s len=%u", save.key.c_str(), save.data.size()); } s_pending_save.erase(s_pending_save.begin() + i); } @@ -137,6 +142,22 @@ class ESP32Preferences : public ESPPreferences { return !any_failed; } + bool is_changed(const uint32_t nvs_handle, const NVSData &to_save) { + NVSData stored_data{}; + size_t actual_len; + esp_err_t err = nvs_get_blob(nvs_handle, to_save.key.c_str(), nullptr, &actual_len); + if (err != 0) { + ESP_LOGV(TAG, "nvs_get_blob('%s'): %s - the key might not be set yet", to_save.key.c_str(), esp_err_to_name(err)); + return true; + } + stored_data.data.reserve(actual_len); + err = nvs_get_blob(nvs_handle, to_save.key.c_str(), stored_data.data.data(), &actual_len); + if (err != 0) { + ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", to_save.key.c_str(), esp_err_to_name(err)); + return true; + } + return to_save.data == stored_data.data; + } }; void setup_preferences() { From 9b6b9c1fa24b272c87232611126dbbbbcbac6a33 Mon Sep 17 00:00:00 2001 From: Samuel Sieb Date: Tue, 17 May 2022 01:15:02 -0700 Subject: [PATCH 17/60] Retry Tuya init commands (#3482) Co-authored-by: Samuel Sieb --- esphome/components/tuya/tuya.cpp | 28 +++++++++++++++++++++++----- esphome/components/tuya/tuya.h | 2 ++ 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/esphome/components/tuya/tuya.cpp b/esphome/components/tuya/tuya.cpp index 1b35121c57..f4744064e3 100644 --- a/esphome/components/tuya/tuya.cpp +++ b/esphome/components/tuya/tuya.cpp @@ -11,6 +11,7 @@ namespace tuya { static const char *const TAG = "tuya"; static const int COMMAND_DELAY = 10; static const int RECEIVE_TIMEOUT = 300; +static const int MAX_RETRIES = 5; void Tuya::setup() { this->set_interval("heartbeat", 15000, [this] { this->send_empty_command_(TuyaCommandType::HEARTBEAT); }); @@ -31,8 +32,12 @@ void Tuya::loop() { void Tuya::dump_config() { ESP_LOGCONFIG(TAG, "Tuya:"); if (this->init_state_ != TuyaInitState::INIT_DONE) { - ESP_LOGCONFIG(TAG, " Configuration will be reported when setup is complete. Current init_state: %u", - static_cast(this->init_state_)); + if (this->init_failed_) { + ESP_LOGCONFIG(TAG, " Initialization failed. Current init_state: %u", static_cast(this->init_state_)); + } else { + ESP_LOGCONFIG(TAG, " Configuration will be reported when setup is complete. Current init_state: %u", + static_cast(this->init_state_)); + } ESP_LOGCONFIG(TAG, " If no further output is received, confirm that this is a supported Tuya device."); return; } @@ -54,8 +59,8 @@ void Tuya::dump_config() { } } if ((this->status_pin_reported_ != -1) || (this->reset_pin_reported_ != -1)) { - ESP_LOGCONFIG(TAG, " GPIO Configuration: status: pin %d, reset: pin %d (not supported)", - this->status_pin_reported_, this->reset_pin_reported_); + ESP_LOGCONFIG(TAG, " GPIO Configuration: status: pin %d, reset: pin %d", this->status_pin_reported_, + this->reset_pin_reported_); } if (this->status_pin_.has_value()) { LOG_PIN(" Status Pin: ", this->status_pin_.value()); @@ -134,6 +139,8 @@ void Tuya::handle_command_(uint8_t command, uint8_t version, const uint8_t *buff if (this->expected_response_.has_value() && this->expected_response_ == command_type) { this->expected_response_.reset(); + this->command_queue_.erase(command_queue_.begin()); + this->init_retries_ = 0; } switch (command_type) { @@ -396,13 +403,24 @@ void Tuya::process_command_queue_() { if (this->expected_response_.has_value() && delay > RECEIVE_TIMEOUT) { this->expected_response_.reset(); + if (init_state_ != TuyaInitState::INIT_DONE) { + if (++this->init_retries_ >= MAX_RETRIES) { + this->init_failed_ = true; + ESP_LOGE(TAG, "Initialization failed at init_state %u", static_cast(this->init_state_)); + this->command_queue_.erase(command_queue_.begin()); + this->init_retries_ = 0; + } + } else { + this->command_queue_.erase(command_queue_.begin()); + } } // Left check of delay since last command in case there's ever a command sent by calling send_raw_command_ directly if (delay > COMMAND_DELAY && !this->command_queue_.empty() && this->rx_message_.empty() && !this->expected_response_.has_value()) { this->send_raw_command_(command_queue_.front()); - this->command_queue_.erase(command_queue_.begin()); + if (!this->expected_response_.has_value()) + this->command_queue_.erase(command_queue_.begin()); } } diff --git a/esphome/components/tuya/tuya.h b/esphome/components/tuya/tuya.h index 3a267d75a7..1f21b09c0c 100644 --- a/esphome/components/tuya/tuya.h +++ b/esphome/components/tuya/tuya.h @@ -124,6 +124,8 @@ class Tuya : public Component, public uart::UARTDevice { optional time_id_{}; #endif TuyaInitState init_state_ = TuyaInitState::INIT_HEARTBEAT; + bool init_failed_{false}; + int init_retries_{0}; uint8_t protocol_version_ = -1; optional status_pin_{}; int status_pin_reported_ = -1; From c000e1d6dda3f0b07b2df46b7440a2ba79d33c19 Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Wed, 18 May 2022 23:23:00 +0100 Subject: [PATCH 18/60] Ili9341 8bit indexed mode pt1 (#2490) --- .../components/display/display_color_utils.h | 47 +++++++++++++++++++ esphome/components/ili9341/display.py | 18 +++++++ .../components/ili9341/ili9341_display.cpp | 17 +++++-- esphome/components/ili9341/ili9341_display.h | 10 ++++ esphome/const.py | 1 + 5 files changed, 90 insertions(+), 3 deletions(-) diff --git a/esphome/components/display/display_color_utils.h b/esphome/components/display/display_color_utils.h index 7f29586932..bf6d5445f1 100644 --- a/esphome/components/display/display_color_utils.h +++ b/esphome/components/display/display_color_utils.h @@ -107,6 +107,53 @@ class ColorUtil { uint32_t gs4 = esp_scale8(color.white, 15); return gs4; } + /*** + * Converts a Color value to an 8bit index using a 24bit 888 palette. + * Uses euclidiean distance to calculate the linear distance between + * two points in an RGB cube, then iterates through the full palette + * returning the closest match. + * @param[in] color The target color. + * @param[in] palette The 256*3 byte RGB palette. + * @return The 8 bit index of the closest color (e.g. for display buffer). + */ + // static uint8_t color_to_index8_palette888(Color color, uint8_t *palette) { + static uint8_t color_to_index8_palette888(Color color, const uint8_t *palette) { + uint8_t closest_index = 0; + uint32_t minimum_dist2 = UINT32_MAX; // Smallest distance^2 to the target + // so far + // int8_t(*plt)[][3] = palette; + int16_t tgt_r = color.r; + int16_t tgt_g = color.g; + int16_t tgt_b = color.b; + uint16_t x, y, z; + // Loop through each row of the palette + for (uint16_t i = 0; i < 256; i++) { + // Get the pallet rgb color + int16_t plt_r = (int16_t) palette[i * 3 + 0]; + int16_t plt_g = (int16_t) palette[i * 3 + 1]; + int16_t plt_b = (int16_t) palette[i * 3 + 2]; + // Calculate euclidian distance (linear distance in rgb cube). + x = (uint32_t) std::abs(tgt_r - plt_r); + y = (uint32_t) std::abs(tgt_g - plt_g); + z = (uint32_t) std::abs(tgt_b - plt_b); + uint32_t dist2 = x * x + y * y + z * z; + if (dist2 < minimum_dist2) { + minimum_dist2 = dist2; + closest_index = (uint8_t) i; + } + } + return closest_index; + } + /*** + * Converts an 8bit palette index (e.g. from a display buffer) to a color. + * @param[in] index The index to look up. + * @param[in] palette The 256*3 byte RGB palette. + * @return The RGBW Color object looked up by the palette. + */ + static Color index8_to_color_palette888(uint8_t index, const uint8_t *palette) { + Color color = Color(palette[index * 3 + 0], palette[index * 3 + 1], palette[index * 3 + 2], 0); + return color; + } }; } // namespace display } // namespace esphome diff --git a/esphome/components/ili9341/display.py b/esphome/components/ili9341/display.py index 157e8212bd..6b18196ef1 100644 --- a/esphome/components/ili9341/display.py +++ b/esphome/components/ili9341/display.py @@ -3,13 +3,16 @@ import esphome.config_validation as cv from esphome import pins from esphome.components import display, spi from esphome.const import ( + CONF_COLOR_PALETTE, CONF_DC_PIN, CONF_ID, CONF_LAMBDA, CONF_MODEL, CONF_PAGES, + CONF_RAW_DATA_ID, CONF_RESET_PIN, ) +from esphome.core import HexInt DEPENDENCIES = ["spi"] @@ -23,6 +26,7 @@ ILI9341M5Stack = ili9341_ns.class_("ILI9341M5Stack", ili9341) ILI9341TFT24 = ili9341_ns.class_("ILI9341TFT24", ili9341) ILI9341Model = ili9341_ns.enum("ILI9341Model") +ILI9341ColorMode = ili9341_ns.enum("ILI9341ColorMode") MODELS = { "M5STACK": ILI9341Model.M5STACK, @@ -31,6 +35,8 @@ MODELS = { ILI9341_MODEL = cv.enum(MODELS, upper=True, space="_") +COLOR_PALETTE = cv.one_of("NONE", "GRAYSCALE") + CONFIG_SCHEMA = cv.All( display.FULL_DISPLAY_SCHEMA.extend( { @@ -39,6 +45,8 @@ CONFIG_SCHEMA = cv.All( 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): pins.gpio_output_pin_schema, + cv.Optional(CONF_COLOR_PALETTE, default="NONE"): COLOR_PALETTE, + cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8), } ) .extend(cv.polling_component_schema("1s")) @@ -73,3 +81,13 @@ async def to_code(config): if CONF_LED_PIN in config: led_pin = await cg.gpio_pin_expression(config[CONF_LED_PIN]) cg.add(var.set_led_pin(led_pin)) + + if config[CONF_COLOR_PALETTE] == "GRAYSCALE": + cg.add(var.set_buffer_color_mode(ILI9341ColorMode.BITS_8_INDEXED)) + rhs = [] + for x in range(256): + rhs.extend([HexInt(x), HexInt(x), HexInt(x)]) + prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) + cg.add(var.set_palette(prog_arr)) + else: + pass diff --git a/esphome/components/ili9341/ili9341_display.cpp b/esphome/components/ili9341/ili9341_display.cpp index 09524ba787..0ad5446d9a 100644 --- a/esphome/components/ili9341/ili9341_display.cpp +++ b/esphome/components/ili9341/ili9341_display.cpp @@ -161,8 +161,13 @@ void HOT ILI9341Display::draw_absolute_pixel_internal(int x, int y, Color color) this->y_high_ = (y > this->y_high_) ? y : this->y_high_; uint32_t pos = (y * width_) + x; - uint8_t color332 = display::ColorUtil::color_to_332(color, display::ColorOrder::COLOR_ORDER_RGB); - buffer_[pos] = color332; + if (this->buffer_color_mode_ == BITS_8) { + uint8_t color332 = display::ColorUtil::color_to_332(color, display::ColorOrder::COLOR_ORDER_RGB); + buffer_[pos] = color332; + } else { // if (this->buffer_color_mode_ == BITS_8_INDEXED) { + uint8_t index = display::ColorUtil::color_to_index8_palette888(color, this->palette_); + buffer_[pos] = index; + } } // should return the total size: return this->get_width_internal() * this->get_height_internal() * 2 // 16bit color @@ -227,7 +232,13 @@ uint32_t ILI9341Display::buffer_to_transfer_(uint32_t pos, uint32_t sz) { } for (uint32_t i = 0; i < sz; ++i) { - uint16_t color = display::ColorUtil::color_to_565(display::ColorUtil::rgb332_to_color(*src++)); + uint16_t color; + if (this->buffer_color_mode_ == BITS_8) { + color = display::ColorUtil::color_to_565(display::ColorUtil::rgb332_to_color(*src++)); + } else { // if (this->buffer_color_mode == BITS_8_INDEXED) { + Color col = display::ColorUtil::index8_to_color_palette888(*src++, this->palette_); + color = display::ColorUtil::color_to_565(col); + } *dst++ = (uint8_t)(color >> 8); *dst++ = (uint8_t) color; } diff --git a/esphome/components/ili9341/ili9341_display.h b/esphome/components/ili9341/ili9341_display.h index eeff688f4f..6014dbf9a6 100644 --- a/esphome/components/ili9341/ili9341_display.h +++ b/esphome/components/ili9341/ili9341_display.h @@ -14,6 +14,11 @@ enum ILI9341Model { TFT_24, }; +enum ILI9341ColorMode { + BITS_8, + BITS_8_INDEXED, +}; + class ILI9341Display : public PollingComponent, public display::DisplayBuffer, public spi::SPIDevicereset_pin_ = reset; } void set_led_pin(GPIOPin *led) { this->led_pin_ = led; } void set_model(ILI9341Model model) { this->model_ = model; } + void set_palette(const uint8_t *palette) { this->palette_ = palette; } + void set_buffer_color_mode(ILI9341ColorMode color_mode) { this->buffer_color_mode_ = color_mode; } void command(uint8_t value); void data(uint8_t value); @@ -59,6 +66,9 @@ class ILI9341Display : public PollingComponent, uint16_t y_low_{0}; uint16_t x_high_{0}; uint16_t y_high_{0}; + const uint8_t *palette_; + + ILI9341ColorMode buffer_color_mode_{BITS_8}; uint32_t get_buffer_length_(); int get_width_internal() override; diff --git a/esphome/const.py b/esphome/const.py index c2aa53be70..b73d7e33bd 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -105,6 +105,7 @@ CONF_COLOR_BRIGHTNESS = "color_brightness" CONF_COLOR_CORRECT = "color_correct" CONF_COLOR_INTERLOCK = "color_interlock" CONF_COLOR_MODE = "color_mode" +CONF_COLOR_PALETTE = "color_palette" CONF_COLOR_TEMPERATURE = "color_temperature" CONF_COLORS = "colors" CONF_COMMAND = "command" From 78821056617a9acb35755b2f7a3bdf22965f5c2c Mon Sep 17 00:00:00 2001 From: user897943 Date: Wed, 18 May 2022 23:25:42 +0100 Subject: [PATCH 19/60] Update bedjet_const.h to remove blank spaces before speed steps, fixes Unknown Error when using climate.set_fan_mode in HA (#3476) --- esphome/components/bedjet/bedjet_const.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/bedjet/bedjet_const.h b/esphome/components/bedjet/bedjet_const.h index e6bfa45d3a..ae10ca1885 100644 --- a/esphome/components/bedjet/bedjet_const.h +++ b/esphome/components/bedjet/bedjet_const.h @@ -66,8 +66,8 @@ enum BedjetCommand : uint8_t { #define BEDJET_FAN_STEP_NAMES_ \ { \ - " 5%", " 10%", " 15%", " 20%", " 25%", " 30%", " 35%", " 40%", " 45%", " 50%", " 55%", " 60%", " 65%", " 70%", \ - " 75%", " 80%", " 85%", " 90%", " 95%", "100%" \ + "5%", "10%", "15%", "20%", "25%", "30%", "35%", "40%", "45%", "50%", "55%", "60%", "65%", "70%", "75%", "80%", \ + "85%", "90%", "95%", "100%" \ } static const char *const BEDJET_FAN_STEP_NAMES[20] = BEDJET_FAN_STEP_NAMES_; From 9c78049359711006cabf94bf02b2c1a46549777f Mon Sep 17 00:00:00 2001 From: myml Date: Thu, 19 May 2022 08:23:50 +0800 Subject: [PATCH 20/60] feat: esp32-camera add stream event (#3285) --- esphome/components/esp32_camera/__init__.py | 37 ++++++++++++++++++- .../components/esp32_camera/esp32_camera.cpp | 16 +++++++- .../components/esp32_camera/esp32_camera.h | 23 ++++++++++++ 3 files changed, 73 insertions(+), 3 deletions(-) diff --git a/esphome/components/esp32_camera/__init__.py b/esphome/components/esp32_camera/__init__.py index 912e705766..753b6ed9da 100644 --- a/esphome/components/esp32_camera/__init__.py +++ b/esphome/components/esp32_camera/__init__.py @@ -1,5 +1,6 @@ import esphome.codegen as cg import esphome.config_validation as cv +from esphome import automation from esphome import pins from esphome.const import ( CONF_FREQUENCY, @@ -12,6 +13,7 @@ from esphome.const import ( CONF_RESOLUTION, CONF_BRIGHTNESS, CONF_CONTRAST, + CONF_TRIGGER_ID, ) from esphome.core import CORE from esphome.components.esp32 import add_idf_sdkconfig_option @@ -23,7 +25,14 @@ AUTO_LOAD = ["psram"] esp32_camera_ns = cg.esphome_ns.namespace("esp32_camera") ESP32Camera = esp32_camera_ns.class_("ESP32Camera", cg.PollingComponent, cg.EntityBase) - +ESP32CameraStreamStartTrigger = esp32_camera_ns.class_( + "ESP32CameraStreamStartTrigger", + automation.Trigger.template(), +) +ESP32CameraStreamStopTrigger = esp32_camera_ns.class_( + "ESP32CameraStreamStopTrigger", + automation.Trigger.template(), +) ESP32CameraFrameSize = esp32_camera_ns.enum("ESP32CameraFrameSize") FRAME_SIZES = { "160X120": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_160X120, @@ -111,6 +120,10 @@ CONF_TEST_PATTERN = "test_pattern" CONF_MAX_FRAMERATE = "max_framerate" CONF_IDLE_FRAMERATE = "idle_framerate" +# stream trigger +CONF_ON_STREAM_START = "on_stream_start" +CONF_ON_STREAM_STOP = "on_stream_stop" + camera_range_param = cv.int_range(min=-2, max=2) CONFIG_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend( @@ -178,6 +191,20 @@ CONFIG_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend( cv.Optional(CONF_IDLE_FRAMERATE, default="0.1 fps"): cv.All( cv.framerate, cv.Range(min=0, max=1) ), + cv.Optional(CONF_ON_STREAM_START): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + ESP32CameraStreamStartTrigger + ), + } + ), + cv.Optional(CONF_ON_STREAM_STOP): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + ESP32CameraStreamStopTrigger + ), + } + ), } ).extend(cv.COMPONENT_SCHEMA) @@ -238,3 +265,11 @@ async def to_code(config): if CORE.using_esp_idf: cg.add_library("espressif/esp32-camera", "1.0.0") add_idf_sdkconfig_option("CONFIG_RTCIO_SUPPORT_RTC_GPIO_DESC", True) + + for conf in config.get(CONF_ON_STREAM_START, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + + for conf in config.get(CONF_ON_STREAM_STOP, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) diff --git a/esphome/components/esp32_camera/esp32_camera.cpp b/esphome/components/esp32_camera/esp32_camera.cpp index 851926b083..65b316dc8d 100644 --- a/esphome/components/esp32_camera/esp32_camera.cpp +++ b/esphome/components/esp32_camera/esp32_camera.cpp @@ -282,8 +282,20 @@ void ESP32Camera::set_idle_update_interval(uint32_t idle_update_interval) { void ESP32Camera::add_image_callback(std::function)> &&f) { this->new_image_callback_.add(std::move(f)); } -void ESP32Camera::start_stream(CameraRequester requester) { this->stream_requesters_ |= (1U << requester); } -void ESP32Camera::stop_stream(CameraRequester requester) { this->stream_requesters_ &= ~(1U << requester); } +void ESP32Camera::add_stream_start_callback(std::function &&callback) { + this->stream_start_callback_.add(std::move(callback)); +} +void ESP32Camera::add_stream_stop_callback(std::function &&callback) { + this->stream_stop_callback_.add(std::move(callback)); +} +void ESP32Camera::start_stream(CameraRequester requester) { + this->stream_start_callback_.call(); + this->stream_requesters_ |= (1U << requester); +} +void ESP32Camera::stop_stream(CameraRequester requester) { + this->stream_stop_callback_.call(); + this->stream_requesters_ &= ~(1U << requester); +} void ESP32Camera::request_image(CameraRequester requester) { this->single_requesters_ |= (1U << requester); } void ESP32Camera::update_camera_parameters() { sensor_t *s = esp_camera_sensor_get(); diff --git a/esphome/components/esp32_camera/esp32_camera.h b/esphome/components/esp32_camera/esp32_camera.h index 743b5bde5f..8bf73a0fa6 100644 --- a/esphome/components/esp32_camera/esp32_camera.h +++ b/esphome/components/esp32_camera/esp32_camera.h @@ -2,6 +2,7 @@ #ifdef USE_ESP32 +#include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/entity_base.h" #include "esphome/core/helpers.h" @@ -145,6 +146,9 @@ class ESP32Camera : public Component, public EntityBase { void request_image(CameraRequester requester); void update_camera_parameters(); + void add_stream_start_callback(std::function &&callback); + void add_stream_stop_callback(std::function &&callback); + protected: /* internal methods */ uint32_t hash_base() override; @@ -187,6 +191,8 @@ class ESP32Camera : public Component, public EntityBase { QueueHandle_t framebuffer_get_queue_; QueueHandle_t framebuffer_return_queue_; CallbackManager)> new_image_callback_; + CallbackManager stream_start_callback_{}; + CallbackManager stream_stop_callback_{}; uint32_t last_idle_request_{0}; uint32_t last_update_{0}; @@ -195,6 +201,23 @@ class ESP32Camera : public Component, public EntityBase { // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) extern ESP32Camera *global_esp32_camera; +class ESP32CameraStreamStartTrigger : public Trigger<> { + public: + explicit ESP32CameraStreamStartTrigger(ESP32Camera *parent) { + parent->add_stream_start_callback([this]() { this->trigger(); }); + } + + protected: +}; +class ESP32CameraStreamStopTrigger : public Trigger<> { + public: + explicit ESP32CameraStreamStopTrigger(ESP32Camera *parent) { + parent->add_stream_stop_callback([this]() { this->trigger(); }); + } + + protected: +}; + } // namespace esp32_camera } // namespace esphome From 0ed7db979be7057cd844033b37fe6f7079d16ed7 Mon Sep 17 00:00:00 2001 From: Martin <25747549+martgras@users.noreply.github.com> Date: Thu, 19 May 2022 02:47:33 +0200 Subject: [PATCH 21/60] Add support for SGP41 (#3382) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 1 + .../sgp40/sensirion_voc_algorithm.cpp | 628 ------------------ .../sgp40/sensirion_voc_algorithm.h | 147 ---- esphome/components/sgp40/sensor.py | 68 +- esphome/components/sgp40/sgp40.cpp | 274 -------- esphome/components/sgp40/sgp40.h | 93 --- esphome/components/sgp4x/__init__.py | 0 esphome/components/sgp4x/sensor.py | 144 ++++ esphome/components/sgp4x/sgp4x.cpp | 343 ++++++++++ esphome/components/sgp4x/sgp4x.h | 142 ++++ platformio.ini | 2 + tests/test2.yaml | 23 +- 12 files changed, 655 insertions(+), 1210 deletions(-) delete mode 100644 esphome/components/sgp40/sensirion_voc_algorithm.cpp delete mode 100644 esphome/components/sgp40/sensirion_voc_algorithm.h delete mode 100644 esphome/components/sgp40/sgp40.cpp delete mode 100644 esphome/components/sgp40/sgp40.h create mode 100644 esphome/components/sgp4x/__init__.py create mode 100644 esphome/components/sgp4x/sensor.py create mode 100644 esphome/components/sgp4x/sgp4x.cpp create mode 100644 esphome/components/sgp4x/sgp4x.h diff --git a/CODEOWNERS b/CODEOWNERS index be6e8be3f7..3e82a372ce 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -178,6 +178,7 @@ esphome/components/sen5x/* @martgras esphome/components/sensirion_common/* @martgras esphome/components/sensor/* @esphome/core esphome/components/sgp40/* @SenexCrenshaw +esphome/components/sgp4x/* @SenexCrenshaw @martgras esphome/components/shelly_dimmer/* @edge90 @rnauber esphome/components/sht4x/* @sjtrny esphome/components/shutdown/* @esphome/core @jsuanet diff --git a/esphome/components/sgp40/sensirion_voc_algorithm.cpp b/esphome/components/sgp40/sensirion_voc_algorithm.cpp deleted file mode 100644 index d76b776641..0000000000 --- a/esphome/components/sgp40/sensirion_voc_algorithm.cpp +++ /dev/null @@ -1,628 +0,0 @@ - -#include "sensirion_voc_algorithm.h" - -namespace esphome { -namespace sgp40 { - -/* The VOC code were originally created by - * https://github.com/Sensirion/embedded-sgp - * The fixed point arithmetic parts of this code were originally created by - * https://github.com/PetteriAimonen/libfixmath - */ - -/*!< the maximum value of fix16_t */ -#define FIX16_MAXIMUM 0x7FFFFFFF -/*!< the minimum value of fix16_t */ -static const uint32_t FIX16_MINIMUM = 0x80000000; -/*!< the value used to indicate overflows when FIXMATH_NO_OVERFLOW is not - * specified */ -static const uint32_t FIX16_OVERFLOW = 0x80000000; -/*!< fix16_t value of 1 */ -const uint32_t FIX16_ONE = 0x00010000; - -inline fix16_t fix16_from_int(int32_t a) { return a * FIX16_ONE; } - -inline int32_t fix16_cast_to_int(fix16_t a) { return (a >> 16); } - -/*! Multiplies the two given fix16_t's and returns the result. */ -static fix16_t fix16_mul(fix16_t in_arg0, fix16_t in_arg1); - -/*! Divides the first given fix16_t by the second and returns the result. */ -static fix16_t fix16_div(fix16_t a, fix16_t b); - -/*! Returns the square root of the given fix16_t. */ -static fix16_t fix16_sqrt(fix16_t in_value); - -/*! Returns the exponent (e^) of the given fix16_t. */ -static fix16_t fix16_exp(fix16_t in_value); - -static fix16_t fix16_mul(fix16_t in_arg0, fix16_t in_arg1) { - // Each argument is divided to 16-bit parts. - // AB - // * CD - // ----------- - // BD 16 * 16 -> 32 bit products - // CB - // AD - // AC - // |----| 64 bit product - int32_t a = (in_arg0 >> 16), c = (in_arg1 >> 16); - uint32_t b = (in_arg0 & 0xFFFF), d = (in_arg1 & 0xFFFF); - - int32_t ac = a * c; - int32_t ad_cb = a * d + c * b; - uint32_t bd = b * d; - - int32_t product_hi = ac + (ad_cb >> 16); // NOLINT - - // Handle carry from lower 32 bits to upper part of result. - uint32_t ad_cb_temp = ad_cb << 16; // NOLINT - uint32_t product_lo = bd + ad_cb_temp; - if (product_lo < bd) - product_hi++; - -#ifndef FIXMATH_NO_OVERFLOW - // The upper 17 bits should all be the same (the sign). - if (product_hi >> 31 != product_hi >> 15) - return FIX16_OVERFLOW; -#endif - -#ifdef FIXMATH_NO_ROUNDING - return (product_hi << 16) | (product_lo >> 16); -#else - // Subtracting 0x8000 (= 0.5) and then using signed right shift - // achieves proper rounding to result-1, except in the corner - // case of negative numbers and lowest word = 0x8000. - // To handle that, we also have to subtract 1 for negative numbers. - uint32_t product_lo_tmp = product_lo; - product_lo -= 0x8000; - product_lo -= (uint32_t) product_hi >> 31; - if (product_lo > product_lo_tmp) - product_hi--; - - // Discard the lowest 16 bits. Note that this is not exactly the same - // as dividing by 0x10000. For example if product = -1, result will - // also be -1 and not 0. This is compensated by adding +1 to the result - // and compensating this in turn in the rounding above. - fix16_t result = (product_hi << 16) | (product_lo >> 16); // NOLINT - result += 1; - return result; -#endif -} - -static fix16_t fix16_div(fix16_t a, fix16_t b) { - // This uses the basic binary restoring division algorithm. - // It appears to be faster to do the whole division manually than - // trying to compose a 64-bit divide out of 32-bit divisions on - // platforms without hardware divide. - - if (b == 0) - return FIX16_MINIMUM; - - uint32_t remainder = (a >= 0) ? a : (-a); - uint32_t divider = (b >= 0) ? b : (-b); - - uint32_t quotient = 0; - uint32_t bit = 0x10000; - - /* The algorithm requires D >= R */ - while (divider < remainder) { - divider <<= 1; - bit <<= 1; - } - -#ifndef FIXMATH_NO_OVERFLOW - if (!bit) - return FIX16_OVERFLOW; -#endif - - if (divider & 0x80000000) { - // Perform one step manually to avoid overflows later. - // We know that divider's bottom bit is 0 here. - if (remainder >= divider) { - quotient |= bit; - remainder -= divider; - } - divider >>= 1; - bit >>= 1; - } - - /* Main division loop */ - while (bit && remainder) { - if (remainder >= divider) { - quotient |= bit; - remainder -= divider; - } - - remainder <<= 1; - bit >>= 1; - } - -#ifndef FIXMATH_NO_ROUNDING - if (remainder >= divider) { - quotient++; - } -#endif - - fix16_t result = quotient; - - /* Figure out the sign of result */ - if ((a ^ b) & 0x80000000) { -#ifndef FIXMATH_NO_OVERFLOW - if (result == FIX16_MINIMUM) // NOLINT(clang-diagnostic-sign-compare) - return FIX16_OVERFLOW; -#endif - - result = -result; - } - - return result; -} - -static fix16_t fix16_sqrt(fix16_t in_value) { - // It is assumed that x is not negative - - uint32_t num = in_value; - uint32_t result = 0; - uint32_t bit; - uint8_t n; - - bit = (uint32_t) 1 << 30; - while (bit > num) - bit >>= 2; - - // The main part is executed twice, in order to avoid - // using 64 bit values in computations. - for (n = 0; n < 2; n++) { - // First we get the top 24 bits of the answer. - while (bit) { - if (num >= result + bit) { - num -= result + bit; - result = (result >> 1) + bit; - } else { - result = (result >> 1); - } - bit >>= 2; - } - - if (n == 0) { - // Then process it again to get the lowest 8 bits. - if (num > 65535) { - // The remainder 'num' is too large to be shifted left - // by 16, so we have to add 1 to result manually and - // adjust 'num' accordingly. - // num = a - (result + 0.5)^2 - // = num + result^2 - (result + 0.5)^2 - // = num - result - 0.5 - num -= result; - num = (num << 16) - 0x8000; - result = (result << 16) + 0x8000; - } else { - num <<= 16; - result <<= 16; - } - - bit = 1 << 14; - } - } - -#ifndef FIXMATH_NO_ROUNDING - // Finally, if next bit would have been 1, round the result upwards. - if (num > result) { - result++; - } -#endif - - return (fix16_t) result; -} - -static fix16_t fix16_exp(fix16_t in_value) { - // Function to approximate exp(); optimized more for code size than speed - - // exp(x) for x = +/- {1, 1/8, 1/64, 1/512} - fix16_t x = in_value; - static const uint8_t NUM_EXP_VALUES = 4; - static const fix16_t EXP_POS_VALUES[4] = {F16(2.7182818), F16(1.1331485), F16(1.0157477), F16(1.0019550)}; - static const fix16_t EXP_NEG_VALUES[4] = {F16(0.3678794), F16(0.8824969), F16(0.9844964), F16(0.9980488)}; - const fix16_t *exp_values; - - fix16_t res, arg; - uint16_t i; - - if (x >= F16(10.3972)) - return FIX16_MAXIMUM; - if (x <= F16(-11.7835)) - return 0; - - if (x < 0) { - x = -x; - exp_values = EXP_NEG_VALUES; - } else { - exp_values = EXP_POS_VALUES; - } - - res = FIX16_ONE; - arg = FIX16_ONE; - for (i = 0; i < NUM_EXP_VALUES; i++) { - while (x >= arg) { - res = fix16_mul(res, exp_values[i]); - x -= arg; - } - arg >>= 3; - } - return res; -} - -static void voc_algorithm_init_instances(VocAlgorithmParams *params); -static void voc_algorithm_mean_variance_estimator_init(VocAlgorithmParams *params); -static void voc_algorithm_mean_variance_estimator_init_instances(VocAlgorithmParams *params); -static void voc_algorithm_mean_variance_estimator_set_parameters(VocAlgorithmParams *params, fix16_t std_initial, - fix16_t tau_mean_variance_hours, - fix16_t gating_max_duration_minutes); -static void voc_algorithm_mean_variance_estimator_set_states(VocAlgorithmParams *params, fix16_t mean, fix16_t std, - fix16_t uptime_gamma); -static fix16_t voc_algorithm_mean_variance_estimator_get_std(VocAlgorithmParams *params); -static fix16_t voc_algorithm_mean_variance_estimator_get_mean(VocAlgorithmParams *params); -static void voc_algorithm_mean_variance_estimator_calculate_gamma(VocAlgorithmParams *params, - fix16_t voc_index_from_prior); -static void voc_algorithm_mean_variance_estimator_process(VocAlgorithmParams *params, fix16_t sraw, - fix16_t voc_index_from_prior); -static void voc_algorithm_mean_variance_estimator_sigmoid_init(VocAlgorithmParams *params); -static void voc_algorithm_mean_variance_estimator_sigmoid_set_parameters(VocAlgorithmParams *params, fix16_t l, - fix16_t x0, fix16_t k); -static fix16_t voc_algorithm_mean_variance_estimator_sigmoid_process(VocAlgorithmParams *params, fix16_t sample); -static void voc_algorithm_mox_model_init(VocAlgorithmParams *params); -static void voc_algorithm_mox_model_set_parameters(VocAlgorithmParams *params, fix16_t sraw_std, fix16_t sraw_mean); -static fix16_t voc_algorithm_mox_model_process(VocAlgorithmParams *params, fix16_t sraw); -static void voc_algorithm_sigmoid_scaled_init(VocAlgorithmParams *params); -static void voc_algorithm_sigmoid_scaled_set_parameters(VocAlgorithmParams *params, fix16_t offset); -static fix16_t voc_algorithm_sigmoid_scaled_process(VocAlgorithmParams *params, fix16_t sample); -static void voc_algorithm_adaptive_lowpass_init(VocAlgorithmParams *params); -static void voc_algorithm_adaptive_lowpass_set_parameters(VocAlgorithmParams *params); -static fix16_t voc_algorithm_adaptive_lowpass_process(VocAlgorithmParams *params, fix16_t sample); - -void voc_algorithm_init(VocAlgorithmParams *params) { - params->mVoc_Index_Offset = F16(VOC_ALGORITHM_VOC_INDEX_OFFSET_DEFAULT); - params->mTau_Mean_Variance_Hours = F16(VOC_ALGORITHM_TAU_MEAN_VARIANCE_HOURS); - params->mGating_Max_Duration_Minutes = F16(VOC_ALGORITHM_GATING_MAX_DURATION_MINUTES); - params->mSraw_Std_Initial = F16(VOC_ALGORITHM_SRAW_STD_INITIAL); - params->mUptime = F16(0.); - params->mSraw = F16(0.); - params->mVoc_Index = 0; - voc_algorithm_init_instances(params); -} - -static void voc_algorithm_init_instances(VocAlgorithmParams *params) { - voc_algorithm_mean_variance_estimator_init(params); - voc_algorithm_mean_variance_estimator_set_parameters( - params, params->mSraw_Std_Initial, params->mTau_Mean_Variance_Hours, params->mGating_Max_Duration_Minutes); - voc_algorithm_mox_model_init(params); - voc_algorithm_mox_model_set_parameters(params, voc_algorithm_mean_variance_estimator_get_std(params), - voc_algorithm_mean_variance_estimator_get_mean(params)); - voc_algorithm_sigmoid_scaled_init(params); - voc_algorithm_sigmoid_scaled_set_parameters(params, params->mVoc_Index_Offset); - voc_algorithm_adaptive_lowpass_init(params); - voc_algorithm_adaptive_lowpass_set_parameters(params); -} - -void voc_algorithm_get_states(VocAlgorithmParams *params, int32_t *state0, int32_t *state1) { - *state0 = voc_algorithm_mean_variance_estimator_get_mean(params); - *state1 = voc_algorithm_mean_variance_estimator_get_std(params); -} - -void voc_algorithm_set_states(VocAlgorithmParams *params, int32_t state0, int32_t state1) { - voc_algorithm_mean_variance_estimator_set_states(params, state0, state1, F16(VOC_ALGORITHM_PERSISTENCE_UPTIME_GAMMA)); - params->mSraw = state0; -} - -void voc_algorithm_set_tuning_parameters(VocAlgorithmParams *params, int32_t voc_index_offset, - int32_t learning_time_hours, int32_t gating_max_duration_minutes, - int32_t std_initial) { - params->mVoc_Index_Offset = (fix16_from_int(voc_index_offset)); - params->mTau_Mean_Variance_Hours = (fix16_from_int(learning_time_hours)); - params->mGating_Max_Duration_Minutes = (fix16_from_int(gating_max_duration_minutes)); - params->mSraw_Std_Initial = (fix16_from_int(std_initial)); - voc_algorithm_init_instances(params); -} - -void voc_algorithm_process(VocAlgorithmParams *params, int32_t sraw, int32_t *voc_index) { - if ((params->mUptime <= F16(VOC_ALGORITHM_INITIAL_BLACKOUT))) { - params->mUptime = (params->mUptime + F16(VOC_ALGORITHM_SAMPLING_INTERVAL)); - } else { - if (((sraw > 0) && (sraw < 65000))) { - if ((sraw < 20001)) { - sraw = 20001; - } else if ((sraw > 52767)) { - sraw = 52767; - } - params->mSraw = (fix16_from_int((sraw - 20000))); - } - params->mVoc_Index = voc_algorithm_mox_model_process(params, params->mSraw); - params->mVoc_Index = voc_algorithm_sigmoid_scaled_process(params, params->mVoc_Index); - params->mVoc_Index = voc_algorithm_adaptive_lowpass_process(params, params->mVoc_Index); - if ((params->mVoc_Index < F16(0.5))) { - params->mVoc_Index = F16(0.5); - } - if ((params->mSraw > F16(0.))) { - voc_algorithm_mean_variance_estimator_process(params, params->mSraw, params->mVoc_Index); - voc_algorithm_mox_model_set_parameters(params, voc_algorithm_mean_variance_estimator_get_std(params), - voc_algorithm_mean_variance_estimator_get_mean(params)); - } - } - *voc_index = (fix16_cast_to_int((params->mVoc_Index + F16(0.5)))); -} - -static void voc_algorithm_mean_variance_estimator_init(VocAlgorithmParams *params) { - voc_algorithm_mean_variance_estimator_set_parameters(params, F16(0.), F16(0.), F16(0.)); - voc_algorithm_mean_variance_estimator_init_instances(params); -} - -static void voc_algorithm_mean_variance_estimator_init_instances(VocAlgorithmParams *params) { - voc_algorithm_mean_variance_estimator_sigmoid_init(params); -} - -static void voc_algorithm_mean_variance_estimator_set_parameters(VocAlgorithmParams *params, fix16_t std_initial, - fix16_t tau_mean_variance_hours, - fix16_t gating_max_duration_minutes) { - params->m_Mean_Variance_Estimator_Gating_Max_Duration_Minutes = gating_max_duration_minutes; - params->m_Mean_Variance_Estimator_Initialized = false; - params->m_Mean_Variance_Estimator_Mean = F16(0.); - params->m_Mean_Variance_Estimator_Sraw_Offset = F16(0.); - params->m_Mean_Variance_Estimator_Std = std_initial; - params->m_Mean_Variance_Estimator_Gamma = - (fix16_div(F16((VOC_ALGORITHM_MEAN_VARIANCE_ESTIMATOR_GAMMA_SCALING * (VOC_ALGORITHM_SAMPLING_INTERVAL / 3600.))), - (tau_mean_variance_hours + F16((VOC_ALGORITHM_SAMPLING_INTERVAL / 3600.))))); - params->m_Mean_Variance_Estimator_Gamma_Initial_Mean = - F16(((VOC_ALGORITHM_MEAN_VARIANCE_ESTIMATOR_GAMMA_SCALING * VOC_ALGORITHM_SAMPLING_INTERVAL) / - (VOC_ALGORITHM_TAU_INITIAL_MEAN + VOC_ALGORITHM_SAMPLING_INTERVAL))); - params->m_Mean_Variance_Estimator_Gamma_Initial_Variance = - F16(((VOC_ALGORITHM_MEAN_VARIANCE_ESTIMATOR_GAMMA_SCALING * VOC_ALGORITHM_SAMPLING_INTERVAL) / - (VOC_ALGORITHM_TAU_INITIAL_VARIANCE + VOC_ALGORITHM_SAMPLING_INTERVAL))); - params->m_Mean_Variance_Estimator_Gamma_Mean = F16(0.); - params->m_Mean_Variance_Estimator_Gamma_Variance = F16(0.); - params->m_Mean_Variance_Estimator_Uptime_Gamma = F16(0.); - params->m_Mean_Variance_Estimator_Uptime_Gating = F16(0.); - params->m_Mean_Variance_Estimator_Gating_Duration_Minutes = F16(0.); -} - -static void voc_algorithm_mean_variance_estimator_set_states(VocAlgorithmParams *params, fix16_t mean, fix16_t std, - fix16_t uptime_gamma) { - params->m_Mean_Variance_Estimator_Mean = mean; - params->m_Mean_Variance_Estimator_Std = std; - params->m_Mean_Variance_Estimator_Uptime_Gamma = uptime_gamma; - params->m_Mean_Variance_Estimator_Initialized = true; -} - -static fix16_t voc_algorithm_mean_variance_estimator_get_std(VocAlgorithmParams *params) { - return params->m_Mean_Variance_Estimator_Std; -} - -static fix16_t voc_algorithm_mean_variance_estimator_get_mean(VocAlgorithmParams *params) { - return (params->m_Mean_Variance_Estimator_Mean + params->m_Mean_Variance_Estimator_Sraw_Offset); -} - -static void voc_algorithm_mean_variance_estimator_calculate_gamma(VocAlgorithmParams *params, - fix16_t voc_index_from_prior) { - fix16_t uptime_limit; - fix16_t sigmoid_gamma_mean; - fix16_t gamma_mean; - fix16_t gating_threshold_mean; - fix16_t sigmoid_gating_mean; - fix16_t sigmoid_gamma_variance; - fix16_t gamma_variance; - fix16_t gating_threshold_variance; - fix16_t sigmoid_gating_variance; - - uptime_limit = F16((VOC_ALGORITHM_MEAN_VARIANCE_ESTIMATOR_FI_X16_MAX - VOC_ALGORITHM_SAMPLING_INTERVAL)); - if ((params->m_Mean_Variance_Estimator_Uptime_Gamma < uptime_limit)) { - params->m_Mean_Variance_Estimator_Uptime_Gamma = - (params->m_Mean_Variance_Estimator_Uptime_Gamma + F16(VOC_ALGORITHM_SAMPLING_INTERVAL)); - } - if ((params->m_Mean_Variance_Estimator_Uptime_Gating < uptime_limit)) { - params->m_Mean_Variance_Estimator_Uptime_Gating = - (params->m_Mean_Variance_Estimator_Uptime_Gating + F16(VOC_ALGORITHM_SAMPLING_INTERVAL)); - } - voc_algorithm_mean_variance_estimator_sigmoid_set_parameters(params, F16(1.), F16(VOC_ALGORITHM_INIT_DURATION_MEAN), - F16(VOC_ALGORITHM_INIT_TRANSITION_MEAN)); - sigmoid_gamma_mean = - voc_algorithm_mean_variance_estimator_sigmoid_process(params, params->m_Mean_Variance_Estimator_Uptime_Gamma); - gamma_mean = - (params->m_Mean_Variance_Estimator_Gamma + - (fix16_mul((params->m_Mean_Variance_Estimator_Gamma_Initial_Mean - params->m_Mean_Variance_Estimator_Gamma), - sigmoid_gamma_mean))); - gating_threshold_mean = (F16(VOC_ALGORITHM_GATING_THRESHOLD) + - (fix16_mul(F16((VOC_ALGORITHM_GATING_THRESHOLD_INITIAL - VOC_ALGORITHM_GATING_THRESHOLD)), - voc_algorithm_mean_variance_estimator_sigmoid_process( - params, params->m_Mean_Variance_Estimator_Uptime_Gating)))); - voc_algorithm_mean_variance_estimator_sigmoid_set_parameters(params, F16(1.), gating_threshold_mean, - F16(VOC_ALGORITHM_GATING_THRESHOLD_TRANSITION)); - sigmoid_gating_mean = voc_algorithm_mean_variance_estimator_sigmoid_process(params, voc_index_from_prior); - params->m_Mean_Variance_Estimator_Gamma_Mean = (fix16_mul(sigmoid_gating_mean, gamma_mean)); - voc_algorithm_mean_variance_estimator_sigmoid_set_parameters( - params, F16(1.), F16(VOC_ALGORITHM_INIT_DURATION_VARIANCE), F16(VOC_ALGORITHM_INIT_TRANSITION_VARIANCE)); - sigmoid_gamma_variance = - voc_algorithm_mean_variance_estimator_sigmoid_process(params, params->m_Mean_Variance_Estimator_Uptime_Gamma); - gamma_variance = - (params->m_Mean_Variance_Estimator_Gamma + - (fix16_mul((params->m_Mean_Variance_Estimator_Gamma_Initial_Variance - params->m_Mean_Variance_Estimator_Gamma), - (sigmoid_gamma_variance - sigmoid_gamma_mean)))); - gating_threshold_variance = - (F16(VOC_ALGORITHM_GATING_THRESHOLD) + - (fix16_mul(F16((VOC_ALGORITHM_GATING_THRESHOLD_INITIAL - VOC_ALGORITHM_GATING_THRESHOLD)), - voc_algorithm_mean_variance_estimator_sigmoid_process( - params, params->m_Mean_Variance_Estimator_Uptime_Gating)))); - voc_algorithm_mean_variance_estimator_sigmoid_set_parameters(params, F16(1.), gating_threshold_variance, - F16(VOC_ALGORITHM_GATING_THRESHOLD_TRANSITION)); - sigmoid_gating_variance = voc_algorithm_mean_variance_estimator_sigmoid_process(params, voc_index_from_prior); - params->m_Mean_Variance_Estimator_Gamma_Variance = (fix16_mul(sigmoid_gating_variance, gamma_variance)); - params->m_Mean_Variance_Estimator_Gating_Duration_Minutes = - (params->m_Mean_Variance_Estimator_Gating_Duration_Minutes + - (fix16_mul(F16((VOC_ALGORITHM_SAMPLING_INTERVAL / 60.)), - ((fix16_mul((F16(1.) - sigmoid_gating_mean), F16((1. + VOC_ALGORITHM_GATING_MAX_RATIO)))) - - F16(VOC_ALGORITHM_GATING_MAX_RATIO))))); - if ((params->m_Mean_Variance_Estimator_Gating_Duration_Minutes < F16(0.))) { - params->m_Mean_Variance_Estimator_Gating_Duration_Minutes = F16(0.); - } - if ((params->m_Mean_Variance_Estimator_Gating_Duration_Minutes > - params->m_Mean_Variance_Estimator_Gating_Max_Duration_Minutes)) { - params->m_Mean_Variance_Estimator_Uptime_Gating = F16(0.); - } -} - -static void voc_algorithm_mean_variance_estimator_process(VocAlgorithmParams *params, fix16_t sraw, - fix16_t voc_index_from_prior) { - fix16_t delta_sgp; - fix16_t c; - fix16_t additional_scaling; - - if ((!params->m_Mean_Variance_Estimator_Initialized)) { - params->m_Mean_Variance_Estimator_Initialized = true; - params->m_Mean_Variance_Estimator_Sraw_Offset = sraw; - params->m_Mean_Variance_Estimator_Mean = F16(0.); - } else { - if (((params->m_Mean_Variance_Estimator_Mean >= F16(100.)) || - (params->m_Mean_Variance_Estimator_Mean <= F16(-100.)))) { - params->m_Mean_Variance_Estimator_Sraw_Offset = - (params->m_Mean_Variance_Estimator_Sraw_Offset + params->m_Mean_Variance_Estimator_Mean); - params->m_Mean_Variance_Estimator_Mean = F16(0.); - } - sraw = (sraw - params->m_Mean_Variance_Estimator_Sraw_Offset); - voc_algorithm_mean_variance_estimator_calculate_gamma(params, voc_index_from_prior); - delta_sgp = (fix16_div((sraw - params->m_Mean_Variance_Estimator_Mean), - F16(VOC_ALGORITHM_MEAN_VARIANCE_ESTIMATOR_GAMMA_SCALING))); - if ((delta_sgp < F16(0.))) { - c = (params->m_Mean_Variance_Estimator_Std - delta_sgp); - } else { - c = (params->m_Mean_Variance_Estimator_Std + delta_sgp); - } - additional_scaling = F16(1.); - if ((c > F16(1440.))) { - additional_scaling = F16(4.); - } - params->m_Mean_Variance_Estimator_Std = (fix16_mul( - fix16_sqrt((fix16_mul(additional_scaling, (F16(VOC_ALGORITHM_MEAN_VARIANCE_ESTIMATOR_GAMMA_SCALING) - - params->m_Mean_Variance_Estimator_Gamma_Variance)))), - fix16_sqrt(((fix16_mul(params->m_Mean_Variance_Estimator_Std, - (fix16_div(params->m_Mean_Variance_Estimator_Std, - (fix16_mul(F16(VOC_ALGORITHM_MEAN_VARIANCE_ESTIMATOR_GAMMA_SCALING), - additional_scaling)))))) + - (fix16_mul((fix16_div((fix16_mul(params->m_Mean_Variance_Estimator_Gamma_Variance, delta_sgp)), - additional_scaling)), - delta_sgp)))))); - params->m_Mean_Variance_Estimator_Mean = - (params->m_Mean_Variance_Estimator_Mean + (fix16_mul(params->m_Mean_Variance_Estimator_Gamma_Mean, delta_sgp))); - } -} - -static void voc_algorithm_mean_variance_estimator_sigmoid_init(VocAlgorithmParams *params) { - voc_algorithm_mean_variance_estimator_sigmoid_set_parameters(params, F16(0.), F16(0.), F16(0.)); -} - -static void voc_algorithm_mean_variance_estimator_sigmoid_set_parameters(VocAlgorithmParams *params, fix16_t l, - fix16_t x0, fix16_t k) { - params->m_Mean_Variance_Estimator_Sigmoid_L = l; - params->m_Mean_Variance_Estimator_Sigmoid_K = k; - params->m_Mean_Variance_Estimator_Sigmoid_X0 = x0; -} - -static fix16_t voc_algorithm_mean_variance_estimator_sigmoid_process(VocAlgorithmParams *params, fix16_t sample) { - fix16_t x; - - x = (fix16_mul(params->m_Mean_Variance_Estimator_Sigmoid_K, (sample - params->m_Mean_Variance_Estimator_Sigmoid_X0))); - if ((x < F16(-50.))) { - return params->m_Mean_Variance_Estimator_Sigmoid_L; - } else if ((x > F16(50.))) { - return F16(0.); - } else { - return (fix16_div(params->m_Mean_Variance_Estimator_Sigmoid_L, (F16(1.) + fix16_exp(x)))); - } -} - -static void voc_algorithm_mox_model_init(VocAlgorithmParams *params) { - voc_algorithm_mox_model_set_parameters(params, F16(1.), F16(0.)); -} - -static void voc_algorithm_mox_model_set_parameters(VocAlgorithmParams *params, fix16_t sraw_std, fix16_t sraw_mean) { - params->m_Mox_Model_Sraw_Std = sraw_std; - params->m_Mox_Model_Sraw_Mean = sraw_mean; -} - -static fix16_t voc_algorithm_mox_model_process(VocAlgorithmParams *params, fix16_t sraw) { - return (fix16_mul((fix16_div((sraw - params->m_Mox_Model_Sraw_Mean), - (-(params->m_Mox_Model_Sraw_Std + F16(VOC_ALGORITHM_SRAW_STD_BONUS))))), - F16(VOC_ALGORITHM_VOC_INDEX_GAIN))); -} - -static void voc_algorithm_sigmoid_scaled_init(VocAlgorithmParams *params) { - voc_algorithm_sigmoid_scaled_set_parameters(params, F16(0.)); -} - -static void voc_algorithm_sigmoid_scaled_set_parameters(VocAlgorithmParams *params, fix16_t offset) { - params->m_Sigmoid_Scaled_Offset = offset; -} - -static fix16_t voc_algorithm_sigmoid_scaled_process(VocAlgorithmParams *params, fix16_t sample) { - fix16_t x; - fix16_t shift; - - x = (fix16_mul(F16(VOC_ALGORITHM_SIGMOID_K), (sample - F16(VOC_ALGORITHM_SIGMOID_X0)))); - if ((x < F16(-50.))) { - return F16(VOC_ALGORITHM_SIGMOID_L); - } else if ((x > F16(50.))) { - return F16(0.); - } else { - if ((sample >= F16(0.))) { - shift = - (fix16_div((F16(VOC_ALGORITHM_SIGMOID_L) - (fix16_mul(F16(5.), params->m_Sigmoid_Scaled_Offset))), F16(4.))); - return ((fix16_div((F16(VOC_ALGORITHM_SIGMOID_L) + shift), (F16(1.) + fix16_exp(x)))) - shift); - } else { - return (fix16_mul((fix16_div(params->m_Sigmoid_Scaled_Offset, F16(VOC_ALGORITHM_VOC_INDEX_OFFSET_DEFAULT))), - (fix16_div(F16(VOC_ALGORITHM_SIGMOID_L), (F16(1.) + fix16_exp(x)))))); - } - } -} - -static void voc_algorithm_adaptive_lowpass_init(VocAlgorithmParams *params) { - voc_algorithm_adaptive_lowpass_set_parameters(params); -} - -static void voc_algorithm_adaptive_lowpass_set_parameters(VocAlgorithmParams *params) { - params->m_Adaptive_Lowpass_A1 = - F16((VOC_ALGORITHM_SAMPLING_INTERVAL / (VOC_ALGORITHM_LP_TAU_FAST + VOC_ALGORITHM_SAMPLING_INTERVAL))); - params->m_Adaptive_Lowpass_A2 = - F16((VOC_ALGORITHM_SAMPLING_INTERVAL / (VOC_ALGORITHM_LP_TAU_SLOW + VOC_ALGORITHM_SAMPLING_INTERVAL))); - params->m_Adaptive_Lowpass_Initialized = false; -} - -static fix16_t voc_algorithm_adaptive_lowpass_process(VocAlgorithmParams *params, fix16_t sample) { - fix16_t abs_delta; - fix16_t f1; - fix16_t tau_a; - fix16_t a3; - - if ((!params->m_Adaptive_Lowpass_Initialized)) { - params->m_Adaptive_Lowpass_X1 = sample; - params->m_Adaptive_Lowpass_X2 = sample; - params->m_Adaptive_Lowpass_X3 = sample; - params->m_Adaptive_Lowpass_Initialized = true; - } - params->m_Adaptive_Lowpass_X1 = - ((fix16_mul((F16(1.) - params->m_Adaptive_Lowpass_A1), params->m_Adaptive_Lowpass_X1)) + - (fix16_mul(params->m_Adaptive_Lowpass_A1, sample))); - params->m_Adaptive_Lowpass_X2 = - ((fix16_mul((F16(1.) - params->m_Adaptive_Lowpass_A2), params->m_Adaptive_Lowpass_X2)) + - (fix16_mul(params->m_Adaptive_Lowpass_A2, sample))); - abs_delta = (params->m_Adaptive_Lowpass_X1 - params->m_Adaptive_Lowpass_X2); - if ((abs_delta < F16(0.))) { - abs_delta = (-abs_delta); - } - f1 = fix16_exp((fix16_mul(F16(VOC_ALGORITHM_LP_ALPHA), abs_delta))); - tau_a = - ((fix16_mul(F16((VOC_ALGORITHM_LP_TAU_SLOW - VOC_ALGORITHM_LP_TAU_FAST)), f1)) + F16(VOC_ALGORITHM_LP_TAU_FAST)); - a3 = (fix16_div(F16(VOC_ALGORITHM_SAMPLING_INTERVAL), (F16(VOC_ALGORITHM_SAMPLING_INTERVAL) + tau_a))); - params->m_Adaptive_Lowpass_X3 = - ((fix16_mul((F16(1.) - a3), params->m_Adaptive_Lowpass_X3)) + (fix16_mul(a3, sample))); - return params->m_Adaptive_Lowpass_X3; -} -} // namespace sgp40 -} // namespace esphome diff --git a/esphome/components/sgp40/sensirion_voc_algorithm.h b/esphome/components/sgp40/sensirion_voc_algorithm.h deleted file mode 100644 index adef6b29e8..0000000000 --- a/esphome/components/sgp40/sensirion_voc_algorithm.h +++ /dev/null @@ -1,147 +0,0 @@ -#pragma once -#include -namespace esphome { -namespace sgp40 { - -/* The VOC code were originally created by - * https://github.com/Sensirion/embedded-sgp - * The fixed point arithmetic parts of this code were originally created by - * https://github.com/PetteriAimonen/libfixmath - */ - -using fix16_t = int32_t; - -#define F16(x) ((fix16_t)(((x) >= 0) ? ((x) *65536.0 + 0.5) : ((x) *65536.0 - 0.5))) - -static const float VOC_ALGORITHM_SAMPLING_INTERVAL(1.); -static const float VOC_ALGORITHM_INITIAL_BLACKOUT(45.); -static const float VOC_ALGORITHM_VOC_INDEX_GAIN(230.); -static const float VOC_ALGORITHM_SRAW_STD_INITIAL(50.); -static const float VOC_ALGORITHM_SRAW_STD_BONUS(220.); -static const float VOC_ALGORITHM_TAU_MEAN_VARIANCE_HOURS(12.); -static const float VOC_ALGORITHM_TAU_INITIAL_MEAN(20.); -static const float VOC_ALGORITHM_INIT_DURATION_MEAN((3600. * 0.75)); -static const float VOC_ALGORITHM_INIT_TRANSITION_MEAN(0.01); -static const float VOC_ALGORITHM_TAU_INITIAL_VARIANCE(2500.); -static const float VOC_ALGORITHM_INIT_DURATION_VARIANCE((3600. * 1.45)); -static const float VOC_ALGORITHM_INIT_TRANSITION_VARIANCE(0.01); -static const float VOC_ALGORITHM_GATING_THRESHOLD(340.); -static const float VOC_ALGORITHM_GATING_THRESHOLD_INITIAL(510.); -static const float VOC_ALGORITHM_GATING_THRESHOLD_TRANSITION(0.09); -static const float VOC_ALGORITHM_GATING_MAX_DURATION_MINUTES((60. * 3.)); -static const float VOC_ALGORITHM_GATING_MAX_RATIO(0.3); -static const float VOC_ALGORITHM_SIGMOID_L(500.); -static const float VOC_ALGORITHM_SIGMOID_K(-0.0065); -static const float VOC_ALGORITHM_SIGMOID_X0(213.); -static const float VOC_ALGORITHM_VOC_INDEX_OFFSET_DEFAULT(100.); -static const float VOC_ALGORITHM_LP_TAU_FAST(20.0); -static const float VOC_ALGORITHM_LP_TAU_SLOW(500.0); -static const float VOC_ALGORITHM_LP_ALPHA(-0.2); -static const float VOC_ALGORITHM_PERSISTENCE_UPTIME_GAMMA((3. * 3600.)); -static const float VOC_ALGORITHM_MEAN_VARIANCE_ESTIMATOR_GAMMA_SCALING(64.); -static const float VOC_ALGORITHM_MEAN_VARIANCE_ESTIMATOR_FI_X16_MAX(32767.); - -/** - * Struct to hold all the states of the VOC algorithm. - */ -struct VocAlgorithmParams { - fix16_t mVoc_Index_Offset; - fix16_t mTau_Mean_Variance_Hours; - fix16_t mGating_Max_Duration_Minutes; - fix16_t mSraw_Std_Initial; - fix16_t mUptime; - fix16_t mSraw; - fix16_t mVoc_Index; - fix16_t m_Mean_Variance_Estimator_Gating_Max_Duration_Minutes; - bool m_Mean_Variance_Estimator_Initialized; - fix16_t m_Mean_Variance_Estimator_Mean; - fix16_t m_Mean_Variance_Estimator_Sraw_Offset; - fix16_t m_Mean_Variance_Estimator_Std; - fix16_t m_Mean_Variance_Estimator_Gamma; - fix16_t m_Mean_Variance_Estimator_Gamma_Initial_Mean; - fix16_t m_Mean_Variance_Estimator_Gamma_Initial_Variance; - fix16_t m_Mean_Variance_Estimator_Gamma_Mean; - fix16_t m_Mean_Variance_Estimator_Gamma_Variance; - fix16_t m_Mean_Variance_Estimator_Uptime_Gamma; - fix16_t m_Mean_Variance_Estimator_Uptime_Gating; - fix16_t m_Mean_Variance_Estimator_Gating_Duration_Minutes; - fix16_t m_Mean_Variance_Estimator_Sigmoid_L; - fix16_t m_Mean_Variance_Estimator_Sigmoid_K; - fix16_t m_Mean_Variance_Estimator_Sigmoid_X0; - fix16_t m_Mox_Model_Sraw_Std; - fix16_t m_Mox_Model_Sraw_Mean; - fix16_t m_Sigmoid_Scaled_Offset; - fix16_t m_Adaptive_Lowpass_A1; - fix16_t m_Adaptive_Lowpass_A2; - bool m_Adaptive_Lowpass_Initialized; - fix16_t m_Adaptive_Lowpass_X1; - fix16_t m_Adaptive_Lowpass_X2; - fix16_t m_Adaptive_Lowpass_X3; -}; - -/** - * Initialize the VOC algorithm parameters. Call this once at the beginning or - * whenever the sensor stopped measurements. - * @param params Pointer to the VocAlgorithmParams struct - */ -void voc_algorithm_init(VocAlgorithmParams *params); - -/** - * Get current algorithm states. Retrieved values can be used in - * voc_algorithm_set_states() to resume operation after a short interruption, - * skipping initial learning phase. This feature can only be used after at least - * 3 hours of continuous operation. - * @param params Pointer to the VocAlgorithmParams struct - * @param state0 State0 to be stored - * @param state1 State1 to be stored - */ -void voc_algorithm_get_states(VocAlgorithmParams *params, int32_t *state0, int32_t *state1); - -/** - * Set previously retrieved algorithm states to resume operation after a short - * interruption, skipping initial learning phase. This feature should not be - * used after inerruptions of more than 10 minutes. Call this once after - * voc_algorithm_init() and the optional voc_algorithm_set_tuning_parameters(), if - * desired. Otherwise, the algorithm will start with initial learning phase. - * @param params Pointer to the VocAlgorithmParams struct - * @param state0 State0 to be restored - * @param state1 State1 to be restored - */ -void voc_algorithm_set_states(VocAlgorithmParams *params, int32_t state0, int32_t state1); - -/** - * Set parameters to customize the VOC algorithm. Call this once after - * voc_algorithm_init(), if desired. Otherwise, the default values will be used. - * - * @param params Pointer to the VocAlgorithmParams struct - * @param voc_index_offset VOC index representing typical (average) - * conditions. Range 1..250, default 100 - * @param learning_time_hours Time constant of long-term estimator. - * Past events will be forgotten after about - * twice the learning time. - * Range 1..72 [hours], default 12 [hours] - * @param gating_max_duration_minutes Maximum duration of gating (freeze of - * estimator during high VOC index signal). - * 0 (no gating) or range 1..720 [minutes], - * default 180 [minutes] - * @param std_initial Initial estimate for standard deviation. - * Lower value boosts events during initial - * learning period, but may result in larger - * device-to-device variations. - * Range 10..500, default 50 - */ -void voc_algorithm_set_tuning_parameters(VocAlgorithmParams *params, int32_t voc_index_offset, - int32_t learning_time_hours, int32_t gating_max_duration_minutes, - int32_t std_initial); - -/** - * Calculate the VOC index value from the raw sensor value. - * - * @param params Pointer to the VocAlgorithmParams struct - * @param sraw Raw value from the SGP40 sensor - * @param voc_index Calculated VOC index value from the raw sensor value. Zero - * during initial blackout period and 1..500 afterwards - */ -void voc_algorithm_process(VocAlgorithmParams *params, int32_t sraw, int32_t *voc_index); -} // namespace sgp40 -} // namespace esphome diff --git a/esphome/components/sgp40/sensor.py b/esphome/components/sgp40/sensor.py index ee267d6062..cb4231c168 100644 --- a/esphome/components/sgp40/sensor.py +++ b/esphome/components/sgp40/sensor.py @@ -1,70 +1,8 @@ -import esphome.codegen as cg import esphome.config_validation as cv -from esphome.components import i2c, sensor, sensirion_common - -from esphome.const import ( - CONF_STORE_BASELINE, - CONF_TEMPERATURE_SOURCE, - ICON_RADIATOR, - DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, - STATE_CLASS_MEASUREMENT, -) - -DEPENDENCIES = ["i2c"] -AUTO_LOAD = ["sensirion_common"] CODEOWNERS = ["@SenexCrenshaw"] -sgp40_ns = cg.esphome_ns.namespace("sgp40") -SGP40Component = sgp40_ns.class_( - "SGP40Component", - sensor.Sensor, - cg.PollingComponent, - sensirion_common.SensirionI2CDevice, +CONFIG_SCHEMA = CONFIG_SCHEMA = cv.invalid( + "SGP40 is deprecated.\nPlease use the SGP4x platform instead.\nSGP4x supports both SPG40 and SGP41.\n" + " See https://esphome.io/components/sensor/sgp4x.html" ) - -CONF_COMPENSATION = "compensation" -CONF_HUMIDITY_SOURCE = "humidity_source" -CONF_VOC_BASELINE = "voc_baseline" - -CONFIG_SCHEMA = ( - sensor.sensor_schema( - SGP40Component, - icon=ICON_RADIATOR, - accuracy_decimals=0, - device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, - state_class=STATE_CLASS_MEASUREMENT, - ) - .extend( - { - cv.Optional(CONF_STORE_BASELINE, default=True): cv.boolean, - cv.Optional(CONF_VOC_BASELINE): cv.hex_uint16_t, - cv.Optional(CONF_COMPENSATION): cv.Schema( - { - cv.Required(CONF_HUMIDITY_SOURCE): cv.use_id(sensor.Sensor), - cv.Required(CONF_TEMPERATURE_SOURCE): cv.use_id(sensor.Sensor), - }, - ), - } - ) - .extend(cv.polling_component_schema("60s")) - .extend(i2c.i2c_device_schema(0x59)) -) - - -async def to_code(config): - var = await sensor.new_sensor(config) - await cg.register_component(var, config) - await i2c.register_i2c_device(var, config) - - if CONF_COMPENSATION in config: - compensation_config = config[CONF_COMPENSATION] - sens = await cg.get_variable(compensation_config[CONF_HUMIDITY_SOURCE]) - cg.add(var.set_humidity_sensor(sens)) - sens = await cg.get_variable(compensation_config[CONF_TEMPERATURE_SOURCE]) - cg.add(var.set_temperature_sensor(sens)) - - cg.add(var.set_store_baseline(config[CONF_STORE_BASELINE])) - - if CONF_VOC_BASELINE in config: - cg.add(var.set_voc_baseline(CONF_VOC_BASELINE)) diff --git a/esphome/components/sgp40/sgp40.cpp b/esphome/components/sgp40/sgp40.cpp deleted file mode 100644 index 9d78572b50..0000000000 --- a/esphome/components/sgp40/sgp40.cpp +++ /dev/null @@ -1,274 +0,0 @@ -#include "sgp40.h" -#include "esphome/core/log.h" -#include "esphome/core/hal.h" -#include - -namespace esphome { -namespace sgp40 { - -static const char *const TAG = "sgp40"; - -void SGP40Component::setup() { - ESP_LOGCONFIG(TAG, "Setting up SGP40..."); - - // Serial Number identification - if (!this->write_command(SGP40_CMD_GET_SERIAL_ID)) { - this->error_code_ = COMMUNICATION_FAILED; - this->mark_failed(); - return; - } - uint16_t raw_serial_number[3]; - - if (!this->read_data(raw_serial_number, 3)) { - this->mark_failed(); - return; - } - this->serial_number_ = (uint64_t(raw_serial_number[0]) << 24) | (uint64_t(raw_serial_number[1]) << 16) | - (uint64_t(raw_serial_number[2])); - ESP_LOGD(TAG, "Serial Number: %" PRIu64, this->serial_number_); - - // Featureset identification for future use - if (!this->write_command(SGP40_CMD_GET_FEATURESET)) { - ESP_LOGD(TAG, "raw_featureset write_command_ failed"); - this->mark_failed(); - return; - } - uint16_t raw_featureset; - if (!this->read_data(raw_featureset)) { - ESP_LOGD(TAG, "raw_featureset read_data_ failed"); - this->mark_failed(); - return; - } - - this->featureset_ = raw_featureset; - if ((this->featureset_ & 0x1FF) != SGP40_FEATURESET) { - ESP_LOGD(TAG, "Product feature set failed 0x%0X , expecting 0x%0X", uint16_t(this->featureset_ & 0x1FF), - SGP40_FEATURESET); - this->mark_failed(); - return; - } - - ESP_LOGD(TAG, "Product version: 0x%0X", uint16_t(this->featureset_ & 0x1FF)); - - voc_algorithm_init(&this->voc_algorithm_params_); - - if (this->store_baseline_) { - // Hash with compilation time - // This ensures the baseline storage is cleared after OTA - uint32_t hash = fnv1_hash(App.get_compilation_time()); - this->pref_ = global_preferences->make_preference(hash, true); - - if (this->pref_.load(&this->baselines_storage_)) { - this->state0_ = this->baselines_storage_.state0; - this->state1_ = this->baselines_storage_.state1; - ESP_LOGI(TAG, "Loaded VOC baseline state0: 0x%04X, state1: 0x%04X", this->baselines_storage_.state0, - baselines_storage_.state1); - } - - // Initialize storage timestamp - this->seconds_since_last_store_ = 0; - - if (this->baselines_storage_.state0 > 0 && this->baselines_storage_.state1 > 0) { - ESP_LOGI(TAG, "Setting VOC baseline from save state0: 0x%04X, state1: 0x%04X", this->baselines_storage_.state0, - baselines_storage_.state1); - voc_algorithm_set_states(&this->voc_algorithm_params_, this->baselines_storage_.state0, - this->baselines_storage_.state1); - } - } - - this->self_test_(); - - /* The official spec for this sensor at https://docs.rs-online.com/1956/A700000007055193.pdf - indicates this sensor should be driven at 1Hz. Comments from the developers at: - https://github.com/Sensirion/embedded-sgp/issues/136 indicate the algorithm should be a bit - resilient to slight timing variations so the software timer should be accurate enough for - this. - - This block starts sampling from the sensor at 1Hz, and is done seperately from the call - to the update method. This seperation is to support getting accurate measurements but - limit the amount of communication done over wifi for power consumption or to keep the - number of records reported from being overwhelming. - */ - ESP_LOGD(TAG, "Component requires sampling of 1Hz, setting up background sampler"); - this->set_interval(1000, [this]() { this->update_voc_index(); }); -} - -void SGP40Component::self_test_() { - ESP_LOGD(TAG, "Self-test started"); - if (!this->write_command(SGP40_CMD_SELF_TEST)) { - this->error_code_ = COMMUNICATION_FAILED; - ESP_LOGD(TAG, "Self-test communication failed"); - this->mark_failed(); - } - - this->set_timeout(250, [this]() { - uint16_t reply; - if (!this->read_data(reply)) { - ESP_LOGD(TAG, "Self-test read_data_ failed"); - this->mark_failed(); - return; - } - - if (reply == 0xD400) { - this->self_test_complete_ = true; - ESP_LOGD(TAG, "Self-test completed"); - return; - } - - ESP_LOGD(TAG, "Self-test failed"); - this->mark_failed(); - }); -} - -/** - * @brief Combined the measured gasses, temperature, and humidity - * to calculate the VOC Index - * - * @param temperature The measured temperature in degrees C - * @param humidity The measured relative humidity in % rH - * @return int32_t The VOC Index - */ -int32_t SGP40Component::measure_voc_index_() { - int32_t voc_index; - - uint16_t sraw = measure_raw_(); - - if (sraw == UINT16_MAX) - return UINT16_MAX; - - this->status_clear_warning(); - - voc_algorithm_process(&voc_algorithm_params_, sraw, &voc_index); - - // Store baselines after defined interval or if the difference between current and stored baseline becomes too - // much - if (this->store_baseline_ && this->seconds_since_last_store_ > SHORTEST_BASELINE_STORE_INTERVAL) { - voc_algorithm_get_states(&voc_algorithm_params_, &this->state0_, &this->state1_); - if ((uint32_t) abs(this->baselines_storage_.state0 - this->state0_) > MAXIMUM_STORAGE_DIFF || - (uint32_t) abs(this->baselines_storage_.state1 - this->state1_) > MAXIMUM_STORAGE_DIFF) { - this->seconds_since_last_store_ = 0; - this->baselines_storage_.state0 = this->state0_; - this->baselines_storage_.state1 = this->state1_; - - if (this->pref_.save(&this->baselines_storage_)) { - ESP_LOGI(TAG, "Stored VOC baseline state0: 0x%04X ,state1: 0x%04X", this->baselines_storage_.state0, - baselines_storage_.state1); - } else { - ESP_LOGW(TAG, "Could not store VOC baselines"); - } - } - } - - return voc_index; -} - -/** - * @brief Return the raw gas measurement - * - * @param temperature The measured temperature in degrees C - * @param humidity The measured relative humidity in % rH - * @return uint16_t The current raw gas measurement - */ -uint16_t SGP40Component::measure_raw_() { - float humidity = NAN; - - if (!this->self_test_complete_) { - ESP_LOGD(TAG, "Self-test not yet complete"); - return UINT16_MAX; - } - - if (this->humidity_sensor_ != nullptr) { - humidity = this->humidity_sensor_->state; - } - if (std::isnan(humidity) || humidity < 0.0f || humidity > 100.0f) { - humidity = 50; - } - - float temperature = NAN; - if (this->temperature_sensor_ != nullptr) { - temperature = float(this->temperature_sensor_->state); - } - if (std::isnan(temperature) || temperature < -40.0f || temperature > 85.0f) { - temperature = 25; - } - - uint16_t data[2]; - uint16_t rhticks = llround((uint16_t)((humidity * 65535) / 100)); - uint16_t tempticks = (uint16_t)(((temperature + 45) * 65535) / 175); - // first paramater is the relative humidity ticks - data[0] = rhticks; - // second paramater is the temperature ticks - data[1] = tempticks; - - if (!this->write_command(SGP40_CMD_MEASURE_RAW, data, 2)) { - this->status_set_warning(); - ESP_LOGD(TAG, "write error (%d)", this->last_error_); - return false; - } - delay(30); - - uint16_t raw_data; - if (!this->read_data(raw_data)) { - this->status_set_warning(); - ESP_LOGD(TAG, "read_data_ error"); - return UINT16_MAX; - } - return raw_data; -} - -void SGP40Component::update_voc_index() { - this->seconds_since_last_store_ += 1; - - this->voc_index_ = this->measure_voc_index_(); - if (this->samples_read_ < this->samples_to_stabalize_) { - this->samples_read_++; - ESP_LOGD(TAG, "Sensor has not collected enough samples yet. (%d/%d) VOC index is: %u", this->samples_read_, - this->samples_to_stabalize_, this->voc_index_); - return; - } -} - -void SGP40Component::update() { - if (this->samples_read_ < this->samples_to_stabalize_) { - return; - } - - if (this->voc_index_ != UINT16_MAX) { - this->status_clear_warning(); - this->publish_state(this->voc_index_); - } else { - this->status_set_warning(); - } -} - -void SGP40Component::dump_config() { - ESP_LOGCONFIG(TAG, "SGP40:"); - LOG_I2C_DEVICE(this); - ESP_LOGCONFIG(TAG, " store_baseline: %d", this->store_baseline_); - - if (this->is_failed()) { - switch (this->error_code_) { - case COMMUNICATION_FAILED: - ESP_LOGW(TAG, "Communication failed! Is the sensor connected?"); - break; - default: - ESP_LOGW(TAG, "Unknown setup error!"); - break; - } - } else { - ESP_LOGCONFIG(TAG, " Serial number: %" PRIu64, this->serial_number_); - ESP_LOGCONFIG(TAG, " Minimum Samples: %f", VOC_ALGORITHM_INITIAL_BLACKOUT); - } - LOG_UPDATE_INTERVAL(this); - - if (this->humidity_sensor_ != nullptr && this->temperature_sensor_ != nullptr) { - ESP_LOGCONFIG(TAG, " Compensation:"); - LOG_SENSOR(" ", "Temperature Source:", this->temperature_sensor_); - LOG_SENSOR(" ", "Humidity Source:", this->humidity_sensor_); - } else { - ESP_LOGCONFIG(TAG, " Compensation: No source configured"); - } -} - -} // namespace sgp40 -} // namespace esphome diff --git a/esphome/components/sgp40/sgp40.h b/esphome/components/sgp40/sgp40.h deleted file mode 100644 index c5b7d2dfa0..0000000000 --- a/esphome/components/sgp40/sgp40.h +++ /dev/null @@ -1,93 +0,0 @@ -#pragma once - -#include "esphome/core/component.h" -#include "esphome/components/sensor/sensor.h" -#include "esphome/components/sensirion_common/i2c_sensirion.h" -#include "esphome/core/application.h" -#include "esphome/core/preferences.h" -#include "sensirion_voc_algorithm.h" - -#include - -namespace esphome { -namespace sgp40 { - -struct SGP40Baselines { - int32_t state0; - int32_t state1; -} PACKED; // NOLINT - -// commands and constants -static const uint8_t SGP40_FEATURESET = 0x0020; ///< The required set for this library -static const uint8_t SGP40_CRC8_POLYNOMIAL = 0x31; ///< Seed for SGP40's CRC polynomial -static const uint8_t SGP40_CRC8_INIT = 0xFF; ///< Init value for CRC -static const uint8_t SGP40_WORD_LEN = 2; ///< 2 bytes per word - -// Commands - -static const uint16_t SGP40_CMD_GET_SERIAL_ID = 0x3682; -static const uint16_t SGP40_CMD_GET_FEATURESET = 0x202f; -static const uint16_t SGP40_CMD_SELF_TEST = 0x280e; -static const uint16_t SGP40_CMD_MEASURE_RAW = 0x260F; - -// Shortest time interval of 3H for storing baseline values. -// Prevents wear of the flash because of too many write operations -const uint32_t SHORTEST_BASELINE_STORE_INTERVAL = 10800; - -// Store anyway if the baseline difference exceeds the max storage diff value -const uint32_t MAXIMUM_STORAGE_DIFF = 50; - -class SGP40Component; - -/// This class implements support for the Sensirion sgp40 i2c GAS (VOC) sensors. -class SGP40Component : public PollingComponent, public sensor::Sensor, public sensirion_common::SensirionI2CDevice { - public: - void set_humidity_sensor(sensor::Sensor *humidity) { humidity_sensor_ = humidity; } - void set_temperature_sensor(sensor::Sensor *temperature) { temperature_sensor_ = temperature; } - - void setup() override; - void update() override; - void update_voc_index(); - void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } - void set_store_baseline(bool store_baseline) { store_baseline_ = store_baseline; } - - protected: - /// Input sensor for humidity and temperature compensation. - sensor::Sensor *humidity_sensor_{nullptr}; - sensor::Sensor *temperature_sensor_{nullptr}; - int16_t sensirion_init_sensors_(); - int16_t sgp40_probe_(); - uint64_t serial_number_; - uint16_t featureset_; - int32_t measure_voc_index_(); - uint8_t generate_crc_(const uint8_t *data, uint8_t datalen); - uint16_t measure_raw_(); - ESPPreferenceObject pref_; - uint32_t seconds_since_last_store_; - SGP40Baselines baselines_storage_; - VocAlgorithmParams voc_algorithm_params_; - bool self_test_complete_; - bool store_baseline_; - int32_t state0_; - int32_t state1_; - int32_t voc_index_ = 0; - uint8_t samples_read_ = 0; - uint8_t samples_to_stabalize_ = static_cast(VOC_ALGORITHM_INITIAL_BLACKOUT) * 2; - - /** - * @brief Request the sensor to perform a self-test, returning the result - * - * @return true: success false:failure - */ - void self_test_(); - enum ErrorCode { - COMMUNICATION_FAILED, - MEASUREMENT_INIT_FAILED, - INVALID_ID, - UNSUPPORTED_ID, - UNKNOWN - } error_code_{UNKNOWN}; -}; -} // namespace sgp40 -} // namespace esphome diff --git a/esphome/components/sgp4x/__init__.py b/esphome/components/sgp4x/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/sgp4x/sensor.py b/esphome/components/sgp4x/sensor.py new file mode 100644 index 0000000000..4855d7f066 --- /dev/null +++ b/esphome/components/sgp4x/sensor.py @@ -0,0 +1,144 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor, sensirion_common +from esphome.const import ( + CONF_ID, + CONF_STORE_BASELINE, + CONF_TEMPERATURE_SOURCE, + ICON_RADIATOR, + DEVICE_CLASS_NITROUS_OXIDE, + DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, + STATE_CLASS_MEASUREMENT, +) + +DEPENDENCIES = ["i2c"] +AUTO_LOAD = ["sensirion_common"] +CODEOWNERS = ["@SenexCrenshaw", "@martgras"] + +sgp4x_ns = cg.esphome_ns.namespace("sgp4x") +SGP4xComponent = sgp4x_ns.class_( + "SGP4xComponent", + sensor.Sensor, + cg.PollingComponent, + sensirion_common.SensirionI2CDevice, +) + +CONF_ALGORITHM_TUNING = "algorithm_tuning" +CONF_COMPENSATION = "compensation" +CONF_GAIN_FACTOR = "gain_factor" +CONF_GATING_MAX_DURATION_MINUTES = "gating_max_duration_minutes" +CONF_HUMIDITY_SOURCE = "humidity_source" +CONF_INDEX_OFFSET = "index_offset" +CONF_LEARNING_TIME_GAIN_HOURS = "learning_time_gain_hours" +CONF_LEARNING_TIME_OFFSET_HOURS = "learning_time_offset_hours" +CONF_NOX = "nox" +CONF_STD_INITIAL = "std_initial" +CONF_VOC = "voc" +CONF_VOC_BASELINE = "voc_baseline" + + +def validate_sensors(config): + if CONF_VOC not in config and CONF_NOX not in config: + raise cv.Invalid( + f"At least one sensor is required. Define {CONF_VOC} and/or {CONF_NOX}" + ) + return config + + +GAS_SENSOR = cv.Schema( + { + cv.Optional(CONF_ALGORITHM_TUNING): cv.Schema( + { + cv.Optional(CONF_INDEX_OFFSET, default=100): cv.int_, + cv.Optional(CONF_LEARNING_TIME_OFFSET_HOURS, default=12): cv.int_, + cv.Optional(CONF_LEARNING_TIME_GAIN_HOURS, default=12): cv.int_, + cv.Optional(CONF_GATING_MAX_DURATION_MINUTES, default=720): cv.int_, + cv.Optional(CONF_STD_INITIAL, default=50): cv.int_, + cv.Optional(CONF_GAIN_FACTOR, default=230): cv.int_, + } + ) + } +) + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(SGP4xComponent), + cv.Optional(CONF_VOC): sensor.sensor_schema( + icon=ICON_RADIATOR, + accuracy_decimals=0, + device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, + state_class=STATE_CLASS_MEASUREMENT, + ).extend(GAS_SENSOR), + cv.Optional(CONF_NOX): sensor.sensor_schema( + icon=ICON_RADIATOR, + accuracy_decimals=0, + device_class=DEVICE_CLASS_NITROUS_OXIDE, + state_class=STATE_CLASS_MEASUREMENT, + ).extend(GAS_SENSOR), + cv.Optional(CONF_STORE_BASELINE, default=True): cv.boolean, + cv.Optional(CONF_VOC_BASELINE): cv.hex_uint16_t, + cv.Optional(CONF_COMPENSATION): cv.Schema( + { + cv.Required(CONF_HUMIDITY_SOURCE): cv.use_id(sensor.Sensor), + cv.Required(CONF_TEMPERATURE_SOURCE): cv.use_id(sensor.Sensor), + }, + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x59)), + validate_sensors, +) + + +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) + + if CONF_COMPENSATION in config: + compensation_config = config[CONF_COMPENSATION] + sens = await cg.get_variable(compensation_config[CONF_HUMIDITY_SOURCE]) + cg.add(var.set_humidity_sensor(sens)) + sens = await cg.get_variable(compensation_config[CONF_TEMPERATURE_SOURCE]) + cg.add(var.set_temperature_sensor(sens)) + + cg.add(var.set_store_baseline(config[CONF_STORE_BASELINE])) + + if CONF_VOC_BASELINE in config: + cg.add(var.set_voc_baseline(CONF_VOC_BASELINE)) + + if CONF_VOC in config: + sens = await sensor.new_sensor(config[CONF_VOC]) + cg.add(var.set_voc_sensor(sens)) + if CONF_ALGORITHM_TUNING in config[CONF_VOC]: + cfg = config[CONF_VOC][CONF_ALGORITHM_TUNING] + cg.add( + var.set_voc_algorithm_tuning( + cfg[CONF_INDEX_OFFSET], + cfg[CONF_LEARNING_TIME_OFFSET_HOURS], + cfg[CONF_LEARNING_TIME_GAIN_HOURS], + cfg[CONF_GATING_MAX_DURATION_MINUTES], + cfg[CONF_STD_INITIAL], + cfg[CONF_GAIN_FACTOR], + ) + ) + + if CONF_NOX in config: + sens = await sensor.new_sensor(config[CONF_NOX]) + cg.add(var.set_nox_sensor(sens)) + if CONF_ALGORITHM_TUNING in config[CONF_NOX]: + cfg = config[CONF_NOX][CONF_ALGORITHM_TUNING] + cg.add( + var.set_nox_algorithm_tuning( + cfg[CONF_INDEX_OFFSET], + cfg[CONF_LEARNING_TIME_OFFSET_HOURS], + cfg[CONF_LEARNING_TIME_GAIN_HOURS], + cfg[CONF_GATING_MAX_DURATION_MINUTES], + cfg[CONF_GAIN_FACTOR], + ) + ) + cg.add_library( + None, None, "https://github.com/Sensirion/arduino-gas-index-algorithm.git" + ) diff --git a/esphome/components/sgp4x/sgp4x.cpp b/esphome/components/sgp4x/sgp4x.cpp new file mode 100644 index 0000000000..a6f57e0342 --- /dev/null +++ b/esphome/components/sgp4x/sgp4x.cpp @@ -0,0 +1,343 @@ +#include "sgp4x.h" +#include "esphome/core/log.h" +#include "esphome/core/hal.h" +#include + +namespace esphome { +namespace sgp4x { + +static const char *const TAG = "sgp4x"; + +void SGP4xComponent::setup() { + ESP_LOGCONFIG(TAG, "Setting up SGP4x..."); + + // Serial Number identification + uint16_t raw_serial_number[3]; + if (!this->get_register(SGP4X_CMD_GET_SERIAL_ID, raw_serial_number, 3, 1)) { + ESP_LOGE(TAG, "Failed to read serial number"); + this->error_code_ = SERIAL_NUMBER_IDENTIFICATION_FAILED; + this->mark_failed(); + return; + } + this->serial_number_ = (uint64_t(raw_serial_number[0]) << 24) | (uint64_t(raw_serial_number[1]) << 16) | + (uint64_t(raw_serial_number[2])); + ESP_LOGD(TAG, "Serial Number: %" PRIu64, this->serial_number_); + + // Featureset identification for future use + uint16_t raw_featureset; + if (!this->get_register(SGP4X_CMD_GET_FEATURESET, raw_featureset, 1)) { + ESP_LOGD(TAG, "raw_featureset write_command_ failed"); + this->mark_failed(); + return; + } + this->featureset_ = raw_featureset; + if ((this->featureset_ & 0x1FF) == SGP40_FEATURESET) { + sgp_type_ = SGP40; + self_test_time_ = SPG40_SELFTEST_TIME; + measure_time_ = SGP40_MEASURE_TIME; + if (this->nox_sensor_) { + ESP_LOGE(TAG, "Measuring NOx requires a SGP41 sensor but a SGP40 sensor is detected"); + // disable the sensor + this->nox_sensor_->set_disabled_by_default(true); + // make sure it's not visiable in HA + this->nox_sensor_->set_internal(true); + this->nox_sensor_->state = NAN; + // remove pointer to sensor + this->nox_sensor_ = nullptr; + } + } else { + if ((this->featureset_ & 0x1FF) == SGP41_FEATURESET) { + sgp_type_ = SGP41; + self_test_time_ = SPG41_SELFTEST_TIME; + measure_time_ = SGP41_MEASURE_TIME; + } else { + ESP_LOGD(TAG, "Product feature set failed 0x%0X , expecting 0x%0X", uint16_t(this->featureset_ & 0x1FF), + SGP40_FEATURESET); + this->mark_failed(); + return; + } + } + + ESP_LOGD(TAG, "Product version: 0x%0X", uint16_t(this->featureset_ & 0x1FF)); + + if (this->store_baseline_) { + // Hash with compilation time + // This ensures the baseline storage is cleared after OTA + uint32_t hash = fnv1_hash(App.get_compilation_time()); + this->pref_ = global_preferences->make_preference(hash, true); + + if (this->pref_.load(&this->voc_baselines_storage_)) { + this->voc_state0_ = this->voc_baselines_storage_.state0; + this->voc_state1_ = this->voc_baselines_storage_.state1; + ESP_LOGI(TAG, "Loaded VOC baseline state0: 0x%04X, state1: 0x%04X", this->voc_baselines_storage_.state0, + voc_baselines_storage_.state1); + } + + // Initialize storage timestamp + this->seconds_since_last_store_ = 0; + + if (this->voc_baselines_storage_.state0 > 0 && this->voc_baselines_storage_.state1 > 0) { + ESP_LOGI(TAG, "Setting VOC baseline from save state0: 0x%04X, state1: 0x%04X", + this->voc_baselines_storage_.state0, voc_baselines_storage_.state1); + voc_algorithm_.set_states(this->voc_baselines_storage_.state0, this->voc_baselines_storage_.state1); + } + } + if (this->voc_sensor_ && this->voc_tuning_params_.has_value()) { + voc_algorithm_.set_tuning_parameters( + voc_tuning_params_.value().index_offset, voc_tuning_params_.value().learning_time_offset_hours, + voc_tuning_params_.value().learning_time_gain_hours, voc_tuning_params_.value().gating_max_duration_minutes, + voc_tuning_params_.value().std_initial, voc_tuning_params_.value().gain_factor); + } + + if (this->nox_sensor_ && this->nox_tuning_params_.has_value()) { + nox_algorithm_.set_tuning_parameters( + nox_tuning_params_.value().index_offset, nox_tuning_params_.value().learning_time_offset_hours, + nox_tuning_params_.value().learning_time_gain_hours, nox_tuning_params_.value().gating_max_duration_minutes, + nox_tuning_params_.value().std_initial, nox_tuning_params_.value().gain_factor); + } + + this->self_test_(); + + /* The official spec for this sensor at + https://sensirion.com/media/documents/296373BB/6203C5DF/Sensirion_Gas_Sensors_Datasheet_SGP40.pdf indicates this + sensor should be driven at 1Hz. Comments from the developers at: + https://github.com/Sensirion/embedded-sgp/issues/136 indicate the algorithm should be a bit resilient to slight + timing variations so the software timer should be accurate enough for this. + + This block starts sampling from the sensor at 1Hz, and is done seperately from the call + to the update method. This seperation is to support getting accurate measurements but + limit the amount of communication done over wifi for power consumption or to keep the + number of records reported from being overwhelming. + */ + ESP_LOGD(TAG, "Component requires sampling of 1Hz, setting up background sampler"); + this->set_interval(1000, [this]() { this->update_gas_indices(); }); +} + +void SGP4xComponent::self_test_() { + ESP_LOGD(TAG, "Self-test started"); + if (!this->write_command(SGP4X_CMD_SELF_TEST)) { + this->error_code_ = COMMUNICATION_FAILED; + ESP_LOGD(TAG, "Self-test communication failed"); + this->mark_failed(); + } + + this->set_timeout(self_test_time_, [this]() { + uint16_t reply; + if (!this->read_data(reply)) { + this->error_code_ = SELF_TEST_FAILED; + ESP_LOGD(TAG, "Self-test read_data_ failed"); + this->mark_failed(); + return; + } + + if (reply == 0xD400) { + this->self_test_complete_ = true; + ESP_LOGD(TAG, "Self-test completed"); + return; + } else { + this->error_code_ = SELF_TEST_FAILED; + ESP_LOGD(TAG, "Self-test failed 0x%X", reply); + return; + } + + ESP_LOGD(TAG, "Self-test failed 0x%X", reply); + this->mark_failed(); + }); +} + +/** + * @brief Combined the measured gasses, temperature, and humidity + * to calculate the VOC Index + * + * @param temperature The measured temperature in degrees C + * @param humidity The measured relative humidity in % rH + * @return int32_t The VOC Index + */ +bool SGP4xComponent::measure_gas_indices_(int32_t &voc, int32_t &nox) { + uint16_t voc_sraw; + uint16_t nox_sraw; + if (!measure_raw_(voc_sraw, nox_sraw)) + return false; + + this->status_clear_warning(); + + voc = voc_algorithm_.process(voc_sraw); + if (nox_sensor_) { + nox = nox_algorithm_.process(nox_sraw); + } + ESP_LOGV(TAG, "VOC = %d, NOx = %d", voc, nox); + // Store baselines after defined interval or if the difference between current and stored baseline becomes too + // much + if (this->store_baseline_ && this->seconds_since_last_store_ > SHORTEST_BASELINE_STORE_INTERVAL) { + voc_algorithm_.get_states(this->voc_state0_, this->voc_state1_); + if ((uint32_t) abs(this->voc_baselines_storage_.state0 - this->voc_state0_) > MAXIMUM_STORAGE_DIFF || + (uint32_t) abs(this->voc_baselines_storage_.state1 - this->voc_state1_) > MAXIMUM_STORAGE_DIFF) { + this->seconds_since_last_store_ = 0; + this->voc_baselines_storage_.state0 = this->voc_state0_; + this->voc_baselines_storage_.state1 = this->voc_state1_; + + if (this->pref_.save(&this->voc_baselines_storage_)) { + ESP_LOGI(TAG, "Stored VOC baseline state0: 0x%04X ,state1: 0x%04X", this->voc_baselines_storage_.state0, + voc_baselines_storage_.state1); + } else { + ESP_LOGW(TAG, "Could not store VOC baselines"); + } + } + } + + return true; +} +/** + * @brief Return the raw gas measurement + * + * @param temperature The measured temperature in degrees C + * @param humidity The measured relative humidity in % rH + * @return uint16_t The current raw gas measurement + */ +bool SGP4xComponent::measure_raw_(uint16_t &voc_raw, uint16_t &nox_raw) { + float humidity = NAN; + static uint32_t nox_conditioning_start = millis(); + + if (!this->self_test_complete_) { + ESP_LOGD(TAG, "Self-test not yet complete"); + return false; + } + if (this->humidity_sensor_ != nullptr) { + humidity = this->humidity_sensor_->state; + } + if (std::isnan(humidity) || humidity < 0.0f || humidity > 100.0f) { + humidity = 50; + } + + float temperature = NAN; + if (this->temperature_sensor_ != nullptr) { + temperature = float(this->temperature_sensor_->state); + } + if (std::isnan(temperature) || temperature < -40.0f || temperature > 85.0f) { + temperature = 25; + } + + uint16_t command; + uint16_t data[2]; + size_t response_words; + // Use SGP40 measure command if we don't care about NOx + if (nox_sensor_ == nullptr) { + command = SGP40_CMD_MEASURE_RAW; + response_words = 1; + } else { + // SGP41 sensor must use NOx conditioning command for the first 10 seconds + if (millis() - nox_conditioning_start < 10000) { + command = SGP41_CMD_NOX_CONDITIONING; + response_words = 1; + } else { + command = SGP41_CMD_MEASURE_RAW; + response_words = 2; + } + } + uint16_t rhticks = llround((uint16_t)((humidity * 65535) / 100)); + uint16_t tempticks = (uint16_t)(((temperature + 45) * 65535) / 175); + // first paramater are the relative humidity ticks + data[0] = rhticks; + // secomd paramater are the temperature ticks + data[1] = tempticks; + + if (!this->write_command(command, data, 2)) { + this->status_set_warning(); + ESP_LOGD(TAG, "write error (%d)", this->last_error_); + return false; + } + delay(measure_time_); + uint16_t raw_data[2]; + raw_data[1] = 0; + if (!this->read_data(raw_data, response_words)) { + this->status_set_warning(); + ESP_LOGD(TAG, "read error (%d)", this->last_error_); + return false; + } + voc_raw = raw_data[0]; + nox_raw = raw_data[1]; // either 0 or the measured NOx ticks + return true; +} + +void SGP4xComponent::update_gas_indices() { + if (!this->self_test_complete_) + return; + + this->seconds_since_last_store_ += 1; + if (!this->measure_gas_indices_(this->voc_index_, this->nox_index_)) { + // Set values to UINT16_MAX to indicate failure + this->voc_index_ = this->nox_index_ = UINT16_MAX; + ESP_LOGE(TAG, "measure gas indices failed"); + return; + } + if (this->samples_read_ < this->samples_to_stabilize_) { + this->samples_read_++; + ESP_LOGD(TAG, "Sensor has not collected enough samples yet. (%d/%d) VOC index is: %u", this->samples_read_, + this->samples_to_stabilize_, this->voc_index_); + return; + } +} + +void SGP4xComponent::update() { + if (this->samples_read_ < this->samples_to_stabilize_) { + return; + } + if (this->voc_sensor_) { + if (this->voc_index_ != UINT16_MAX) { + this->status_clear_warning(); + this->voc_sensor_->publish_state(this->voc_index_); + } else { + this->status_set_warning(); + } + } + if (this->nox_sensor_) { + if (this->nox_index_ != UINT16_MAX) { + this->status_clear_warning(); + this->nox_sensor_->publish_state(this->nox_index_); + } else { + this->status_set_warning(); + } + } +} + +void SGP4xComponent::dump_config() { + ESP_LOGCONFIG(TAG, "SGP4x:"); + LOG_I2C_DEVICE(this); + ESP_LOGCONFIG(TAG, " store_baseline: %d", this->store_baseline_); + + if (this->is_failed()) { + switch (this->error_code_) { + case COMMUNICATION_FAILED: + ESP_LOGW(TAG, "Communication failed! Is the sensor connected?"); + break; + case SERIAL_NUMBER_IDENTIFICATION_FAILED: + ESP_LOGW(TAG, "Get Serial number failed."); + break; + case SELF_TEST_FAILED: + ESP_LOGW(TAG, "Self test failed."); + break; + + default: + ESP_LOGW(TAG, "Unknown setup error!"); + break; + } + } else { + ESP_LOGCONFIG(TAG, " Type: %s", sgp_type_ == SGP41 ? "SGP41" : "SPG40"); + ESP_LOGCONFIG(TAG, " Serial number: %" PRIu64, this->serial_number_); + ESP_LOGCONFIG(TAG, " Minimum Samples: %f", GasIndexAlgorithm_INITIAL_BLACKOUT); + } + LOG_UPDATE_INTERVAL(this); + + if (this->humidity_sensor_ != nullptr && this->temperature_sensor_ != nullptr) { + ESP_LOGCONFIG(TAG, " Compensation:"); + LOG_SENSOR(" ", "Temperature Source:", this->temperature_sensor_); + LOG_SENSOR(" ", "Humidity Source:", this->humidity_sensor_); + } else { + ESP_LOGCONFIG(TAG, " Compensation: No source configured"); + } + LOG_SENSOR(" ", "VOC", this->voc_sensor_); + LOG_SENSOR(" ", "NOx", this->nox_sensor_); +} + +} // namespace sgp4x +} // namespace esphome diff --git a/esphome/components/sgp4x/sgp4x.h b/esphome/components/sgp4x/sgp4x.h new file mode 100644 index 0000000000..3060972fc3 --- /dev/null +++ b/esphome/components/sgp4x/sgp4x.h @@ -0,0 +1,142 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/sensirion_common/i2c_sensirion.h" +#include "esphome/core/application.h" +#include "esphome/core/preferences.h" +#include +#include + +#include + +namespace esphome { +namespace sgp4x { + +struct SGP4xBaselines { + int32_t state0; + int32_t state1; +} PACKED; // NOLINT + +enum SgpType { SGP40, SGP41 }; + +struct GasTuning { + uint16_t index_offset; + uint16_t learning_time_offset_hours; + uint16_t learning_time_gain_hours; + uint16_t gating_max_duration_minutes; + uint16_t std_initial; + uint16_t gain_factor; +}; + +// commands and constants +static const uint8_t SGP40_FEATURESET = 0x0020; // can measure VOC +static const uint8_t SGP41_FEATURESET = 0x0040; // can measure VOC and NOX +// Commands +static const uint16_t SGP4X_CMD_GET_SERIAL_ID = 0x3682; +static const uint16_t SGP4X_CMD_GET_FEATURESET = 0x202f; +static const uint16_t SGP4X_CMD_SELF_TEST = 0x280e; +static const uint16_t SGP40_CMD_MEASURE_RAW = 0x260F; +static const uint16_t SGP41_CMD_MEASURE_RAW = 0x2619; +static const uint16_t SGP41_CMD_NOX_CONDITIONING = 0x2612; +static const uint8_t SGP41_SUBCMD_NOX_CONDITIONING = 0x12; + +// Shortest time interval of 3H for storing baseline values. +// Prevents wear of the flash because of too many write operations +const uint32_t SHORTEST_BASELINE_STORE_INTERVAL = 10800; +static const uint16_t SPG40_SELFTEST_TIME = 250; // 250 ms for self test +static const uint16_t SPG41_SELFTEST_TIME = 320; // 320 ms for self test +static const uint16_t SGP40_MEASURE_TIME = 30; +static const uint16_t SGP41_MEASURE_TIME = 55; +// Store anyway if the baseline difference exceeds the max storage diff value +const uint32_t MAXIMUM_STORAGE_DIFF = 50; + +class SGP4xComponent; + +/// This class implements support for the Sensirion sgp4x i2c GAS (VOC) sensors. +class SGP4xComponent : public PollingComponent, public sensor::Sensor, public sensirion_common::SensirionI2CDevice { + enum ErrorCode { + COMMUNICATION_FAILED, + MEASUREMENT_INIT_FAILED, + INVALID_ID, + UNSUPPORTED_ID, + SERIAL_NUMBER_IDENTIFICATION_FAILED, + SELF_TEST_FAILED, + UNKNOWN + } error_code_{UNKNOWN}; + + public: + // SGP4xComponent() {}; + void set_humidity_sensor(sensor::Sensor *humidity) { humidity_sensor_ = humidity; } + void set_temperature_sensor(sensor::Sensor *temperature) { temperature_sensor_ = temperature; } + + void setup() override; + void update() override; + void update_gas_indices(); + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + void set_store_baseline(bool store_baseline) { store_baseline_ = store_baseline; } + void set_voc_sensor(sensor::Sensor *voc_sensor) { voc_sensor_ = voc_sensor; } + void set_nox_sensor(sensor::Sensor *nox_sensor) { nox_sensor_ = nox_sensor; } + void set_voc_algorithm_tuning(uint16_t index_offset, uint16_t learning_time_offset_hours, + uint16_t learning_time_gain_hours, uint16_t gating_max_duration_minutes, + uint16_t std_initial, uint16_t gain_factor) { + voc_tuning_params_.value().index_offset = index_offset; + voc_tuning_params_.value().learning_time_offset_hours = learning_time_offset_hours; + voc_tuning_params_.value().learning_time_gain_hours = learning_time_gain_hours; + voc_tuning_params_.value().gating_max_duration_minutes = gating_max_duration_minutes; + voc_tuning_params_.value().std_initial = std_initial; + voc_tuning_params_.value().gain_factor = gain_factor; + } + void set_nox_algorithm_tuning(uint16_t index_offset, uint16_t learning_time_offset_hours, + uint16_t learning_time_gain_hours, uint16_t gating_max_duration_minutes, + uint16_t gain_factor) { + nox_tuning_params_.value().index_offset = index_offset; + nox_tuning_params_.value().learning_time_offset_hours = learning_time_offset_hours; + nox_tuning_params_.value().learning_time_gain_hours = learning_time_gain_hours; + nox_tuning_params_.value().gating_max_duration_minutes = gating_max_duration_minutes; + nox_tuning_params_.value().std_initial = 50; + nox_tuning_params_.value().gain_factor = gain_factor; + } + + protected: + void self_test_(); + + /// Input sensor for humidity and temperature compensation. + sensor::Sensor *humidity_sensor_{nullptr}; + sensor::Sensor *temperature_sensor_{nullptr}; + int16_t sensirion_init_sensors_(); + + bool measure_gas_indices_(int32_t &voc, int32_t &nox); + bool measure_raw_(uint16_t &voc_raw, uint16_t &nox_raw); + + SgpType sgp_type_{SGP40}; + uint64_t serial_number_; + uint16_t featureset_; + + bool self_test_complete_; + uint16_t self_test_time_; + + sensor::Sensor *voc_sensor_{nullptr}; + VOCGasIndexAlgorithm voc_algorithm_; + optional voc_tuning_params_; + int32_t voc_state0_; + int32_t voc_state1_; + int32_t voc_index_ = 0; + + sensor::Sensor *nox_sensor_{nullptr}; + int32_t nox_index_ = 0; + NOxGasIndexAlgorithm nox_algorithm_; + optional nox_tuning_params_; + + uint16_t measure_time_; + uint8_t samples_read_ = 0; + uint8_t samples_to_stabilize_ = static_cast(GasIndexAlgorithm_INITIAL_BLACKOUT) * 2; + + bool store_baseline_; + ESPPreferenceObject pref_; + uint32_t seconds_since_last_store_; + SGP4xBaselines voc_baselines_storage_; +}; +} // namespace sgp4x +} // namespace esphome diff --git a/platformio.ini b/platformio.ini index bc2cddb9f7..82cf6eeb9a 100644 --- a/platformio.ini +++ b/platformio.ini @@ -39,6 +39,8 @@ lib_deps = bblanchon/ArduinoJson@6.18.5 ; json wjtje/qr-code-generator-library@1.7.0 ; qr_code functionpointer/arduino-MLX90393@1.0.0 ; mlx90393 + ; This is using the repository until a new release is published to PlatformIO + https://github.com/Sensirion/arduino-gas-index-algorithm.git ; Sensirion Gas Index Algorithm Arduino Library build_flags = -DESPHOME_LOG_LEVEL=ESPHOME_LOG_LEVEL_VERY_VERBOSE src_filter = diff --git a/tests/test2.yaml b/tests/test2.yaml index a7a9ef9661..f88486524f 100644 --- a/tests/test2.yaml +++ b/tests/test2.yaml @@ -281,10 +281,27 @@ sensor: window_correction_factor: 1.0 address: 0x53 update_interval: 60s - - platform: sgp40 - name: 'Workshop VOC' + - platform: sgp4x + voc: + name: "VOC Index" + id: sgp40_voc_index + algorithm_tuning: + index_offset: 100 + learning_time_offset_hours: 12 + learning_time_gain_hours: 12 + gating_max_duration_minutes: 180 + std_initial: 50 + gain_factor: 230 + nox: + name: "NOx" + algorithm_tuning: + index_offset: 100 + learning_time_offset_hours: 12 + learning_time_gain_hours: 12 + gating_max_duration_minutes: 180 + std_initial: 50 + gain_factor: 230 update_interval: 5s - store_baseline: 'true' - platform: mcp3008 update_interval: 5s mcp3008_id: 'mcp3008_hub' From 4f52d43347c6b87529238d737473bed974b5705e Mon Sep 17 00:00:00 2001 From: gazoodle <47351872+gazoodle@users.noreply.github.com> Date: Thu, 19 May 2022 01:49:12 +0100 Subject: [PATCH 22/60] add support user-defined modbus functions (#3461) --- esphome/components/modbus/modbus.cpp | 67 ++++++++++++++++++---------- 1 file changed, 44 insertions(+), 23 deletions(-) diff --git a/esphome/components/modbus/modbus.cpp b/esphome/components/modbus/modbus.cpp index 60ce50097c..19b5e8019e 100644 --- a/esphome/components/modbus/modbus.cpp +++ b/esphome/components/modbus/modbus.cpp @@ -68,33 +68,54 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) { uint8_t data_len = raw[2]; uint8_t data_offset = 3; - // the response for write command mirrors the requests and data startes at offset 2 instead of 3 for read commands - if (function_code == 0x5 || function_code == 0x06 || function_code == 0xF || function_code == 0x10) { - data_offset = 2; - data_len = 4; - } - // Error ( msb indicates error ) - // response format: Byte[0] = device address, Byte[1] function code | 0x80 , Byte[2] excpetion code, Byte[3-4] crc - if ((function_code & 0x80) == 0x80) { - data_offset = 2; - data_len = 1; - } + // Per https://modbus.org/docs/Modbus_Application_Protocol_V1_1b3.pdf Ch 5 User-Defined function codes + if (((function_code >= 65) && (function_code <= 72)) || ((function_code >= 100) && (function_code <= 110))) { + // Handle user-defined function, since we don't know how big this ought to be, + // ideally we should delegate the entire length detection to whatever handler is + // installed, but wait, there is the CRC, and if we get a hit there is a good + // chance that this is a complete message ... admittedly there is a small chance is + // isn't but that is quite small given the purpose of the CRC in the first place + data_len = at; + data_offset = 1; - // Byte data_offset..data_offset+data_len-1: Data - if (at < data_offset + data_len) - return true; + uint16_t computed_crc = crc16(raw, data_offset + data_len); + uint16_t remote_crc = uint16_t(raw[data_offset + data_len]) | (uint16_t(raw[data_offset + data_len + 1]) << 8); - // Byte 3+data_len: CRC_LO (over all bytes) - if (at == data_offset + data_len) - return true; + if (computed_crc != remote_crc) + return true; - // Byte data_offset+len+1: CRC_HI (over all bytes) - uint16_t computed_crc = crc16(raw, data_offset + data_len); - uint16_t remote_crc = uint16_t(raw[data_offset + data_len]) | (uint16_t(raw[data_offset + data_len + 1]) << 8); - if (computed_crc != remote_crc) { - ESP_LOGW(TAG, "Modbus CRC Check failed! %02X!=%02X", computed_crc, remote_crc); - return false; + ESP_LOGD(TAG, "Modbus user-defined function %02X found", function_code); + + } else { + // the response for write command mirrors the requests and data startes at offset 2 instead of 3 for read commands + if (function_code == 0x5 || function_code == 0x06 || function_code == 0xF || function_code == 0x10) { + data_offset = 2; + data_len = 4; + } + + // Error ( msb indicates error ) + // response format: Byte[0] = device address, Byte[1] function code | 0x80 , Byte[2] excpetion code, Byte[3-4] crc + if ((function_code & 0x80) == 0x80) { + data_offset = 2; + data_len = 1; + } + + // Byte data_offset..data_offset+data_len-1: Data + if (at < data_offset + data_len) + return true; + + // Byte 3+data_len: CRC_LO (over all bytes) + if (at == data_offset + data_len) + return true; + + // Byte data_offset+len+1: CRC_HI (over all bytes) + uint16_t computed_crc = crc16(raw, data_offset + data_len); + uint16_t remote_crc = uint16_t(raw[data_offset + data_len]) | (uint16_t(raw[data_offset + data_len + 1]) << 8); + if (computed_crc != remote_crc) { + ESP_LOGW(TAG, "Modbus CRC Check failed! %02X!=%02X", computed_crc, remote_crc); + return false; + } } std::vector data(this->rx_buffer_.begin() + data_offset, this->rx_buffer_.begin() + data_offset + data_len); bool found = false; From f0c890f160dc1f073b351d39525f0b377be2055f Mon Sep 17 00:00:00 2001 From: Keilin Bickar Date: Wed, 18 May 2022 20:50:44 -0400 Subject: [PATCH 23/60] Remove deprecated fan speeds (#3397) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/api/api_connection.cpp | 12 ----- esphome/components/fan/fan.cpp | 17 ------- esphome/components/fan/fan.h | 7 --- esphome/components/fan/fan_helpers.cpp | 23 ---------- esphome/components/fan/fan_helpers.h | 20 --------- .../components/hbridge/fan/hbridge_fan.cpp | 1 - esphome/components/mqtt/mqtt_fan.cpp | 45 +------------------ esphome/components/speed/fan/speed_fan.cpp | 1 - esphome/components/tuya/fan/tuya_fan.cpp | 1 - esphome/components/web_server/web_server.cpp | 24 ---------- 10 files changed, 2 insertions(+), 149 deletions(-) delete mode 100644 esphome/components/fan/fan_helpers.cpp delete mode 100644 esphome/components/fan/fan_helpers.h diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 81f2465b74..4f399d95d0 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -12,9 +12,6 @@ #ifdef USE_HOMEASSISTANT_TIME #include "esphome/components/homeassistant/time/homeassistant_time.h" #endif -#ifdef USE_FAN -#include "esphome/components/fan/fan_helpers.h" -#endif namespace esphome { namespace api { @@ -253,9 +250,6 @@ void APIConnection::cover_command(const CoverCommandRequest &msg) { #endif #ifdef USE_FAN -// Shut-up about usage of deprecated speed_level_to_enum/speed_enum_to_level functions for a bit. -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wdeprecated-declarations" bool APIConnection::send_fan_state(fan::Fan *fan) { if (!this->state_subscription_) return false; @@ -268,7 +262,6 @@ bool APIConnection::send_fan_state(fan::Fan *fan) { resp.oscillating = fan->oscillating; if (traits.supports_speed()) { resp.speed_level = fan->speed; - resp.speed = static_cast(fan::speed_level_to_enum(fan->speed, traits.supported_speed_count())); } if (traits.supports_direction()) resp.direction = static_cast(fan->direction); @@ -295,8 +288,6 @@ void APIConnection::fan_command(const FanCommandRequest &msg) { if (fan == nullptr) return; - auto traits = fan->get_traits(); - auto call = fan->make_call(); if (msg.has_state) call.set_state(msg.state); @@ -305,14 +296,11 @@ void APIConnection::fan_command(const FanCommandRequest &msg) { if (msg.has_speed_level) { // Prefer level call.set_speed(msg.speed_level); - } else if (msg.has_speed) { - call.set_speed(fan::speed_enum_to_level(static_cast(msg.speed), traits.supported_speed_count())); } if (msg.has_direction) call.set_direction(static_cast(msg.direction)); call.perform(); } -#pragma GCC diagnostic pop #endif #ifdef USE_LIGHT diff --git a/esphome/components/fan/fan.cpp b/esphome/components/fan/fan.cpp index 5f9660f6d6..f7c4ab2e11 100644 --- a/esphome/components/fan/fan.cpp +++ b/esphome/components/fan/fan.cpp @@ -1,5 +1,4 @@ #include "fan.h" -#include "fan_helpers.h" #include "esphome/core/log.h" namespace esphome { @@ -61,22 +60,6 @@ void FanCall::validate_() { } } -// This whole method is deprecated, don't warn about usage of deprecated methods inside of it. -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wdeprecated-declarations" -FanCall &FanCall::set_speed(const char *legacy_speed) { - const auto supported_speed_count = this->parent_.get_traits().supported_speed_count(); - if (strcasecmp(legacy_speed, "low") == 0) { - this->set_speed(fan::speed_enum_to_level(FAN_SPEED_LOW, supported_speed_count)); - } else if (strcasecmp(legacy_speed, "medium") == 0) { - this->set_speed(fan::speed_enum_to_level(FAN_SPEED_MEDIUM, supported_speed_count)); - } else if (strcasecmp(legacy_speed, "high") == 0) { - this->set_speed(fan::speed_enum_to_level(FAN_SPEED_HIGH, supported_speed_count)); - } - return *this; -} -#pragma GCC diagnostic pop - FanCall FanRestoreState::to_call(Fan &fan) { auto call = fan.make_call(); call.set_state(this->state); diff --git a/esphome/components/fan/fan.h b/esphome/components/fan/fan.h index cafb5843d1..ef2ecd0f3f 100644 --- a/esphome/components/fan/fan.h +++ b/esphome/components/fan/fan.h @@ -16,13 +16,6 @@ namespace fan { (obj)->dump_traits_(TAG, prefix); \ } -/// Simple enum to represent the speed of a fan. - DEPRECATED - Will be deleted soon -enum ESPDEPRECATED("FanSpeed is deprecated.", "2021.9") FanSpeed { - FAN_SPEED_LOW = 0, ///< The fan is running on low speed. - FAN_SPEED_MEDIUM = 1, ///< The fan is running on medium speed. - FAN_SPEED_HIGH = 2 ///< The fan is running on high/full speed. -}; - /// Simple enum to represent the direction of a fan. enum class FanDirection { FORWARD = 0, REVERSE = 1 }; diff --git a/esphome/components/fan/fan_helpers.cpp b/esphome/components/fan/fan_helpers.cpp deleted file mode 100644 index 34883617e6..0000000000 --- a/esphome/components/fan/fan_helpers.cpp +++ /dev/null @@ -1,23 +0,0 @@ -#include -#include "fan_helpers.h" - -namespace esphome { -namespace fan { - -// This whole file is deprecated, don't warn about usage of deprecated types in here. -#pragma GCC diagnostic ignored "-Wdeprecated-declarations" - -FanSpeed speed_level_to_enum(int speed_level, int supported_speed_levels) { - const auto speed_ratio = static_cast(speed_level) / (supported_speed_levels + 1); - const auto legacy_level = clamp(static_cast(ceilf(speed_ratio * 3)), 1, 3); - return static_cast(legacy_level - 1); -} - -int speed_enum_to_level(FanSpeed speed, int supported_speed_levels) { - const auto enum_level = static_cast(speed) + 1; - const auto speed_level = roundf(enum_level / 3.0f * supported_speed_levels); - return static_cast(speed_level); -} - -} // namespace fan -} // namespace esphome diff --git a/esphome/components/fan/fan_helpers.h b/esphome/components/fan/fan_helpers.h deleted file mode 100644 index 8e8e3859bd..0000000000 --- a/esphome/components/fan/fan_helpers.h +++ /dev/null @@ -1,20 +0,0 @@ -#pragma once - -#include "fan.h" - -namespace esphome { -namespace fan { - -// Shut-up about usage of deprecated FanSpeed for a bit. -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wdeprecated-declarations" - -ESPDEPRECATED("FanSpeed and speed_level_to_enum() are deprecated.", "2021.9") -FanSpeed speed_level_to_enum(int speed_level, int supported_speed_levels); -ESPDEPRECATED("FanSpeed and speed_enum_to_level() are deprecated.", "2021.9") -int speed_enum_to_level(FanSpeed speed, int supported_speed_levels); - -#pragma GCC diagnostic pop - -} // namespace fan -} // namespace esphome diff --git a/esphome/components/hbridge/fan/hbridge_fan.cpp b/esphome/components/hbridge/fan/hbridge_fan.cpp index 52d2b3d8b7..44cf5ae049 100644 --- a/esphome/components/hbridge/fan/hbridge_fan.cpp +++ b/esphome/components/hbridge/fan/hbridge_fan.cpp @@ -1,5 +1,4 @@ #include "hbridge_fan.h" -#include "esphome/components/fan/fan_helpers.h" #include "esphome/core/log.h" namespace esphome { diff --git a/esphome/components/mqtt/mqtt_fan.cpp b/esphome/components/mqtt/mqtt_fan.cpp index e4d867843c..6433ead6b2 100644 --- a/esphome/components/mqtt/mqtt_fan.cpp +++ b/esphome/components/mqtt/mqtt_fan.cpp @@ -5,7 +5,6 @@ #ifdef USE_MQTT #ifdef USE_FAN -#include "esphome/components/fan/fan_helpers.h" namespace esphome { namespace mqtt { @@ -88,17 +87,6 @@ void MQTTFanComponent::setup() { }); } - if (this->state_->get_traits().supports_speed()) { - this->subscribe(this->get_speed_command_topic(), [this](const std::string &topic, const std::string &payload) { -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wdeprecated-declarations" - this->state_->make_call() - .set_speed(payload.c_str()) // NOLINT(clang-diagnostic-deprecated-declarations) - .perform(); -#pragma GCC diagnostic pop - }); - } - auto f = std::bind(&MQTTFanComponent::publish_state, this); this->state_->add_on_state_callback([this, f]() { this->defer("send", f); }); } @@ -113,8 +101,6 @@ void MQTTFanComponent::dump_config() { if (this->state_->get_traits().supports_speed()) { ESP_LOGCONFIG(TAG, " Speed Level State Topic: '%s'", this->get_speed_level_state_topic().c_str()); ESP_LOGCONFIG(TAG, " Speed Level Command Topic: '%s'", this->get_speed_level_command_topic().c_str()); - ESP_LOGCONFIG(TAG, " Speed State Topic: '%s'", this->get_speed_state_topic().c_str()); - ESP_LOGCONFIG(TAG, " Speed Command Topic: '%s'", this->get_speed_command_topic().c_str()); } } @@ -126,10 +112,8 @@ void MQTTFanComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig root[MQTT_OSCILLATION_STATE_TOPIC] = this->get_oscillation_state_topic(); } if (this->state_->get_traits().supports_speed()) { - root["speed_level_command_topic"] = this->get_speed_level_command_topic(); - root["speed_level_state_topic"] = this->get_speed_level_state_topic(); - root[MQTT_SPEED_COMMAND_TOPIC] = this->get_speed_command_topic(); - root[MQTT_SPEED_STATE_TOPIC] = this->get_speed_state_topic(); + root[MQTT_PERCENTAGE_COMMAND_TOPIC] = this->get_speed_level_command_topic(); + root[MQTT_PERCENTAGE_STATE_TOPIC] = this->get_speed_level_state_topic(); } } bool MQTTFanComponent::publish_state() { @@ -148,31 +132,6 @@ bool MQTTFanComponent::publish_state() { bool success = this->publish(this->get_speed_level_state_topic(), payload); failed = failed || !success; } - if (traits.supports_speed()) { - const char *payload; -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wdeprecated-declarations" - // NOLINTNEXTLINE(clang-diagnostic-deprecated-declarations) - switch (fan::speed_level_to_enum(this->state_->speed, traits.supported_speed_count())) { - case FAN_SPEED_LOW: { // NOLINT(clang-diagnostic-deprecated-declarations) - payload = "low"; - break; - } - case FAN_SPEED_MEDIUM: { // NOLINT(clang-diagnostic-deprecated-declarations) - payload = "medium"; - break; - } - default: - case FAN_SPEED_HIGH: { // NOLINT(clang-diagnostic-deprecated-declarations) - payload = "high"; - break; - } - } -#pragma GCC diagnostic pop - bool success = this->publish(this->get_speed_state_topic(), payload); - failed = failed || !success; - } - return !failed; } diff --git a/esphome/components/speed/fan/speed_fan.cpp b/esphome/components/speed/fan/speed_fan.cpp index 9ed201982a..3a65f2c365 100644 --- a/esphome/components/speed/fan/speed_fan.cpp +++ b/esphome/components/speed/fan/speed_fan.cpp @@ -1,5 +1,4 @@ #include "speed_fan.h" -#include "esphome/components/fan/fan_helpers.h" #include "esphome/core/log.h" namespace esphome { diff --git a/esphome/components/tuya/fan/tuya_fan.cpp b/esphome/components/tuya/fan/tuya_fan.cpp index 019b504deb..813aee4aa0 100644 --- a/esphome/components/tuya/fan/tuya_fan.cpp +++ b/esphome/components/tuya/fan/tuya_fan.cpp @@ -1,5 +1,4 @@ #include "esphome/core/log.h" -#include "esphome/components/fan/fan_helpers.h" #include "tuya_fan.h" namespace esphome { diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 6822ce9953..18374d606b 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -21,10 +21,6 @@ #include "esphome/components/logger/logger.h" #endif -#ifdef USE_FAN -#include "esphome/components/fan/fan_helpers.h" -#endif - #ifdef USE_CLIMATE #include "esphome/components/climate/climate.h" #endif @@ -482,22 +478,6 @@ std::string WebServer::fan_json(fan::Fan *obj, JsonDetail start_config) { if (traits.supports_speed()) { root["speed_level"] = obj->speed; root["speed_count"] = traits.supported_speed_count(); - -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wdeprecated-declarations" - // NOLINTNEXTLINE(clang-diagnostic-deprecated-declarations) - switch (fan::speed_level_to_enum(obj->speed, traits.supported_speed_count())) { - case fan::FAN_SPEED_LOW: // NOLINT(clang-diagnostic-deprecated-declarations) - root["speed"] = "low"; - break; - case fan::FAN_SPEED_MEDIUM: // NOLINT(clang-diagnostic-deprecated-declarations) - root["speed"] = "medium"; - break; - case fan::FAN_SPEED_HIGH: // NOLINT(clang-diagnostic-deprecated-declarations) - root["speed"] = "high"; - break; - } -#pragma GCC diagnostic pop } if (obj->get_traits().supports_oscillation()) root["oscillation"] = obj->oscillating; @@ -518,10 +498,6 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc auto call = obj->turn_on(); if (request->hasParam("speed")) { String speed = request->getParam("speed")->value(); -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wdeprecated-declarations" - call.set_speed(speed.c_str()); // NOLINT(clang-diagnostic-deprecated-declarations) -#pragma GCC diagnostic pop } if (request->hasParam("speed_level")) { String speed_level = request->getParam("speed_level")->value(); From dda1ddcb26b9ca891ca5de668673a92bcf87ca22 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 19 May 2022 16:23:40 +1200 Subject: [PATCH 24/60] Add missing import to bedjet (#3490) --- esphome/components/bedjet/bedjet.h | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/bedjet/bedjet.h b/esphome/components/bedjet/bedjet.h index 0565be6045..750a20594f 100644 --- a/esphome/components/bedjet/bedjet.h +++ b/esphome/components/bedjet/bedjet.h @@ -4,6 +4,7 @@ #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" #include "esphome/components/climate/climate.h" #include "esphome/core/component.h" +#include "esphome/core/defines.h" #include "esphome/core/hal.h" #include "bedjet_base.h" From d9d2edeb080297c3f4b0e0d82eee161222466692 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 19 May 2022 21:21:42 +1200 Subject: [PATCH 25/60] Fix compile issues on windows (#3491) --- esphome/components/sonoff_d1/sonoff_d1.cpp | 3 +-- esphome/components/tuya/text_sensor/tuya_text_sensor.cpp | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/esphome/components/sonoff_d1/sonoff_d1.cpp b/esphome/components/sonoff_d1/sonoff_d1.cpp index b4bcbc6760..d07f9229b6 100644 --- a/esphome/components/sonoff_d1/sonoff_d1.cpp +++ b/esphome/components/sonoff_d1/sonoff_d1.cpp @@ -41,7 +41,6 @@ * O FF FF FF FF FF FF FF FF - Not used * M 6C - CRC over bytes 2 to F (Addition) \*********************************************************************************************/ -#include #include "sonoff_d1.h" namespace esphome { @@ -263,7 +262,7 @@ void SonoffD1Output::write_state(light::LightState *state) { state->current_values_as_brightness(&brightness); // Convert ESPHome's brightness (0-1) to the device's internal brightness (0-100) - const uint8_t calculated_brightness = std::round(brightness * 100); + const uint8_t calculated_brightness = (uint8_t) roundf(brightness * 100); if (calculated_brightness == 0) { // if(binary) ESP_LOGD(TAG, "current_values_as_binary() returns true for zero brightness"); diff --git a/esphome/components/tuya/text_sensor/tuya_text_sensor.cpp b/esphome/components/tuya/text_sensor/tuya_text_sensor.cpp index 0b51ba90c4..602595e89d 100644 --- a/esphome/components/tuya/text_sensor/tuya_text_sensor.cpp +++ b/esphome/components/tuya/text_sensor/tuya_text_sensor.cpp @@ -20,7 +20,7 @@ void TuyaTextSensor::setup() { break; } default: - ESP_LOGW(TAG, "Unsupported data type for tuya text sensor %u: %#02hhX", datapoint.id, datapoint.type); + ESP_LOGW(TAG, "Unsupported data type for tuya text sensor %u: %#02hhX", datapoint.id, (uint8_t) datapoint.type); break; } }); From 7092f7663e5d129690f5a6603c002778bb2f444d Mon Sep 17 00:00:00 2001 From: Sergey Dudanov Date: Mon, 23 May 2022 11:51:45 +0300 Subject: [PATCH 26/60] midea: New power_toggle action. Auto-use remote transmitter. (#3496) --- esphome/components/midea/ac_automations.h | 5 ++++ esphome/components/midea/air_conditioner.h | 1 + esphome/components/midea/climate.py | 13 +++++++++- tests/test1.yaml | 28 +++++++++++++++------- 4 files changed, 38 insertions(+), 9 deletions(-) diff --git a/esphome/components/midea/ac_automations.h b/esphome/components/midea/ac_automations.h index d4ed2e7168..5084fd1eec 100644 --- a/esphome/components/midea/ac_automations.h +++ b/esphome/components/midea/ac_automations.h @@ -56,6 +56,11 @@ template class PowerOffAction : public MideaActionBase { void play(Ts... x) override { this->parent_->do_power_off(); } }; +template class PowerToggleAction : public MideaActionBase { + public: + void play(Ts... x) override { this->parent_->do_power_toggle(); } +}; + } // namespace ac } // namespace midea } // namespace esphome diff --git a/esphome/components/midea/air_conditioner.h b/esphome/components/midea/air_conditioner.h index a6023b78bb..d809aa78f6 100644 --- a/esphome/components/midea/air_conditioner.h +++ b/esphome/components/midea/air_conditioner.h @@ -39,6 +39,7 @@ class AirConditioner : public ApplianceBase, void do_beeper_off() { this->set_beeper_feedback(false); } void do_power_on() { this->base_.setPowerState(true); } void do_power_off() { this->base_.setPowerState(false); } + void do_power_toggle() { this->base_.setPowerState(this->mode == ClimateMode::CLIMATE_MODE_OFF); } void set_supported_modes(const std::set &modes) { this->supported_modes_ = modes; } void set_supported_swing_modes(const std::set &modes) { this->supported_swing_modes_ = modes; } void set_supported_presets(const std::set &presets) { this->supported_presets_ = presets; } diff --git a/esphome/components/midea/climate.py b/esphome/components/midea/climate.py index 46c0019efa..80b1461576 100644 --- a/esphome/components/midea/climate.py +++ b/esphome/components/midea/climate.py @@ -113,7 +113,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_PERIOD, default="1s"): cv.time_period, cv.Optional(CONF_TIMEOUT, default="2s"): cv.time_period, cv.Optional(CONF_NUM_ATTEMPTS, default=3): cv.int_range(min=1, max=5), - cv.Optional(CONF_TRANSMITTER_ID): cv.use_id( + cv.OnlyWith(CONF_TRANSMITTER_ID, "remote_transmitter"): cv.use_id( remote_transmitter.RemoteTransmitterComponent ), cv.Optional(CONF_BEEPER, default=False): cv.boolean, @@ -163,6 +163,7 @@ BeeperOnAction = midea_ac_ns.class_("BeeperOnAction", automation.Action) BeeperOffAction = midea_ac_ns.class_("BeeperOffAction", automation.Action) PowerOnAction = midea_ac_ns.class_("PowerOnAction", automation.Action) PowerOffAction = midea_ac_ns.class_("PowerOffAction", automation.Action) +PowerToggleAction = midea_ac_ns.class_("PowerToggleAction", automation.Action) MIDEA_ACTION_BASE_SCHEMA = cv.Schema( { @@ -249,6 +250,16 @@ async def power_off_to_code(var, config, args): pass +# Power Toggle action +@register_action( + "power_toggle", + PowerToggleAction, + cv.Schema({}), +) +async def power_inv_to_code(var, config, args): + pass + + async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) diff --git a/tests/test1.yaml b/tests/test1.yaml index 7bb1fbe954..52aa03c371 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -1901,14 +1901,6 @@ script: preset: SLEEP switch: - - platform: template - name: MIDEA_AC_TOGGLE_LIGHT - turn_on_action: - midea_ac.display_toggle: - - platform: template - name: MIDEA_AC_SWING_STEP - turn_on_action: - midea_ac.swing_step: - platform: template name: MIDEA_AC_BEEPER_CONTROL optimistic: true @@ -2834,3 +2826,23 @@ button: id: scd40 - scd4x.factory_reset: id: scd40 + - platform: template + name: Midea Display Toggle + on_press: + midea_ac.display_toggle: + - platform: template + name: Midea Swing Step + on_press: + midea_ac.swing_step: + - platform: template + name: Midea Power On + on_press: + midea_ac.power_on: + - platform: template + name: Midea Power Off + on_press: + midea_ac.power_off: + - platform: template + name: Midea Power Inverse + on_press: + midea_ac.power_toggle: From a8ceeaa7b0616b132091f2fcd1d02c137238e4d8 Mon Sep 17 00:00:00 2001 From: Martin <25747549+martgras@users.noreply.github.com> Date: Mon, 23 May 2022 10:56:26 +0200 Subject: [PATCH 27/60] esp32: fix NVS (#3497) --- esphome/components/esp32/preferences.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/esp32/preferences.cpp b/esphome/components/esp32/preferences.cpp index a78159825e..aa03c5acc7 100644 --- a/esphome/components/esp32/preferences.cpp +++ b/esphome/components/esp32/preferences.cpp @@ -156,7 +156,7 @@ class ESP32Preferences : public ESPPreferences { ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", to_save.key.c_str(), esp_err_to_name(err)); return true; } - return to_save.data == stored_data.data; + return to_save.data != stored_data.data; } }; From 9dc804ee27426ee3550855347e30b4ebd2fcd2d9 Mon Sep 17 00:00:00 2001 From: joseph douce Date: Tue, 24 May 2022 01:52:54 +0100 Subject: [PATCH 28/60] Output a true RMS voltage % (#3494) --- esphome/components/ac_dimmer/ac_dimmer.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/esphome/components/ac_dimmer/ac_dimmer.cpp b/esphome/components/ac_dimmer/ac_dimmer.cpp index e9af828a9d..1d0cd8d0ab 100644 --- a/esphome/components/ac_dimmer/ac_dimmer.cpp +++ b/esphome/components/ac_dimmer/ac_dimmer.cpp @@ -121,7 +121,11 @@ void IRAM_ATTR HOT AcDimmerDataStore::gpio_intr() { // calculate time until enable in µs: (1.0-value)*cycle_time, but with integer arithmetic // also take into account min_power auto min_us = this->cycle_time_us * this->min_power / 1000; - this->enable_time_us = std::max((uint32_t) 1, ((65535 - this->value) * (this->cycle_time_us - min_us)) / 65535); + // calculate required value to provide a true RMS voltage output + this->enable_time_us = + std::max((uint32_t) 1, (uint32_t)((65535 - (acos(1 - (2 * this->value / 65535.0)) / 3.14159 * 65535)) * + (this->cycle_time_us - min_us)) / + 65535); if (this->method == DIM_METHOD_LEADING_PULSE) { // Minimum pulse time should be enough for the triac to trigger when it is close to the ZC zone // this is for brightness near 99% From cd35ead890a934d474093e6c9791c5551e08ce04 Mon Sep 17 00:00:00 2001 From: Wumpf Date: Tue, 24 May 2022 03:00:06 +0200 Subject: [PATCH 29/60] [scd4x] Fix not passing arguments to templatable value for perform_forced_calibration (#3495) --- esphome/components/scd4x/automation.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/scd4x/automation.h b/esphome/components/scd4x/automation.h index 21ecb2ea4c..dc43e9eb56 100644 --- a/esphome/components/scd4x/automation.h +++ b/esphome/components/scd4x/automation.h @@ -11,7 +11,7 @@ template class PerformForcedCalibrationAction : public Actionvalue_.has_value()) { - this->parent_->perform_forced_calibration(value_.value()); + this->parent_->perform_forced_calibration(this->value_.value(x...)); } } From adb7aa69509d2389144433e36a0cf965c47776c0 Mon Sep 17 00:00:00 2001 From: Michael Davidson Date: Wed, 25 May 2022 13:44:26 +1000 Subject: [PATCH 30/60] Thermostat preset with modes (#3298) * Rework HOME/AWAY support to being driven via a map of ClimatePreset/ThermostatClimateTargetTempConfig This opens up to theoretically being able to support other presets (ECO, SLEEP, etc) * Add support for additional presets Configuration takes the form; ``` climate: platform: preset ... preset: [eco | away | boost | comfort | home | sleep | activity]: default_target_temperature_low: 20 default_target_temperature_high: 24 ``` These will be available in the Home Assistant UI and, like the existing Home/Away config will reset the temperature in line with these defaults when selected. The existing away_config/home_config is still respected (although preset->home/preset->away will be applied after them and override them if both styles are specified) * Add support for specifying MODE, FAN_MODE and SWING_MODE on a preset When switching presets these will implicitly flow through to the controller. However calls to climate.control which specify any of these will take precedence even when changing the mode (think of the preset version as the default for that preset) * Add `preset_change` mode trigger When defined this trigger will fire when the preset for the thermostat has been changed. The intent of this is similar to `auto_mode` - it's not intended to be used to control the preset's state (eg. communicate with the physical thermostat) but instead might be used to update a visual indicator, for instance. * Apply lint, clang-format, and clang-tidy fixes * Additional clang-format fixes * Wrap log related strings in LOG_STR_ARG * Add support for custom presets This also changes the configuration syntax to; ```yaml preset: # Standard preset - name: [eco | away | boost | comfort | home | sleep | activity] default_target_temperature_low: 20 ... # Custom preset - name: My custom preset default_target_temperature_low: 18 ``` For the end user there is no difference between a custom and built in preset. For developers custom presets are set via `climate.control` `custom_preset` property instead of the `preset` * Lint/clang-format/clang-tidy fixes * Additional lint/clang-format/clang-tidy fixes * Clang-tidy changes * Sort imports * Improve configuration validation for presets - Unify temperature validation across default, away, and preset configuration - Validate modes for presets have the required actions * Trigger a refresh after changing internals of the thermostat * Apply formatting fixes * Validate mode, fan_mode, and swing_mode on presets * Add preset temperature validation against visual min/max configuration * Apply code formatting fixes * Fix preset temperature validation --- esphome/components/thermostat/climate.py | 235 ++++++++++++++++-- .../thermostat/thermostat_climate.cpp | 191 ++++++++++---- .../thermostat/thermostat_climate.h | 42 +++- 3 files changed, 373 insertions(+), 95 deletions(-) diff --git a/esphome/components/thermostat/climate.py b/esphome/components/thermostat/climate.py index 20565e811c..5e26e6d6de 100644 --- a/esphome/components/thermostat/climate.py +++ b/esphome/components/thermostat/climate.py @@ -14,6 +14,7 @@ from esphome.const import ( CONF_DEFAULT_TARGET_TEMPERATURE_LOW, CONF_DRY_ACTION, CONF_DRY_MODE, + CONF_FAN_MODE, CONF_FAN_MODE_ON_ACTION, CONF_FAN_MODE_OFF_ACTION, CONF_FAN_MODE_AUTO_ACTION, @@ -37,6 +38,7 @@ from esphome.const import ( CONF_IDLE_ACTION, CONF_MAX_COOLING_RUN_TIME, CONF_MAX_HEATING_RUN_TIME, + CONF_MAX_TEMPERATURE, CONF_MIN_COOLING_OFF_TIME, CONF_MIN_COOLING_RUN_TIME, CONF_MIN_FAN_MODE_SWITCHING_TIME, @@ -45,7 +47,11 @@ from esphome.const import ( CONF_MIN_HEATING_OFF_TIME, CONF_MIN_HEATING_RUN_TIME, CONF_MIN_IDLE_TIME, + CONF_MIN_TEMPERATURE, + CONF_NAME, + CONF_MODE, CONF_OFF_MODE, + CONF_PRESET, CONF_SENSOR, CONF_SET_POINT_MINIMUM_DIFFERENTIAL, CONF_STARTUP_DELAY, @@ -55,11 +61,15 @@ from esphome.const import ( CONF_SUPPLEMENTAL_HEATING_DELTA, CONF_SWING_BOTH_ACTION, CONF_SWING_HORIZONTAL_ACTION, + CONF_SWING_MODE, CONF_SWING_OFF_ACTION, CONF_SWING_VERTICAL_ACTION, CONF_TARGET_TEMPERATURE_CHANGE_ACTION, + CONF_VISUAL, ) +CONF_PRESET_CHANGE = "preset_change" + CODEOWNERS = ["@kbx81"] climate_ns = cg.esphome_ns.namespace("climate") @@ -82,6 +92,38 @@ CLIMATE_MODES = { } validate_climate_mode = cv.enum(CLIMATE_MODES, upper=True) +ClimatePreset = climate_ns.enum("ClimatePreset") + +PRESET_CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(ThermostatClimateTargetTempConfig), + cv.Required(CONF_NAME): cv.string_strict, + cv.Optional(CONF_MODE): validate_climate_mode, + cv.Optional(CONF_DEFAULT_TARGET_TEMPERATURE_HIGH): cv.temperature, + cv.Optional(CONF_DEFAULT_TARGET_TEMPERATURE_LOW): cv.temperature, + cv.Optional(CONF_FAN_MODE): cv.templatable(climate.validate_climate_fan_mode), + cv.Optional(CONF_SWING_MODE): cv.templatable( + climate.validate_climate_swing_mode + ), + } +) + + +def validate_temperature_preset(preset, root_config, name, requirements): + # verify temperature settings for the provided preset / default / away configuration + for config_temp, req_actions in requirements.items(): + for req_action in req_actions: + # verify corresponding default target temperature exists when a given climate action exists + if config_temp not in preset and req_action in root_config: + raise cv.Invalid( + f"{config_temp} must be defined in {name} config when using {req_action}" + ) + # if a given climate action is NOT defined, it should not have a default target temperature + if config_temp in preset and req_action not in root_config: + raise cv.Invalid( + f"{config_temp} is defined in {name} config with no {req_action}" + ) + def validate_thermostat(config): # verify corresponding action(s) exist(s) for any defined climate mode or action @@ -235,33 +277,22 @@ def validate_thermostat(config): CONF_DEFAULT_TARGET_TEMPERATURE_LOW: [CONF_HEAT_ACTION], } - for config_temp, req_actions in requirements.items(): - for req_action in req_actions: - # verify corresponding default target temperature exists when a given climate action exists - if config_temp not in config and req_action in config: - raise cv.Invalid( - f"{config_temp} must be defined when using {req_action}" - ) - # if a given climate action is NOT defined, it should not have a default target temperature - if config_temp in config and req_action not in config: - raise cv.Invalid(f"{config_temp} is defined with no {req_action}") + # Validate temperature requirements for default configuraation + validate_temperature_preset(config, config, "default", requirements) + # Validate temperature requirements for away configuration if CONF_AWAY_CONFIG in config: away = config[CONF_AWAY_CONFIG] - for config_temp, req_actions in requirements.items(): - for req_action in req_actions: - # verify corresponding default target temperature exists when a given climate action exists - if config_temp not in away and req_action in config: - raise cv.Invalid( - f"{config_temp} must be defined in away configuration when using {req_action}" - ) - # if a given climate action is NOT defined, it should not have a default target temperature - if config_temp in away and req_action not in config: - raise cv.Invalid( - f"{config_temp} is defined in away configuration with no {req_action}" - ) + validate_temperature_preset(away, config, "away", requirements) - # verify default climate mode is valid given above configuration + # Validate temperature requirements for presets + if CONF_PRESET in config: + for preset_config in config[CONF_PRESET]: + validate_temperature_preset( + preset_config, config, preset_config[CONF_NAME], requirements + ) + + # Verify default climate mode is valid given above configuration default_mode = config[CONF_DEFAULT_MODE] requirements = { "HEAT_COOL": [CONF_COOL_ACTION, CONF_HEAT_ACTION], @@ -270,13 +301,108 @@ def validate_thermostat(config): "DRY": [CONF_DRY_ACTION], "FAN_ONLY": [CONF_FAN_ONLY_ACTION], "AUTO": [CONF_COOL_ACTION, CONF_HEAT_ACTION], - }.get(default_mode, []) - for req in requirements: + "OFF": [], + } + actions_for_default_mode = requirements.get(default_mode, []) + for req in actions_for_default_mode: if req not in config: raise cv.Invalid( f"{CONF_DEFAULT_MODE} is set to {default_mode} but {req} is not present in the configuration" ) + # Verify that the modes for presets are valid given the configuration + if CONF_PRESET in config: + # Preset temperature vs Visual temperature validation + + # Default visual configuration from climate_traits.h + visual_min_temperature = 10.0 + visual_max_temperature = 30.0 + if CONF_VISUAL in config: + visual_config = config[CONF_VISUAL] + + if CONF_MIN_TEMPERATURE in visual_config: + visual_min_temperature = visual_config[CONF_MIN_TEMPERATURE] + + if CONF_MAX_TEMPERATURE in visual_config: + visual_max_temperature = visual_config[CONF_MAX_TEMPERATURE] + + for preset_config in config[CONF_PRESET]: + if CONF_DEFAULT_TARGET_TEMPERATURE_LOW in preset_config: + preset_min_temperature = preset_config[ + CONF_DEFAULT_TARGET_TEMPERATURE_LOW + ] + if preset_min_temperature < visual_min_temperature: + raise cv.Invalid( + f"{CONF_DEFAULT_TARGET_TEMPERATURE_LOW} for {preset_config[CONF_NAME]} is set to {preset_min_temperature} which is less than the visual minimum temperature of {visual_min_temperature}" + ) + + if CONF_DEFAULT_TARGET_TEMPERATURE_HIGH in preset_config: + preset_max_temperature = preset_config[ + CONF_DEFAULT_TARGET_TEMPERATURE_HIGH + ] + if preset_max_temperature > visual_max_temperature: + raise cv.Invalid( + f"{CONF_DEFAULT_TARGET_TEMPERATURE_HIGH} for {preset_config[CONF_NAME]} is set to {preset_max_temperature} which is more than the visual maximum temperature of {visual_max_temperature}" + ) + + # Mode validation + for preset_config in config[CONF_PRESET]: + if CONF_MODE not in preset_config: + continue + + mode = preset_config[CONF_MODE] + + for req in requirements[mode]: + if req not in config: + raise cv.Invalid( + f"{CONF_MODE} is set to {mode} for {preset_config[CONF_NAME]} but {req} is not present in the configuration" + ) + + # Fan mode requirements + requirements = { + "ON": [CONF_FAN_MODE_ON_ACTION], + "OFF": [CONF_FAN_MODE_OFF_ACTION], + "AUTO": [CONF_FAN_MODE_AUTO_ACTION], + "LOW": [CONF_FAN_MODE_LOW_ACTION], + "MEDIUM": [CONF_FAN_MODE_MEDIUM_ACTION], + "HIGH": [CONF_FAN_MODE_HIGH_ACTION], + "MIDDLE": [CONF_FAN_MODE_MIDDLE_ACTION], + "FOCUS": [CONF_FAN_MODE_FOCUS_ACTION], + "DIFFUSE": [CONF_FAN_MODE_DIFFUSE_ACTION], + } + + for preset_config in config[CONF_PRESET]: + if CONF_FAN_MODE not in preset_config: + continue + + fan_mode = preset_config[CONF_FAN_MODE] + + for req in requirements[fan_mode]: + if req not in config: + raise cv.Invalid( + f"{CONF_FAN_MODE} is set to {fan_mode} for {preset_config[CONF_NAME]} but {req} is not present in the configuration" + ) + + # Swing mode requirements + requirements = { + "OFF": [CONF_SWING_OFF_ACTION], + "BOTH": [CONF_SWING_BOTH_ACTION], + "VERTICAL": [CONF_SWING_VERTICAL_ACTION], + "HORIZONTAL": [CONF_SWING_HORIZONTAL_ACTION], + } + + for preset_config in config[CONF_PRESET]: + if CONF_SWING_MODE not in preset_config: + continue + + swing_mode = preset_config[CONF_SWING_MODE] + + for req in requirements[swing_mode]: + if req not in config: + raise cv.Invalid( + f"{CONF_SWING_MODE} is set to {swing_mode} for {preset_config[CONF_NAME]} but {req} is not present in the configuration" + ) + if config[CONF_FAN_WITH_COOLING] is True and CONF_FAN_ONLY_ACTION not in config: raise cv.Invalid( f"{CONF_FAN_ONLY_ACTION} must be defined to use {CONF_FAN_WITH_COOLING}" @@ -415,6 +541,10 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_DEFAULT_TARGET_TEMPERATURE_LOW): cv.temperature, } ), + cv.Optional(CONF_PRESET): cv.ensure_list(PRESET_CONFIG_SCHEMA), + cv.Optional(CONF_PRESET_CHANGE): automation.validate_automation( + single=True + ), } ).extend(cv.COMPONENT_SCHEMA), cv.has_at_least_one_key( @@ -531,7 +661,7 @@ async def to_code(config): cg.add(var.set_supports_fan_with_heating(config[CONF_FAN_WITH_HEATING])) cg.add(var.set_use_startup_delay(config[CONF_STARTUP_DELAY])) - cg.add(var.set_normal_config(normal_config)) + cg.add(var.set_preset_config(ClimatePreset.CLIMATE_PRESET_HOME, normal_config)) await automation.build_automation( var.get_idle_action_trigger(), [], config[CONF_IDLE_ACTION] @@ -694,4 +824,55 @@ async def to_code(config): away_config = ThermostatClimateTargetTempConfig( away[CONF_DEFAULT_TARGET_TEMPERATURE_LOW] ) - cg.add(var.set_away_config(away_config)) + cg.add(var.set_preset_config(ClimatePreset.CLIMATE_PRESET_AWAY, away_config)) + + if CONF_PRESET in config: + for preset_config in config[CONF_PRESET]: + + name = preset_config[CONF_NAME] + standard_preset = None + if name.upper() in climate.CLIMATE_PRESETS: + standard_preset = climate.CLIMATE_PRESETS[name.upper()] + + if two_points_available is True: + preset_target_config = ThermostatClimateTargetTempConfig( + preset_config[CONF_DEFAULT_TARGET_TEMPERATURE_LOW], + preset_config[CONF_DEFAULT_TARGET_TEMPERATURE_HIGH], + ) + elif CONF_DEFAULT_TARGET_TEMPERATURE_HIGH in preset_config: + preset_target_config = ThermostatClimateTargetTempConfig( + preset_config[CONF_DEFAULT_TARGET_TEMPERATURE_HIGH] + ) + elif CONF_DEFAULT_TARGET_TEMPERATURE_LOW in preset_config: + preset_target_config = ThermostatClimateTargetTempConfig( + preset_config[CONF_DEFAULT_TARGET_TEMPERATURE_LOW] + ) + + preset_target_variable = cg.new_variable( + preset_config[CONF_ID], preset_target_config + ) + + if CONF_MODE in preset_config: + cg.add(preset_target_variable.set_mode(preset_config[CONF_MODE])) + + if CONF_FAN_MODE in preset_config: + cg.add( + preset_target_variable.set_fan_mode(preset_config[CONF_FAN_MODE]) + ) + + if CONF_SWING_MODE in preset_config: + cg.add( + preset_target_variable.set_swing_mode( + preset_config[CONF_SWING_MODE] + ) + ) + + if standard_preset is not None: + cg.add(var.set_preset_config(standard_preset, preset_target_variable)) + else: + cg.add(var.set_custom_preset_config(name, preset_target_variable)) + + if CONF_PRESET_CHANGE in config: + await automation.build_automation( + var.get_preset_change_trigger(), [], config[CONF_PRESET_CHANGE] + ) diff --git a/esphome/components/thermostat/thermostat_climate.cpp b/esphome/components/thermostat/thermostat_climate.cpp index 760525e2cd..dc4e1e437e 100644 --- a/esphome/components/thermostat/thermostat_climate.cpp +++ b/esphome/components/thermostat/thermostat_climate.cpp @@ -32,7 +32,7 @@ void ThermostatClimate::setup() { } else { // restore from defaults, change_away handles temps for us this->mode = this->default_mode_; - this->change_away_(false); + this->change_preset_(climate::CLIMATE_PRESET_HOME); } // refresh the climate action based on the restored settings, we'll publish_state() later this->switch_to_action_(this->compute_action_(), false); @@ -162,11 +162,20 @@ void ThermostatClimate::control(const climate::ClimateCall &call) { if (call.get_preset().has_value()) { // setup_complete_ blocks modifying/resetting the temps immediately after boot if (this->setup_complete_) { - this->change_away_(*call.get_preset() == climate::CLIMATE_PRESET_AWAY); + this->change_preset_(*call.get_preset()); } else { this->preset = *call.get_preset(); } } + if (call.get_custom_preset().has_value()) { + // setup_complete_ blocks modifying/resetting the temps immediately after boot + if (this->setup_complete_) { + this->change_custom_preset_(*call.get_custom_preset()); + } else { + this->custom_preset = *call.get_custom_preset(); + } + } + if (call.get_mode().has_value()) this->mode = *call.get_mode(); if (call.get_fan_mode().has_value()) @@ -236,8 +245,12 @@ climate::ClimateTraits ThermostatClimate::traits() { if (supports_swing_mode_vertical_) traits.add_supported_swing_mode(climate::CLIMATE_SWING_VERTICAL); - if (supports_away_) - traits.set_supported_presets({climate::CLIMATE_PRESET_HOME, climate::CLIMATE_PRESET_AWAY}); + for (auto &it : this->preset_config_) { + traits.add_supported_preset(it.first); + } + for (auto &it : this->custom_preset_config_) { + traits.add_supported_custom_preset(it.first); + } traits.set_supports_two_point_target_temperature(this->supports_two_points_); traits.set_supports_action(true); @@ -910,30 +923,112 @@ bool ThermostatClimate::supplemental_heating_required_() { (this->supplemental_action_ == climate::CLIMATE_ACTION_HEATING)); } -void ThermostatClimate::change_away_(bool away) { - if (!away) { +void ThermostatClimate::dump_preset_config_(const std::string &preset, + const ThermostatClimateTargetTempConfig &config) { + const auto *preset_name = preset.c_str(); + + if (this->supports_heat_) { if (this->supports_two_points_) { - this->target_temperature_low = this->normal_config_.default_temperature_low; - this->target_temperature_high = this->normal_config_.default_temperature_high; - } else - this->target_temperature = this->normal_config_.default_temperature; - } else { - if (this->supports_two_points_) { - this->target_temperature_low = this->away_config_.default_temperature_low; - this->target_temperature_high = this->away_config_.default_temperature_high; - } else - this->target_temperature = this->away_config_.default_temperature; + ESP_LOGCONFIG(TAG, " %s Default Target Temperature Low: %.1f°C", preset_name, + config.default_temperature_low); + } else { + ESP_LOGCONFIG(TAG, " %s Default Target Temperature Low: %.1f°C", preset_name, config.default_temperature); + } + } + if ((this->supports_cool_) || (this->supports_fan_only_)) { + if (this->supports_two_points_) { + ESP_LOGCONFIG(TAG, " %s Default Target Temperature High: %.1f°C", preset_name, + config.default_temperature_high); + } else { + ESP_LOGCONFIG(TAG, " %s Default Target Temperature High: %.1f°C", preset_name, config.default_temperature); + } + } + + if (config.mode_.has_value()) { + ESP_LOGCONFIG(TAG, " %s Default Mode: %s", preset_name, + LOG_STR_ARG(climate::climate_mode_to_string(*config.mode_))); + } + if (config.fan_mode_.has_value()) { + ESP_LOGCONFIG(TAG, " %s Default Fan Mode: %s", preset_name, + LOG_STR_ARG(climate::climate_fan_mode_to_string(*config.fan_mode_))); + } + if (config.swing_mode_.has_value()) { + ESP_LOGCONFIG(TAG, " %s Default Swing Mode: %s", preset_name, + LOG_STR_ARG(climate::climate_swing_mode_to_string(*config.swing_mode_))); } - this->preset = away ? climate::CLIMATE_PRESET_AWAY : climate::CLIMATE_PRESET_HOME; } -void ThermostatClimate::set_normal_config(const ThermostatClimateTargetTempConfig &normal_config) { - this->normal_config_ = normal_config; +void ThermostatClimate::change_preset_(climate::ClimatePreset preset) { + auto config = this->preset_config_.find(preset); + + if (config != this->preset_config_.end()) { + ESP_LOGI(TAG, "Switching to preset %s", LOG_STR_ARG(climate::climate_preset_to_string(preset))); + this->change_preset_internal_(config->second); + + this->custom_preset.reset(); + this->preset = preset; + } else { + ESP_LOGE(TAG, "Preset %s is not configured, ignoring.", LOG_STR_ARG(climate::climate_preset_to_string(preset))); + } } -void ThermostatClimate::set_away_config(const ThermostatClimateTargetTempConfig &away_config) { - this->supports_away_ = true; - this->away_config_ = away_config; +void ThermostatClimate::change_custom_preset_(const std::string &custom_preset) { + auto config = this->custom_preset_config_.find(custom_preset); + + if (config != this->custom_preset_config_.end()) { + ESP_LOGI(TAG, "Switching to custom preset %s", custom_preset.c_str()); + this->change_preset_internal_(config->second); + + this->preset.reset(); + this->custom_preset = custom_preset; + } else { + ESP_LOGE(TAG, "Custom Preset %s is not configured, ignoring.", custom_preset.c_str()); + } +} + +void ThermostatClimate::change_preset_internal_(const ThermostatClimateTargetTempConfig &config) { + if (this->supports_two_points_) { + this->target_temperature_low = config.default_temperature_low; + this->target_temperature_high = config.default_temperature_high; + } else { + this->target_temperature = config.default_temperature; + } + + // Note: The mode, fan_mode, and swing_mode can all be defined on the preset but if the climate.control call + // also specifies them then the control's version will override these for that call + if (config.mode_.has_value()) { + this->mode = *config.mode_; + ESP_LOGV(TAG, "Setting mode to %s", LOG_STR_ARG(climate::climate_mode_to_string(*config.mode_))); + } + + if (config.fan_mode_.has_value()) { + this->fan_mode = *config.fan_mode_; + ESP_LOGV(TAG, "Setting fan mode to %s", LOG_STR_ARG(climate::climate_fan_mode_to_string(*config.fan_mode_))); + } + + if (config.swing_mode_.has_value()) { + ESP_LOGV(TAG, "Setting swing mode to %s", LOG_STR_ARG(climate::climate_swing_mode_to_string(*config.swing_mode_))); + this->swing_mode = *config.swing_mode_; + } + + // Fire any preset changed trigger if defined + if (this->preset != preset) { + Trigger<> *trig = this->preset_change_trigger_; + assert(trig != nullptr); + trig->trigger(); + } + + this->refresh(); +} + +void ThermostatClimate::set_preset_config(climate::ClimatePreset preset, + const ThermostatClimateTargetTempConfig &config) { + this->preset_config_[preset] = config; +} + +void ThermostatClimate::set_custom_preset_config(const std::string &name, + const ThermostatClimateTargetTempConfig &config) { + this->custom_preset_config_[name] = config; } ThermostatClimate::ThermostatClimate() @@ -963,7 +1058,8 @@ ThermostatClimate::ThermostatClimate() swing_mode_off_trigger_(new Trigger<>()), swing_mode_horizontal_trigger_(new Trigger<>()), swing_mode_vertical_trigger_(new Trigger<>()), - temperature_change_trigger_(new Trigger<>()) {} + temperature_change_trigger_(new Trigger<>()), + preset_change_trigger_(new Trigger<>()) {} void ThermostatClimate::set_default_mode(climate::ClimateMode default_mode) { this->default_mode_ = default_mode; } void ThermostatClimate::set_set_point_minimum_differential(float differential) { @@ -1112,23 +1208,11 @@ Trigger<> *ThermostatClimate::get_swing_mode_off_trigger() const { return this-> Trigger<> *ThermostatClimate::get_swing_mode_horizontal_trigger() const { return this->swing_mode_horizontal_trigger_; } Trigger<> *ThermostatClimate::get_swing_mode_vertical_trigger() const { return this->swing_mode_vertical_trigger_; } Trigger<> *ThermostatClimate::get_temperature_change_trigger() const { return this->temperature_change_trigger_; } +Trigger<> *ThermostatClimate::get_preset_change_trigger() const { return this->preset_change_trigger_; } void ThermostatClimate::dump_config() { LOG_CLIMATE("", "Thermostat", this); - if (this->supports_heat_) { - if (this->supports_two_points_) { - ESP_LOGCONFIG(TAG, " Default Target Temperature Low: %.1f°C", this->normal_config_.default_temperature_low); - } else { - ESP_LOGCONFIG(TAG, " Default Target Temperature Low: %.1f°C", this->normal_config_.default_temperature); - } - } - if ((this->supports_cool_) || (this->supports_fan_only_ && this->supports_fan_only_cooling_)) { - if (this->supports_two_points_) { - ESP_LOGCONFIG(TAG, " Default Target Temperature High: %.1f°C", this->normal_config_.default_temperature_high); - } else { - ESP_LOGCONFIG(TAG, " Default Target Temperature High: %.1f°C", this->normal_config_.default_temperature); - } - } + if (this->supports_two_points_) ESP_LOGCONFIG(TAG, " Minimum Set Point Differential: %.1f°C", this->set_point_minimum_differential_); ESP_LOGCONFIG(TAG, " Start-up Delay Enabled: %s", YESNO(this->use_startup_delay_)); @@ -1194,24 +1278,21 @@ void ThermostatClimate::dump_config() { ESP_LOGCONFIG(TAG, " Supports SWING MODE HORIZONTAL: %s", YESNO(this->supports_swing_mode_horizontal_)); ESP_LOGCONFIG(TAG, " Supports SWING MODE VERTICAL: %s", YESNO(this->supports_swing_mode_vertical_)); ESP_LOGCONFIG(TAG, " Supports TWO SET POINTS: %s", YESNO(this->supports_two_points_)); - ESP_LOGCONFIG(TAG, " Supports AWAY mode: %s", YESNO(this->supports_away_)); - if (this->supports_away_) { - if (this->supports_heat_) { - if (this->supports_two_points_) { - ESP_LOGCONFIG(TAG, " Away Default Target Temperature Low: %.1f°C", - this->away_config_.default_temperature_low); - } else { - ESP_LOGCONFIG(TAG, " Away Default Target Temperature Low: %.1f°C", this->away_config_.default_temperature); - } - } - if ((this->supports_cool_) || (this->supports_fan_only_)) { - if (this->supports_two_points_) { - ESP_LOGCONFIG(TAG, " Away Default Target Temperature High: %.1f°C", - this->away_config_.default_temperature_high); - } else { - ESP_LOGCONFIG(TAG, " Away Default Target Temperature High: %.1f°C", this->away_config_.default_temperature); - } - } + + ESP_LOGCONFIG(TAG, " Supported PRESETS: "); + for (auto &it : this->preset_config_) { + const auto *preset_name = LOG_STR_ARG(climate::climate_preset_to_string(it.first)); + + ESP_LOGCONFIG(TAG, " Supports %s: %s", preset_name, YESNO(true)); + this->dump_preset_config_(preset_name, it.second); + } + + ESP_LOGCONFIG(TAG, " Supported CUSTOM PRESETS: "); + for (auto &it : this->custom_preset_config_) { + const auto *preset_name = it.first.c_str(); + + ESP_LOGCONFIG(TAG, " Supports %s: %s", preset_name, YESNO(true)); + this->dump_preset_config_(preset_name, it.second); } } diff --git a/esphome/components/thermostat/thermostat_climate.h b/esphome/components/thermostat/thermostat_climate.h index 8d3e926752..c9231370ba 100644 --- a/esphome/components/thermostat/thermostat_climate.h +++ b/esphome/components/thermostat/thermostat_climate.h @@ -4,6 +4,7 @@ #include "esphome/core/automation.h" #include "esphome/components/climate/climate.h" #include "esphome/components/sensor/sensor.h" +#include namespace esphome { namespace thermostat { @@ -34,6 +35,10 @@ struct ThermostatClimateTargetTempConfig { ThermostatClimateTargetTempConfig(float default_temperature); ThermostatClimateTargetTempConfig(float default_temperature_low, float default_temperature_high); + void set_fan_mode(climate::ClimateFanMode fan_mode) { this->fan_mode_ = fan_mode; } + void set_swing_mode(climate::ClimateSwingMode swing_mode) { this->swing_mode_ = swing_mode; } + void set_mode(climate::ClimateMode mode) { this->mode_ = mode; } + float default_temperature{NAN}; float default_temperature_low{NAN}; float default_temperature_high{NAN}; @@ -41,6 +46,9 @@ struct ThermostatClimateTargetTempConfig { float cool_overrun_{NAN}; float heat_deadband_{NAN}; float heat_overrun_{NAN}; + optional fan_mode_{}; + optional swing_mode_{}; + optional mode_{}; }; class ThermostatClimate : public climate::Climate, public Component { @@ -94,8 +102,8 @@ class ThermostatClimate : public climate::Climate, public Component { void set_supports_swing_mode_vertical(bool supports_swing_mode_vertical); void set_supports_two_points(bool supports_two_points); - void set_normal_config(const ThermostatClimateTargetTempConfig &normal_config); - void set_away_config(const ThermostatClimateTargetTempConfig &away_config); + void set_preset_config(climate::ClimatePreset preset, const ThermostatClimateTargetTempConfig &config); + void set_custom_preset_config(const std::string &name, const ThermostatClimateTargetTempConfig &config); Trigger<> *get_cool_action_trigger() const; Trigger<> *get_supplemental_cool_action_trigger() const; @@ -124,6 +132,7 @@ class ThermostatClimate : public climate::Climate, public Component { Trigger<> *get_swing_mode_off_trigger() const; Trigger<> *get_swing_mode_vertical_trigger() const; Trigger<> *get_temperature_change_trigger() const; + Trigger<> *get_preset_change_trigger() const; /// Get current hysteresis values float cool_deadband(); float cool_overrun(); @@ -149,8 +158,14 @@ class ThermostatClimate : public climate::Climate, public Component { /// Override control to change settings of the climate device. void control(const climate::ClimateCall &call) override; - /// Change the away setting, will reset target temperatures to defaults. - void change_away_(bool away); + /// Change to a provided preset setting; will reset temperature, mode, fan, and swing modes accordingly + void change_preset_(climate::ClimatePreset preset); + /// Change to a provided custom preset setting; will reset temperature, mode, fan, and swing modes accordingly + void change_custom_preset_(const std::string &custom_preset); + + /// Applies the temperature, mode, fan, and swing modes of the provded config. + /// This is agnostic of custom vs built in preset + void change_preset_internal_(const ThermostatClimateTargetTempConfig &config); /// Return the traits of this controller. climate::ClimateTraits traits() override; @@ -210,6 +225,8 @@ class ThermostatClimate : public climate::Climate, public Component { bool supplemental_cooling_required_(); bool supplemental_heating_required_(); + void dump_preset_config_(const std::string &preset_name, const ThermostatClimateTargetTempConfig &config); + /// The sensor used for getting the current temperature sensor::Sensor *sensor_{nullptr}; @@ -267,11 +284,6 @@ class ThermostatClimate : public climate::Climate, public Component { /// A false value means that the controller has no such support. bool supports_two_points_{false}; - /// Whether the controller supports an "away" mode - /// - /// A false value means that the controller has no such mode. - bool supports_away_{false}; - /// Flags indicating if maximum allowable run time was exceeded bool cooling_max_runtime_exceeded_{false}; bool heating_max_runtime_exceeded_{false}; @@ -368,6 +380,9 @@ class ThermostatClimate : public climate::Climate, public Component { /// The trigger to call when the target temperature(s) change(es). Trigger<> *temperature_change_trigger_{nullptr}; + /// The triggr to call when the preset mode changes + Trigger<> *preset_change_trigger_{nullptr}; + /// A reference to the trigger that was previously active. /// /// This is so that the previous trigger can be stopped before enabling a new one @@ -409,10 +424,6 @@ class ThermostatClimate : public climate::Climate, public Component { /// Minimum allowable duration in seconds for action timers const uint8_t min_timer_duration_{1}; - /// Temperature data for normal/home and away modes - ThermostatClimateTargetTempConfig normal_config_{}; - ThermostatClimateTargetTempConfig away_config_{}; - /// Climate action timers std::vector timer_{ {"cool_run", false, 0, std::bind(&ThermostatClimate::cooling_max_run_time_timer_callback_, this)}, @@ -425,6 +436,11 @@ class ThermostatClimate : public climate::Climate, public Component { {"heat_off", false, 0, std::bind(&ThermostatClimate::heating_off_timer_callback_, this)}, {"heat_on", false, 0, std::bind(&ThermostatClimate::heating_on_timer_callback_, this)}, {"idle_on", false, 0, std::bind(&ThermostatClimate::idle_on_timer_callback_, this)}}; + + /// The set of standard preset configurations this thermostat supports (Eg. AWAY, ECO, etc) + std::map preset_config_{}; + /// The set of custom preset configurations this thermostat supports (eg. "My Custom Preset") + std::map custom_preset_config_{}; }; } // namespace thermostat From d2cefbf22495b3c62956a455c6bbc63daf40c96b Mon Sep 17 00:00:00 2001 From: Jan Grewe Date: Mon, 30 May 2022 21:29:57 +0200 Subject: [PATCH 31/60] Allow Prometheus component to export internal components (#3508) Co-authored-by: Jan Grewe --- esphome/components/prometheus/__init__.py | 8 +++++++- .../components/prometheus/prometheus_handler.cpp | 14 +++++++------- esphome/components/prometheus/prometheus_handler.h | 8 ++++++++ 3 files changed, 22 insertions(+), 8 deletions(-) diff --git a/esphome/components/prometheus/__init__.py b/esphome/components/prometheus/__init__.py index 45345f06e8..e7c0459251 100644 --- a/esphome/components/prometheus/__init__.py +++ b/esphome/components/prometheus/__init__.py @@ -1,6 +1,9 @@ import esphome.config_validation as cv import esphome.codegen as cg -from esphome.const import CONF_ID +from esphome.const import ( + CONF_ID, + CONF_INCLUDE_INTERNAL, +) from esphome.components.web_server_base import CONF_WEB_SERVER_BASE_ID from esphome.components import web_server_base @@ -15,6 +18,7 @@ CONFIG_SCHEMA = cv.Schema( cv.GenerateID(CONF_WEB_SERVER_BASE_ID): cv.use_id( web_server_base.WebServerBase ), + cv.Optional(CONF_INCLUDE_INTERNAL, default=False): cv.boolean, }, cv.only_with_arduino, ).extend(cv.COMPONENT_SCHEMA) @@ -27,3 +31,5 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID], paren) await cg.register_component(var, config) + + cg.add(var.set_include_internal(config[CONF_INCLUDE_INTERNAL])) diff --git a/esphome/components/prometheus/prometheus_handler.cpp b/esphome/components/prometheus/prometheus_handler.cpp index e4dd6b9043..a52347ba57 100644 --- a/esphome/components/prometheus/prometheus_handler.cpp +++ b/esphome/components/prometheus/prometheus_handler.cpp @@ -61,7 +61,7 @@ void PrometheusHandler::sensor_type_(AsyncResponseStream *stream) { stream->print(F("#TYPE esphome_sensor_failed GAUGE\n")); } void PrometheusHandler::sensor_row_(AsyncResponseStream *stream, sensor::Sensor *obj) { - if (obj->is_internal()) + if (obj->is_internal() && !this->include_internal_) return; if (!std::isnan(obj->state)) { // We have a valid value, output this value @@ -98,7 +98,7 @@ void PrometheusHandler::binary_sensor_type_(AsyncResponseStream *stream) { stream->print(F("#TYPE esphome_binary_sensor_failed GAUGE\n")); } void PrometheusHandler::binary_sensor_row_(AsyncResponseStream *stream, binary_sensor::BinarySensor *obj) { - if (obj->is_internal()) + if (obj->is_internal() && !this->include_internal_) return; if (obj->has_state()) { // We have a valid value, output this value @@ -134,7 +134,7 @@ void PrometheusHandler::fan_type_(AsyncResponseStream *stream) { stream->print(F("#TYPE esphome_fan_oscillation GAUGE\n")); } void PrometheusHandler::fan_row_(AsyncResponseStream *stream, fan::Fan *obj) { - if (obj->is_internal()) + if (obj->is_internal() && !this->include_internal_) return; stream->print(F("esphome_fan_failed{id=\"")); stream->print(obj->get_object_id().c_str()); @@ -179,7 +179,7 @@ void PrometheusHandler::light_type_(AsyncResponseStream *stream) { stream->print(F("#TYPE esphome_light_effect_active GAUGE\n")); } void PrometheusHandler::light_row_(AsyncResponseStream *stream, light::LightState *obj) { - if (obj->is_internal()) + if (obj->is_internal() && !this->include_internal_) return; // State stream->print(F("esphome_light_state{id=\"")); @@ -255,7 +255,7 @@ void PrometheusHandler::cover_type_(AsyncResponseStream *stream) { stream->print(F("#TYPE esphome_cover_failed GAUGE\n")); } void PrometheusHandler::cover_row_(AsyncResponseStream *stream, cover::Cover *obj) { - if (obj->is_internal()) + if (obj->is_internal() && !this->include_internal_) return; if (!std::isnan(obj->position)) { // We have a valid value, output this value @@ -298,7 +298,7 @@ void PrometheusHandler::switch_type_(AsyncResponseStream *stream) { stream->print(F("#TYPE esphome_switch_failed GAUGE\n")); } void PrometheusHandler::switch_row_(AsyncResponseStream *stream, switch_::Switch *obj) { - if (obj->is_internal()) + if (obj->is_internal() && !this->include_internal_) return; stream->print(F("esphome_switch_failed{id=\"")); stream->print(obj->get_object_id().c_str()); @@ -322,7 +322,7 @@ void PrometheusHandler::lock_type_(AsyncResponseStream *stream) { stream->print(F("#TYPE esphome_lock_failed GAUGE\n")); } void PrometheusHandler::lock_row_(AsyncResponseStream *stream, lock::Lock *obj) { - if (obj->is_internal()) + if (obj->is_internal() && !this->include_internal_) return; stream->print(F("esphome_lock_failed{id=\"")); stream->print(obj->get_object_id().c_str()); diff --git a/esphome/components/prometheus/prometheus_handler.h b/esphome/components/prometheus/prometheus_handler.h index 5c8d51c60f..b378e46ea3 100644 --- a/esphome/components/prometheus/prometheus_handler.h +++ b/esphome/components/prometheus/prometheus_handler.h @@ -13,6 +13,13 @@ class PrometheusHandler : public AsyncWebHandler, public Component { public: PrometheusHandler(web_server_base::WebServerBase *base) : base_(base) {} + /** Determine whether internal components should be exported as metrics. + * Defaults to false. + * + * @param include_internal Whether internal components should be exported. + */ + void set_include_internal(bool include_internal) { include_internal_ = include_internal; } + bool canHandle(AsyncWebServerRequest *request) override { if (request->method() == HTTP_GET) { if (request->url() == "/metrics") @@ -84,6 +91,7 @@ class PrometheusHandler : public AsyncWebHandler, public Component { #endif web_server_base::WebServerBase *base_; + bool include_internal_{false}; }; } // namespace prometheus From 708672ec7e18745ca76be2afffb05ca320986568 Mon Sep 17 00:00:00 2001 From: Joe Date: Mon, 30 May 2022 23:45:01 -0400 Subject: [PATCH 32/60] [BedJet] Add configurable heating strategy (#3519) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/bedjet/bedjet.cpp | 28 ++++++++++++++++++++++-- esphome/components/bedjet/bedjet.h | 10 +++++++-- esphome/components/bedjet/bedjet_const.h | 8 +++++++ esphome/components/bedjet/climate.py | 10 +++++++++ tests/test1.yaml | 1 + 5 files changed, 53 insertions(+), 4 deletions(-) diff --git a/esphome/components/bedjet/bedjet.cpp b/esphome/components/bedjet/bedjet.cpp index 38ed6206a8..493685448c 100644 --- a/esphome/components/bedjet/bedjet.cpp +++ b/esphome/components/bedjet/bedjet.cpp @@ -36,6 +36,14 @@ static uint8_t bedjet_fan_speed_to_step(const std::string &fan_step_percent) { return -1; } +static BedjetButton heat_button(BedjetHeatMode mode) { + BedjetButton btn = BTN_HEAT; + if (mode == HEAT_MODE_EXTENDED) { + btn = BTN_EXTHT; + } + return btn; +} + void Bedjet::upgrade_firmware() { auto *pkt = this->codec_->get_button_request(MAGIC_UPDATE); auto status = this->write_bedjet_packet_(pkt); @@ -117,7 +125,7 @@ void Bedjet::control(const ClimateCall &call) { pkt = this->codec_->get_button_request(BTN_OFF); break; case climate::CLIMATE_MODE_HEAT: - pkt = this->codec_->get_button_request(BTN_HEAT); + pkt = this->codec_->get_button_request(heat_button(this->heating_mode_)); break; case climate::CLIMATE_MODE_FAN_ONLY: pkt = this->codec_->get_button_request(BTN_COOL); @@ -186,6 +194,8 @@ void Bedjet::control(const ClimateCall &call) { pkt = this->codec_->get_button_request(BTN_M2); } else if (preset == "M3") { pkt = this->codec_->get_button_request(BTN_M3); + } else if (preset == "LTD HT") { + pkt = this->codec_->get_button_request(BTN_HEAT); } else if (preset == "EXT HT") { pkt = this->codec_->get_button_request(BTN_EXTHT); } else { @@ -557,11 +567,25 @@ bool Bedjet::update_status_() { break; case MODE_HEAT: + this->mode = climate::CLIMATE_MODE_HEAT; + this->action = climate::CLIMATE_ACTION_HEATING; + this->preset.reset(); + if (this->heating_mode_ == HEAT_MODE_EXTENDED) { + this->set_custom_preset_("LTD HT"); + } else { + this->custom_preset.reset(); + } + break; + case MODE_EXTHT: this->mode = climate::CLIMATE_MODE_HEAT; this->action = climate::CLIMATE_ACTION_HEATING; - this->custom_preset.reset(); this->preset.reset(); + if (this->heating_mode_ == HEAT_MODE_EXTENDED) { + this->custom_preset.reset(); + } else { + this->set_custom_preset_("EXT HT"); + } break; case MODE_COOL: diff --git a/esphome/components/bedjet/bedjet.h b/esphome/components/bedjet/bedjet.h index 750a20594f..5d66f6f252 100644 --- a/esphome/components/bedjet/bedjet.h +++ b/esphome/components/bedjet/bedjet.h @@ -40,6 +40,8 @@ class Bedjet : public climate::Climate, public esphome::ble_client::BLEClientNod void set_time_id(time::RealTimeClock *time_id) { this->time_id_ = time_id; } #endif void set_status_timeout(uint32_t timeout) { this->timeout_ = timeout; } + /** Sets the default strategy to use for climate::CLIMATE_MODE_HEAT. */ + void set_heating_mode(BedjetHeatMode mode) { this->heating_mode_ = mode; } /** Attempts to check for and apply firmware updates. */ void upgrade_firmware(); @@ -68,12 +70,15 @@ class Bedjet : public climate::Climate, public esphome::ble_client::BLEClientNod // We could fetch biodata from bedjet and set these names that way. // But then we have to invert the lookup in order to send the right preset. // For now, we can leave them as M1-3 to match the remote buttons. - // EXT HT added to match remote button. - "EXT HT", "M1", "M2", "M3", }); + if (this->heating_mode_ == HEAT_MODE_EXTENDED) { + traits.add_supported_custom_preset("LTD HT"); + } else { + traits.add_supported_custom_preset("EXT HT"); + } traits.set_visual_min_temperature(19.0); traits.set_visual_max_temperature(43.0); traits.set_visual_temperature_step(1.0); @@ -90,6 +95,7 @@ class Bedjet : public climate::Climate, public esphome::ble_client::BLEClientNod #endif uint32_t timeout_{DEFAULT_STATUS_TIMEOUT}; + BedjetHeatMode heating_mode_ = HEAT_MODE_HEAT; static const uint32_t MIN_NOTIFY_THROTTLE = 5000; static const uint32_t NOTIFY_WARN_THRESHOLD = 300000; diff --git a/esphome/components/bedjet/bedjet_const.h b/esphome/components/bedjet/bedjet_const.h index ae10ca1885..16f73717c6 100644 --- a/esphome/components/bedjet/bedjet_const.h +++ b/esphome/components/bedjet/bedjet_const.h @@ -24,6 +24,14 @@ enum BedjetMode : uint8_t { MODE_WAIT = 6, }; +/** Optional heating strategies to use for climate::CLIMATE_MODE_HEAT. */ +enum BedjetHeatMode { + /// HVACMode.HEAT is handled using BTN_HEAT (default) + HEAT_MODE_HEAT, + /// HVACMode.HEAT is handled using BTN_EXTHT + HEAT_MODE_EXTENDED, +}; + enum BedjetButton : uint8_t { /// Turn BedJet off BTN_OFF = 0x1, diff --git a/esphome/components/bedjet/climate.py b/esphome/components/bedjet/climate.py index 49353934f6..d718ba9969 100644 --- a/esphome/components/bedjet/climate.py +++ b/esphome/components/bedjet/climate.py @@ -2,6 +2,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import climate, ble_client, time from esphome.const import ( + CONF_HEAT_MODE, CONF_ID, CONF_RECEIVE_TIMEOUT, CONF_TIME_ID, @@ -14,11 +15,19 @@ bedjet_ns = cg.esphome_ns.namespace("bedjet") Bedjet = bedjet_ns.class_( "Bedjet", climate.Climate, ble_client.BLEClientNode, cg.PollingComponent ) +BedjetHeatMode = bedjet_ns.enum("BedjetHeatMode") +BEDJET_HEAT_MODES = { + "heat": BedjetHeatMode.HEAT_MODE_HEAT, + "extended": BedjetHeatMode.HEAT_MODE_EXTENDED, +} CONFIG_SCHEMA = ( climate.CLIMATE_SCHEMA.extend( { cv.GenerateID(): cv.declare_id(Bedjet), + cv.Optional(CONF_HEAT_MODE, default="heat"): cv.enum( + BEDJET_HEAT_MODES, lower=True + ), cv.Optional(CONF_TIME_ID): cv.use_id(time.RealTimeClock), cv.Optional( CONF_RECEIVE_TIMEOUT, default="0s" @@ -35,6 +44,7 @@ async def to_code(config): await cg.register_component(var, config) await climate.register_climate(var, config) await ble_client.register_ble_node(var, config) + cg.add(var.set_heating_mode(config[CONF_HEAT_MODE])) if CONF_TIME_ID in config: time_ = await cg.get_variable(config[CONF_TIME_ID]) cg.add(var.set_time_id(time_)) diff --git a/tests/test1.yaml b/tests/test1.yaml index 52aa03c371..1b8ed7e370 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -1886,6 +1886,7 @@ climate: - platform: bedjet name: My Bedjet ble_client_id: my_bedjet_ble_client + heat_mode: extended script: - id: climate_custom From 5aa42e5e66a5a65528e3dd6218e17b59970dfc82 Mon Sep 17 00:00:00 2001 From: jimtng <2554958+jimtng@users.noreply.github.com> Date: Tue, 31 May 2022 14:45:18 +1000 Subject: [PATCH 33/60] Add variable substitutions for !include (#3510) --- esphome/components/substitutions/__init__.py | 33 +++++++------- esphome/yaml_util.py | 44 ++++++++++++++++++- .../fixtures/yaml_util/includes/included.yaml | 2 + .../fixtures/yaml_util/includes/list.yaml | 2 + .../fixtures/yaml_util/includes/scalar.yaml | 1 + .../fixtures/yaml_util/includetest.yaml | 17 +++++++ tests/unit_tests/test_yaml_util.py | 13 ++++++ 7 files changed, 95 insertions(+), 17 deletions(-) create mode 100644 tests/unit_tests/fixtures/yaml_util/includes/included.yaml create mode 100644 tests/unit_tests/fixtures/yaml_util/includes/list.yaml create mode 100644 tests/unit_tests/fixtures/yaml_util/includes/scalar.yaml create mode 100644 tests/unit_tests/fixtures/yaml_util/includetest.yaml create mode 100644 tests/unit_tests/test_yaml_util.py diff --git a/esphome/components/substitutions/__init__.py b/esphome/components/substitutions/__init__.py index 6188b14b35..5a3da1abbe 100644 --- a/esphome/components/substitutions/__init__.py +++ b/esphome/components/substitutions/__init__.py @@ -48,7 +48,7 @@ VARIABLE_PROG = re.compile( ) -def _expand_substitutions(substitutions, value, path): +def _expand_substitutions(substitutions, value, path, ignore_missing): if "$" not in value: return value @@ -66,13 +66,14 @@ def _expand_substitutions(substitutions, value, path): if name.startswith("{") and name.endswith("}"): name = name[1:-1] if name not in substitutions: - _LOGGER.warning( - "Found '%s' (see %s) which looks like a substitution, but '%s' was " - "not declared", - orig_value, - "->".join(str(x) for x in path), - name, - ) + if not ignore_missing: + _LOGGER.warning( + "Found '%s' (see %s) which looks like a substitution, but '%s' was " + "not declared", + orig_value, + "->".join(str(x) for x in path), + name, + ) i = j continue @@ -92,37 +93,37 @@ def _expand_substitutions(substitutions, value, path): return value -def _substitute_item(substitutions, item, path): +def _substitute_item(substitutions, item, path, ignore_missing): if isinstance(item, list): for i, it in enumerate(item): - sub = _substitute_item(substitutions, it, path + [i]) + sub = _substitute_item(substitutions, it, path + [i], ignore_missing) if sub is not None: item[i] = sub elif isinstance(item, dict): replace_keys = [] for k, v in item.items(): if path or k != CONF_SUBSTITUTIONS: - sub = _substitute_item(substitutions, k, path + [k]) + sub = _substitute_item(substitutions, k, path + [k], ignore_missing) if sub is not None: replace_keys.append((k, sub)) - sub = _substitute_item(substitutions, v, path + [k]) + sub = _substitute_item(substitutions, v, path + [k], ignore_missing) if sub is not None: item[k] = sub for old, new in replace_keys: item[new] = merge_config(item.get(old), item.get(new)) del item[old] elif isinstance(item, str): - sub = _expand_substitutions(substitutions, item, path) + sub = _expand_substitutions(substitutions, item, path, ignore_missing) if sub != item: return sub elif isinstance(item, core.Lambda): - sub = _expand_substitutions(substitutions, item.value, path) + sub = _expand_substitutions(substitutions, item.value, path, ignore_missing) if sub != item: item.value = sub return None -def do_substitution_pass(config, command_line_substitutions): +def do_substitution_pass(config, command_line_substitutions, ignore_missing=False): if CONF_SUBSTITUTIONS not in config and not command_line_substitutions: return @@ -151,4 +152,4 @@ def do_substitution_pass(config, command_line_substitutions): config[CONF_SUBSTITUTIONS] = substitutions # Move substitutions to the first place to replace substitutions in them correctly config.move_to_end(CONF_SUBSTITUTIONS, False) - _substitute_item(substitutions, config, []) + _substitute_item(substitutions, config, [], ignore_missing) diff --git a/esphome/yaml_util.py b/esphome/yaml_util.py index 57009be57e..75aec0edc8 100644 --- a/esphome/yaml_util.py +++ b/esphome/yaml_util.py @@ -251,7 +251,49 @@ class ESPHomeLoader(yaml.SafeLoader): # pylint: disable=too-many-ancestors @_add_data_ref def construct_include(self, node): - return _load_yaml_internal(self._rel_path(node.value)) + def extract_file_vars(node): + fields = self.construct_yaml_map(node) + file = fields.get("file") + if file is None: + raise yaml.MarkedYAMLError("Must include 'file'", node.start_mark) + vars = fields.get("vars") + if vars: + vars = {k: str(v) for k, v in vars.items()} + return file, vars + + def substitute_vars(config, vars): + from esphome.const import CONF_SUBSTITUTIONS + from esphome.components import substitutions + + org_subs = None + result = config + if not isinstance(config, dict): + # when the included yaml contains a list or a scalar + # wrap it into an OrderedDict because do_substitution_pass expects it + result = OrderedDict([("yaml", config)]) + elif CONF_SUBSTITUTIONS in result: + org_subs = result.pop(CONF_SUBSTITUTIONS) + + result[CONF_SUBSTITUTIONS] = vars + # Ignore missing vars that refer to the top level substitutions + substitutions.do_substitution_pass(result, None, ignore_missing=True) + result.pop(CONF_SUBSTITUTIONS) + + if not isinstance(config, dict): + result = result["yaml"] # unwrap the result + elif org_subs: + result[CONF_SUBSTITUTIONS] = org_subs + return result + + if isinstance(node, yaml.nodes.MappingNode): + file, vars = extract_file_vars(node) + else: + file, vars = node.value, None + + result = _load_yaml_internal(self._rel_path(file)) + if vars: + result = substitute_vars(result, vars) + return result @_add_data_ref def construct_include_dir_list(self, node): diff --git a/tests/unit_tests/fixtures/yaml_util/includes/included.yaml b/tests/unit_tests/fixtures/yaml_util/includes/included.yaml new file mode 100644 index 0000000000..e9fca324a3 --- /dev/null +++ b/tests/unit_tests/fixtures/yaml_util/includes/included.yaml @@ -0,0 +1,2 @@ +--- +ssid: ${name} diff --git a/tests/unit_tests/fixtures/yaml_util/includes/list.yaml b/tests/unit_tests/fixtures/yaml_util/includes/list.yaml new file mode 100644 index 0000000000..2fb3838631 --- /dev/null +++ b/tests/unit_tests/fixtures/yaml_util/includes/list.yaml @@ -0,0 +1,2 @@ +--- +- ${var1} diff --git a/tests/unit_tests/fixtures/yaml_util/includes/scalar.yaml b/tests/unit_tests/fixtures/yaml_util/includes/scalar.yaml new file mode 100644 index 0000000000..ddd2156b5e --- /dev/null +++ b/tests/unit_tests/fixtures/yaml_util/includes/scalar.yaml @@ -0,0 +1 @@ +${var1} diff --git a/tests/unit_tests/fixtures/yaml_util/includetest.yaml b/tests/unit_tests/fixtures/yaml_util/includetest.yaml new file mode 100644 index 0000000000..959283df60 --- /dev/null +++ b/tests/unit_tests/fixtures/yaml_util/includetest.yaml @@ -0,0 +1,17 @@ +--- +substitutions: + name: original + +wifi: !include + file: includes/included.yaml + vars: + name: my_custom_ssid + +esphome: + # should be substituted as 'original', not overwritten by vars in the !include above + name: ${name} + name_add_mac_suffix: true + platform: esp8266 + board: !include { file: includes/scalar.yaml, vars: { var1: nodemcu } } + + libraries: !include { file: includes/list.yaml, vars: { var1: Wire } } diff --git a/tests/unit_tests/test_yaml_util.py b/tests/unit_tests/test_yaml_util.py new file mode 100644 index 0000000000..8ee991f5b3 --- /dev/null +++ b/tests/unit_tests/test_yaml_util.py @@ -0,0 +1,13 @@ +from esphome import yaml_util +from esphome.components import substitutions + + +def test_include_with_vars(fixture_path): + yaml_file = fixture_path / "yaml_util" / "includetest.yaml" + + actual = yaml_util.load_yaml(yaml_file) + substitutions.do_substitution_pass(actual, None) + assert actual["esphome"]["name"] == "original" + assert actual["esphome"]["libraries"][0] == "Wire" + assert actual["esphome"]["board"] == "nodemcu" + assert actual["wifi"]["ssid"] == "my_custom_ssid" From a922efeafa9c46bd64fb9de7e88b05ea92de3d2c Mon Sep 17 00:00:00 2001 From: Wolfgang Tremmel Date: Tue, 31 May 2022 06:49:18 +0200 Subject: [PATCH 34/60] Change rain intensity sensor string (#3511) --- esphome/components/hydreon_rgxx/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/hydreon_rgxx/sensor.py b/esphome/components/hydreon_rgxx/sensor.py index 409500305a..c604f8d3c1 100644 --- a/esphome/components/hydreon_rgxx/sensor.py +++ b/esphome/components/hydreon_rgxx/sensor.py @@ -37,7 +37,7 @@ SUPPORTED_SENSORS = { PROTOCOL_NAMES = { CONF_MOISTURE: "R", CONF_ACC: "Acc", - CONF_R_INT: "Rint", + CONF_R_INT: "RInt", CONF_EVENT_ACC: "EventAcc", CONF_TOTAL_ACC: "TotalAcc", } From 6221f6d47d9522e62c1894bccb83c8cbc2bbb7f0 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 2 Jun 2022 17:00:17 +1200 Subject: [PATCH 35/60] Implement Media Player and I2S Media player (#3487) --- CODEOWNERS | 2 + esphome/components/api/api.proto | 62 +++- esphome/components/api/api_connection.cpp | 46 +++ esphome/components/api/api_connection.h | 5 + esphome/components/api/api_pb2.cpp | 278 ++++++++++++++++++ esphome/components/api/api_pb2.h | 67 +++++ esphome/components/api/api_pb2_service.cpp | 42 +++ esphome/components/api/api_pb2_service.h | 15 + esphome/components/api/api_server.cpp | 9 + esphome/components/api/api_server.h | 3 + esphome/components/api/list_entities.cpp | 6 + esphome/components/api/list_entities.h | 3 + esphome/components/api/subscribe_state.cpp | 5 + esphome/components/api/subscribe_state.h | 3 + esphome/components/i2s_audio/__init__.py | 0 .../i2s_audio/i2s_audio_media_player.cpp | 132 +++++++++ .../i2s_audio/i2s_audio_media_player.h | 63 ++++ esphome/components/i2s_audio/media_player.py | 94 ++++++ esphome/components/media_player/__init__.py | 64 ++++ .../components/media_player/media_player.cpp | 115 ++++++++ .../components/media_player/media_player.h | 91 ++++++ esphome/core/application.h | 19 ++ esphome/core/component_iterator.cpp | 18 ++ esphome/core/component_iterator.h | 6 + esphome/core/controller.cpp | 6 + esphome/core/controller.h | 6 + esphome/core/defines.h | 1 + platformio.ini | 2 + tests/test4.yaml | 9 + 29 files changed, 1170 insertions(+), 2 deletions(-) create mode 100644 esphome/components/i2s_audio/__init__.py create mode 100644 esphome/components/i2s_audio/i2s_audio_media_player.cpp create mode 100644 esphome/components/i2s_audio/i2s_audio_media_player.h create mode 100644 esphome/components/i2s_audio/media_player.py create mode 100644 esphome/components/media_player/__init__.py create mode 100644 esphome/components/media_player/media_player.cpp create mode 100644 esphome/components/media_player/media_player.h diff --git a/CODEOWNERS b/CODEOWNERS index 3e82a372ce..d5ce5e6920 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -88,6 +88,7 @@ esphome/components/honeywellabp/* @RubyBailey esphome/components/hrxl_maxsonar_wr/* @netmikey esphome/components/hydreon_rgxx/* @functionpointer esphome/components/i2c/* @esphome/core +esphome/components/i2s_audio/* @jesserockz esphome/components/improv_serial/* @esphome/core esphome/components/ina260/* @MrEditor97 esphome/components/inkbird_ibsth1_mini/* @fkirill @@ -119,6 +120,7 @@ esphome/components/mcp47a1/* @jesserockz esphome/components/mcp9808/* @k7hpn esphome/components/md5/* @esphome/core esphome/components/mdns/* @esphome/core +esphome/components/media_player/* @jesserockz esphome/components/midea/* @dudanov esphome/components/midea_ir/* @dudanov esphome/components/mitsubishi/* @RubyBailey diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index bd39893825..3e9a62f3d8 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -42,6 +42,7 @@ service APIConnection { rpc select_command (SelectCommandRequest) returns (void) {} rpc button_command (ButtonCommandRequest) returns (void) {} rpc lock_command (LockCommandRequest) returns (void) {} + rpc media_player_command (MediaPlayerCommandRequest) returns (void) {} } @@ -991,7 +992,7 @@ message ListEntitiesLockResponse { bool supports_open = 9; bool requires_code = 10; - # Not yet implemented: + // Not yet implemented: string code_format = 11; } message LockStateResponse { @@ -1010,7 +1011,7 @@ message LockCommandRequest { fixed32 key = 1; LockCommand command = 2; - # Not yet implemented: + // Not yet implemented: bool has_code = 3; string code = 4; } @@ -1040,3 +1041,60 @@ message ButtonCommandRequest { fixed32 key = 1; } +// ==================== MEDIA PLAYER ==================== +enum MediaPlayerState { + MEDIA_PLAYER_STATE_NONE = 0; + MEDIA_PLAYER_STATE_IDLE = 1; + MEDIA_PLAYER_STATE_PLAYING = 2; + MEDIA_PLAYER_STATE_PAUSED = 3; +} +enum MediaPlayerCommand { + MEDIA_PLAYER_COMMAND_PLAY = 0; + MEDIA_PLAYER_COMMAND_PAUSE = 1; + MEDIA_PLAYER_COMMAND_STOP = 2; + MEDIA_PLAYER_COMMAND_MUTE = 3; + MEDIA_PLAYER_COMMAND_UNMUTE = 4; +} +message ListEntitiesMediaPlayerResponse { + option (id) = 63; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_MEDIA_PLAYER"; + + string object_id = 1; + fixed32 key = 2; + string name = 3; + string unique_id = 4; + + string icon = 5; + bool disabled_by_default = 6; + EntityCategory entity_category = 7; + + bool supports_pause = 8; +} +message MediaPlayerStateResponse { + option (id) = 64; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_MEDIA_PLAYER"; + option (no_delay) = true; + fixed32 key = 1; + MediaPlayerState state = 2; + float volume = 3; + bool muted = 4; +} +message MediaPlayerCommandRequest { + option (id) = 65; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_MEDIA_PLAYER"; + option (no_delay) = true; + + fixed32 key = 1; + + bool has_command = 2; + MediaPlayerCommand command = 3; + + bool has_volume = 4; + float volume = 5; + + bool has_media_url = 6; + string media_url = 7; +} diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 4f399d95d0..9028034c90 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -733,6 +733,52 @@ void APIConnection::lock_command(const LockCommandRequest &msg) { } #endif +#ifdef USE_MEDIA_PLAYER +bool APIConnection::send_media_player_state(media_player::MediaPlayer *media_player) { + if (!this->state_subscription_) + return false; + + MediaPlayerStateResponse resp{}; + resp.key = media_player->get_object_id_hash(); + resp.state = static_cast(media_player->state); + resp.volume = media_player->volume; + resp.muted = media_player->is_muted(); + return this->send_media_player_state_response(resp); +} +bool APIConnection::send_media_player_info(media_player::MediaPlayer *media_player) { + ListEntitiesMediaPlayerResponse msg; + msg.key = media_player->get_object_id_hash(); + msg.object_id = media_player->get_object_id(); + msg.name = media_player->get_name(); + msg.unique_id = get_default_unique_id("media_player", media_player); + msg.icon = media_player->get_icon(); + msg.disabled_by_default = media_player->is_disabled_by_default(); + msg.entity_category = static_cast(media_player->get_entity_category()); + + auto traits = media_player->get_traits(); + msg.supports_pause = traits.get_supports_pause(); + + return this->send_list_entities_media_player_response(msg); +} +void APIConnection::media_player_command(const MediaPlayerCommandRequest &msg) { + media_player::MediaPlayer *media_player = App.get_media_player_by_key(msg.key); + if (media_player == nullptr) + return; + + auto call = media_player->make_call(); + if (msg.has_command) { + call.set_command(static_cast(msg.command)); + } + if (msg.has_volume) { + call.set_volume(msg.volume); + } + if (msg.has_media_url) { + call.set_media_url(msg.media_url); + } + call.perform(); +} +#endif + #ifdef USE_ESP32_CAMERA void APIConnection::send_camera_state(std::shared_ptr image) { if (!this->state_subscription_) diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 10f0becc54..0787d2f7eb 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -82,6 +82,11 @@ class APIConnection : public APIServerConnection { bool send_lock_state(lock::Lock *a_lock, lock::LockState state); bool send_lock_info(lock::Lock *a_lock); void lock_command(const LockCommandRequest &msg) override; +#endif +#ifdef USE_MEDIA_PLAYER + bool send_media_player_state(media_player::MediaPlayer *media_player); + bool send_media_player_info(media_player::MediaPlayer *media_player); + void media_player_command(const MediaPlayerCommandRequest &msg) override; #endif bool send_log_message(int level, const char *tag, const char *line); void send_homeassistant_service_call(const HomeassistantServiceResponse &call) { diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 5a78587473..70f909c07a 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -308,6 +308,36 @@ template<> const char *proto_enum_to_string(enums::LockComma return "UNKNOWN"; } } +template<> const char *proto_enum_to_string(enums::MediaPlayerState value) { + switch (value) { + case enums::MEDIA_PLAYER_STATE_NONE: + return "MEDIA_PLAYER_STATE_NONE"; + case enums::MEDIA_PLAYER_STATE_IDLE: + return "MEDIA_PLAYER_STATE_IDLE"; + case enums::MEDIA_PLAYER_STATE_PLAYING: + return "MEDIA_PLAYER_STATE_PLAYING"; + case enums::MEDIA_PLAYER_STATE_PAUSED: + return "MEDIA_PLAYER_STATE_PAUSED"; + default: + return "UNKNOWN"; + } +} +template<> const char *proto_enum_to_string(enums::MediaPlayerCommand value) { + switch (value) { + case enums::MEDIA_PLAYER_COMMAND_PLAY: + return "MEDIA_PLAYER_COMMAND_PLAY"; + case enums::MEDIA_PLAYER_COMMAND_PAUSE: + return "MEDIA_PLAYER_COMMAND_PAUSE"; + case enums::MEDIA_PLAYER_COMMAND_STOP: + return "MEDIA_PLAYER_COMMAND_STOP"; + case enums::MEDIA_PLAYER_COMMAND_MUTE: + return "MEDIA_PLAYER_COMMAND_MUTE"; + case enums::MEDIA_PLAYER_COMMAND_UNMUTE: + return "MEDIA_PLAYER_COMMAND_UNMUTE"; + default: + return "UNKNOWN"; + } +} bool HelloRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { case 1: { @@ -4574,6 +4604,254 @@ void ButtonCommandRequest::dump_to(std::string &out) const { out.append("}"); } #endif +bool ListEntitiesMediaPlayerResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 6: { + this->disabled_by_default = value.as_bool(); + return true; + } + case 7: { + this->entity_category = value.as_enum(); + return true; + } + case 8: { + this->supports_pause = value.as_bool(); + return true; + } + default: + return false; + } +} +bool ListEntitiesMediaPlayerResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 1: { + this->object_id = value.as_string(); + return true; + } + case 3: { + this->name = value.as_string(); + return true; + } + case 4: { + this->unique_id = value.as_string(); + return true; + } + case 5: { + this->icon = value.as_string(); + return true; + } + default: + return false; + } +} +bool ListEntitiesMediaPlayerResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 2: { + this->key = value.as_fixed32(); + return true; + } + default: + return false; + } +} +void ListEntitiesMediaPlayerResponse::encode(ProtoWriteBuffer buffer) const { + buffer.encode_string(1, this->object_id); + buffer.encode_fixed32(2, this->key); + buffer.encode_string(3, this->name); + buffer.encode_string(4, this->unique_id); + buffer.encode_string(5, this->icon); + buffer.encode_bool(6, this->disabled_by_default); + buffer.encode_enum(7, this->entity_category); + buffer.encode_bool(8, this->supports_pause); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void ListEntitiesMediaPlayerResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("ListEntitiesMediaPlayerResponse {\n"); + out.append(" object_id: "); + out.append("'").append(this->object_id).append("'"); + out.append("\n"); + + out.append(" key: "); + sprintf(buffer, "%u", this->key); + out.append(buffer); + out.append("\n"); + + out.append(" name: "); + out.append("'").append(this->name).append("'"); + out.append("\n"); + + out.append(" unique_id: "); + out.append("'").append(this->unique_id).append("'"); + out.append("\n"); + + out.append(" icon: "); + out.append("'").append(this->icon).append("'"); + out.append("\n"); + + out.append(" disabled_by_default: "); + out.append(YESNO(this->disabled_by_default)); + out.append("\n"); + + out.append(" entity_category: "); + out.append(proto_enum_to_string(this->entity_category)); + out.append("\n"); + + out.append(" supports_pause: "); + out.append(YESNO(this->supports_pause)); + out.append("\n"); + out.append("}"); +} +#endif +bool MediaPlayerStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 2: { + this->state = value.as_enum(); + return true; + } + case 4: { + this->muted = value.as_bool(); + return true; + } + default: + return false; + } +} +bool MediaPlayerStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 1: { + this->key = value.as_fixed32(); + return true; + } + case 3: { + this->volume = value.as_float(); + return true; + } + default: + return false; + } +} +void MediaPlayerStateResponse::encode(ProtoWriteBuffer buffer) const { + buffer.encode_fixed32(1, this->key); + buffer.encode_enum(2, this->state); + buffer.encode_float(3, this->volume); + buffer.encode_bool(4, this->muted); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void MediaPlayerStateResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("MediaPlayerStateResponse {\n"); + out.append(" key: "); + sprintf(buffer, "%u", this->key); + out.append(buffer); + out.append("\n"); + + out.append(" state: "); + out.append(proto_enum_to_string(this->state)); + out.append("\n"); + + out.append(" volume: "); + sprintf(buffer, "%g", this->volume); + out.append(buffer); + out.append("\n"); + + out.append(" muted: "); + out.append(YESNO(this->muted)); + out.append("\n"); + out.append("}"); +} +#endif +bool MediaPlayerCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 2: { + this->has_command = value.as_bool(); + return true; + } + case 3: { + this->command = value.as_enum(); + return true; + } + case 4: { + this->has_volume = value.as_bool(); + return true; + } + case 6: { + this->has_media_url = value.as_bool(); + return true; + } + default: + return false; + } +} +bool MediaPlayerCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 7: { + this->media_url = value.as_string(); + return true; + } + default: + return false; + } +} +bool MediaPlayerCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 1: { + this->key = value.as_fixed32(); + return true; + } + case 5: { + this->volume = value.as_float(); + return true; + } + default: + return false; + } +} +void MediaPlayerCommandRequest::encode(ProtoWriteBuffer buffer) const { + buffer.encode_fixed32(1, this->key); + buffer.encode_bool(2, this->has_command); + buffer.encode_enum(3, this->command); + buffer.encode_bool(4, this->has_volume); + buffer.encode_float(5, this->volume); + buffer.encode_bool(6, this->has_media_url); + buffer.encode_string(7, this->media_url); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void MediaPlayerCommandRequest::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("MediaPlayerCommandRequest {\n"); + out.append(" key: "); + sprintf(buffer, "%u", this->key); + out.append(buffer); + out.append("\n"); + + out.append(" has_command: "); + out.append(YESNO(this->has_command)); + out.append("\n"); + + out.append(" command: "); + out.append(proto_enum_to_string(this->command)); + out.append("\n"); + + out.append(" has_volume: "); + out.append(YESNO(this->has_volume)); + out.append("\n"); + + out.append(" volume: "); + sprintf(buffer, "%g", this->volume); + out.append(buffer); + out.append("\n"); + + out.append(" has_media_url: "); + out.append(YESNO(this->has_media_url)); + out.append("\n"); + + out.append(" media_url: "); + out.append("'").append(this->media_url).append("'"); + out.append("\n"); + out.append("}"); +} +#endif } // namespace api } // namespace esphome diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 28c0a7ce88..ec1cdc35ac 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -141,6 +141,19 @@ enum LockCommand : uint32_t { LOCK_LOCK = 1, LOCK_OPEN = 2, }; +enum MediaPlayerState : uint32_t { + MEDIA_PLAYER_STATE_NONE = 0, + MEDIA_PLAYER_STATE_IDLE = 1, + MEDIA_PLAYER_STATE_PLAYING = 2, + MEDIA_PLAYER_STATE_PAUSED = 3, +}; +enum MediaPlayerCommand : uint32_t { + MEDIA_PLAYER_COMMAND_PLAY = 0, + MEDIA_PLAYER_COMMAND_PAUSE = 1, + MEDIA_PLAYER_COMMAND_STOP = 2, + MEDIA_PLAYER_COMMAND_MUTE = 3, + MEDIA_PLAYER_COMMAND_UNMUTE = 4, +}; } // namespace enums @@ -1146,6 +1159,60 @@ class ButtonCommandRequest : public ProtoMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; }; +class ListEntitiesMediaPlayerResponse : public ProtoMessage { + public: + std::string object_id{}; + uint32_t key{0}; + std::string name{}; + std::string unique_id{}; + std::string icon{}; + bool disabled_by_default{false}; + enums::EntityCategory entity_category{}; + bool supports_pause{false}; + void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP + void dump_to(std::string &out) const override; +#endif + + 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 MediaPlayerStateResponse : public ProtoMessage { + public: + uint32_t key{0}; + enums::MediaPlayerState state{}; + float volume{0.0f}; + bool muted{false}; + void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP + void dump_to(std::string &out) const override; +#endif + + protected: + bool decode_32bit(uint32_t field_id, Proto32Bit value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class MediaPlayerCommandRequest : public ProtoMessage { + public: + uint32_t key{0}; + bool has_command{false}; + enums::MediaPlayerCommand command{}; + bool has_volume{false}; + float volume{0.0f}; + bool has_media_url{false}; + std::string media_url{}; + void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP + void dump_to(std::string &out) const override; +#endif + + 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; +}; } // namespace api } // namespace esphome diff --git a/esphome/components/api/api_pb2_service.cpp b/esphome/components/api/api_pb2_service.cpp index d981a3bf4e..bd146cb54d 100644 --- a/esphome/components/api/api_pb2_service.cpp +++ b/esphome/components/api/api_pb2_service.cpp @@ -310,6 +310,24 @@ bool APIServerConnectionBase::send_list_entities_button_response(const ListEntit #endif #ifdef USE_BUTTON #endif +#ifdef USE_MEDIA_PLAYER +bool APIServerConnectionBase::send_list_entities_media_player_response(const ListEntitiesMediaPlayerResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "send_list_entities_media_player_response: %s", msg.dump().c_str()); +#endif + return this->send_message_(msg, 63); +} +#endif +#ifdef USE_MEDIA_PLAYER +bool APIServerConnectionBase::send_media_player_state_response(const MediaPlayerStateResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "send_media_player_state_response: %s", msg.dump().c_str()); +#endif + return this->send_message_(msg, 64); +} +#endif +#ifdef USE_MEDIA_PLAYER +#endif bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) { switch (msg_type) { case 1: { @@ -563,6 +581,17 @@ bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, ESP_LOGVV(TAG, "on_button_command_request: %s", msg.dump().c_str()); #endif this->on_button_command_request(msg); +#endif + break; + } + case 65: { +#ifdef USE_MEDIA_PLAYER + MediaPlayerCommandRequest msg; + msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "on_media_player_command_request: %s", msg.dump().c_str()); +#endif + this->on_media_player_command_request(msg); #endif break; } @@ -813,6 +842,19 @@ void APIServerConnection::on_lock_command_request(const LockCommandRequest &msg) this->lock_command(msg); } #endif +#ifdef USE_MEDIA_PLAYER +void APIServerConnection::on_media_player_command_request(const MediaPlayerCommandRequest &msg) { + if (!this->is_connection_setup()) { + this->on_no_setup_connection(); + return; + } + if (!this->is_authenticated()) { + this->on_unauthenticated_access(); + return; + } + this->media_player_command(msg); +} +#endif } // namespace api } // namespace esphome diff --git a/esphome/components/api/api_pb2_service.h b/esphome/components/api/api_pb2_service.h index 5aaf831c91..28ad3fbd15 100644 --- a/esphome/components/api/api_pb2_service.h +++ b/esphome/components/api/api_pb2_service.h @@ -144,6 +144,15 @@ class APIServerConnectionBase : public ProtoService { #endif #ifdef USE_BUTTON virtual void on_button_command_request(const ButtonCommandRequest &value){}; +#endif +#ifdef USE_MEDIA_PLAYER + bool send_list_entities_media_player_response(const ListEntitiesMediaPlayerResponse &msg); +#endif +#ifdef USE_MEDIA_PLAYER + bool send_media_player_state_response(const MediaPlayerStateResponse &msg); +#endif +#ifdef USE_MEDIA_PLAYER + virtual void on_media_player_command_request(const MediaPlayerCommandRequest &value){}; #endif protected: bool read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override; @@ -192,6 +201,9 @@ class APIServerConnection : public APIServerConnectionBase { #endif #ifdef USE_LOCK virtual void lock_command(const LockCommandRequest &msg) = 0; +#endif +#ifdef USE_MEDIA_PLAYER + virtual void media_player_command(const MediaPlayerCommandRequest &msg) = 0; #endif protected: void on_hello_request(const HelloRequest &msg) override; @@ -236,6 +248,9 @@ class APIServerConnection : public APIServerConnectionBase { #ifdef USE_LOCK void on_lock_command_request(const LockCommandRequest &msg) override; #endif +#ifdef USE_MEDIA_PLAYER + void on_media_player_command_request(const MediaPlayerCommandRequest &msg) override; +#endif }; } // namespace api diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 1f2800f298..8375a82313 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -272,6 +272,15 @@ void APIServer::on_lock_update(lock::Lock *obj) { } #endif +#ifdef USE_MEDIA_PLAYER +void APIServer::on_media_player_update(media_player::MediaPlayer *obj) { + if (obj->is_internal()) + return; + for (auto &c : this->clients_) + c->send_media_player_state(obj); +} +#endif + float APIServer::get_setup_priority() const { return setup_priority::AFTER_WIFI; } void APIServer::set_port(uint16_t port) { this->port_ = port; } APIServer *global_api_server = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index f03a83fc7b..6997e23cac 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -68,6 +68,9 @@ class APIServer : public Component, public Controller { #endif #ifdef USE_LOCK void on_lock_update(lock::Lock *obj) override; +#endif +#ifdef USE_MEDIA_PLAYER + void on_media_player_update(media_player::MediaPlayer *obj) override; #endif void send_homeassistant_service_call(const HomeassistantServiceResponse &call); void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); } diff --git a/esphome/components/api/list_entities.cpp b/esphome/components/api/list_entities.cpp index 9f55fda617..85d4cd61ef 100644 --- a/esphome/components/api/list_entities.cpp +++ b/esphome/components/api/list_entities.cpp @@ -64,5 +64,11 @@ bool ListEntitiesIterator::on_number(number::Number *number) { return this->clie bool ListEntitiesIterator::on_select(select::Select *select) { return this->client_->send_select_info(select); } #endif +#ifdef USE_MEDIA_PLAYER +bool ListEntitiesIterator::on_media_player(media_player::MediaPlayer *media_player) { + return this->client_->send_media_player_info(media_player); +} +#endif + } // namespace api } // namespace esphome diff --git a/esphome/components/api/list_entities.h b/esphome/components/api/list_entities.h index 51c343eb03..4fbaa509a2 100644 --- a/esphome/components/api/list_entities.h +++ b/esphome/components/api/list_entities.h @@ -51,6 +51,9 @@ class ListEntitiesIterator : public ComponentIterator { #endif #ifdef USE_LOCK bool on_lock(lock::Lock *a_lock) override; +#endif +#ifdef USE_MEDIA_PLAYER + bool on_media_player(media_player::MediaPlayer *media_player) override; #endif bool on_end() override; diff --git a/esphome/components/api/subscribe_state.cpp b/esphome/components/api/subscribe_state.cpp index ba277502c8..1d1ba0245e 100644 --- a/esphome/components/api/subscribe_state.cpp +++ b/esphome/components/api/subscribe_state.cpp @@ -50,6 +50,11 @@ bool InitialStateIterator::on_select(select::Select *select) { #ifdef USE_LOCK bool InitialStateIterator::on_lock(lock::Lock *a_lock) { return this->client_->send_lock_state(a_lock, a_lock->state); } #endif +#ifdef USE_MEDIA_PLAYER +bool InitialStateIterator::on_media_player(media_player::MediaPlayer *media_player) { + return this->client_->send_media_player_state(media_player); +} +#endif InitialStateIterator::InitialStateIterator(APIConnection *client) : client_(client) {} } // namespace api diff --git a/esphome/components/api/subscribe_state.h b/esphome/components/api/subscribe_state.h index 515e1a2d07..7a7ba697c0 100644 --- a/esphome/components/api/subscribe_state.h +++ b/esphome/components/api/subscribe_state.h @@ -48,6 +48,9 @@ class InitialStateIterator : public ComponentIterator { #endif #ifdef USE_LOCK bool on_lock(lock::Lock *a_lock) override; +#endif +#ifdef USE_MEDIA_PLAYER + bool on_media_player(media_player::MediaPlayer *media_player) override; #endif protected: APIConnection *client_; diff --git a/esphome/components/i2s_audio/__init__.py b/esphome/components/i2s_audio/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/i2s_audio/i2s_audio_media_player.cpp b/esphome/components/i2s_audio/i2s_audio_media_player.cpp new file mode 100644 index 0000000000..0ab3237aeb --- /dev/null +++ b/esphome/components/i2s_audio/i2s_audio_media_player.cpp @@ -0,0 +1,132 @@ +#include "i2s_audio_media_player.h" + +#ifdef USE_ESP32_FRAMEWORK_ARDUINO + +#include "esphome/core/log.h" + +namespace esphome { +namespace i2s_audio { + +static const char *const TAG = "audio"; + +void I2SAudioMediaPlayer::control(const media_player::MediaPlayerCall &call) { + if (call.get_media_url().has_value()) { + if (this->audio_->isRunning()) + this->audio_->stopSong(); + this->high_freq_.start(); + this->audio_->connecttohost(call.get_media_url().value().c_str()); + this->state = media_player::MEDIA_PLAYER_STATE_PLAYING; + } + if (call.get_volume().has_value()) { + this->volume = call.get_volume().value(); + this->set_volume_(volume); + this->unmute_(); + } + if (call.get_command().has_value()) { + switch (call.get_command().value()) { + case media_player::MEDIA_PLAYER_COMMAND_PLAY: + if (!this->audio_->isRunning()) + this->audio_->pauseResume(); + this->state = media_player::MEDIA_PLAYER_STATE_PLAYING; + break; + case media_player::MEDIA_PLAYER_COMMAND_PAUSE: + if (this->audio_->isRunning()) + this->audio_->pauseResume(); + this->state = media_player::MEDIA_PLAYER_STATE_PAUSED; + break; + case media_player::MEDIA_PLAYER_COMMAND_STOP: + this->stop_(); + break; + case media_player::MEDIA_PLAYER_COMMAND_MUTE: + this->mute_(); + break; + case media_player::MEDIA_PLAYER_COMMAND_UNMUTE: + this->unmute_(); + break; + } + } + this->publish_state(); +} + +void I2SAudioMediaPlayer::mute_() { + if (this->mute_pin_ != nullptr) { + this->mute_pin_->digital_write(true); + } else { + this->set_volume_(0.0f, false); + } + this->muted_ = true; +} +void I2SAudioMediaPlayer::unmute_() { + if (this->mute_pin_ != nullptr) { + this->mute_pin_->digital_write(false); + } else { + this->set_volume_(this->volume, false); + } + this->muted_ = false; +} +void I2SAudioMediaPlayer::set_volume_(float volume, bool publish) { + this->audio_->setVolume(remap(volume, 0.0f, 1.0f, 0, 21)); + if (publish) + this->volume = volume; +} + +void I2SAudioMediaPlayer::stop_() { + if (this->audio_->isRunning()) + this->audio_->stopSong(); + this->high_freq_.stop(); + this->state = media_player::MEDIA_PLAYER_STATE_IDLE; +} + +void I2SAudioMediaPlayer::setup() { + ESP_LOGCONFIG(TAG, "Setting up Audio..."); + if (this->internal_dac_mode_ != I2S_DAC_CHANNEL_DISABLE) { + this->audio_ = make_unique