From b778eed4193d14c261e4faee497077454fca987e Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 13 Apr 2022 13:42:28 +1200 Subject: [PATCH 01/56] Bump version to 2022.5.0-dev --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 9096e66f4e..fa5baf4fe2 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2022.4.0-dev" +__version__ = "2022.5.0-dev" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From b4a86ce6cfc110d71dfdcb25ea02a69d96c8c02e Mon Sep 17 00:00:00 2001 From: matthias882 <30553262+matthias882@users.noreply.github.com> Date: Wed, 13 Apr 2022 23:36:16 +0200 Subject: [PATCH 02/56] Changes accuracy of single cell voltage (#3387) --- esphome/components/daly_bms/sensor.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/daly_bms/sensor.py b/esphome/components/daly_bms/sensor.py index e2e8528317..2274a2153a 100644 --- a/esphome/components/daly_bms/sensor.py +++ b/esphome/components/daly_bms/sensor.py @@ -98,6 +98,8 @@ CELL_VOLTAGE_SCHEMA = sensor.sensor_schema( unit_of_measurement=UNIT_VOLT, device_class=DEVICE_CLASS_VOLTAGE, state_class=STATE_CLASS_MEASUREMENT, + icon=ICON_FLASH, + accuracy_decimals=3, ) CONFIG_SCHEMA = cv.All( From 047c18eac0ca65a6609c58d4fd419e389968031f Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 14 Apr 2022 11:25:31 +1200 Subject: [PATCH 03/56] Add default object_id_generator for mqtt (#3389) --- esphome/components/mqtt/mqtt_client.cpp | 7 ++++++- esphome/components/mqtt/mqtt_client.h | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index 3c6ce7cdfc..12a43dc232 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -556,7 +556,12 @@ void MQTTClientComponent::disable_last_will() { this->last_will_.topic = ""; } void MQTTClientComponent::disable_discovery() { this->discovery_info_ = MQTTDiscoveryInfo{ - .prefix = "", .retain = false, .clean = false, .unique_id_generator = MQTT_LEGACY_UNIQUE_ID_GENERATOR}; + .prefix = "", + .retain = false, + .clean = false, + .unique_id_generator = MQTT_LEGACY_UNIQUE_ID_GENERATOR, + .object_id_generator = MQTT_NONE_OBJECT_ID_GENERATOR, + }; } void MQTTClientComponent::on_shutdown() { if (!this->shutdown_message_.topic.empty()) { diff --git a/esphome/components/mqtt/mqtt_client.h b/esphome/components/mqtt/mqtt_client.h index 4880bbaa5b..20b174a66f 100644 --- a/esphome/components/mqtt/mqtt_client.h +++ b/esphome/components/mqtt/mqtt_client.h @@ -277,6 +277,7 @@ class MQTTClientComponent : public Component { .retain = true, .clean = false, .unique_id_generator = MQTT_LEGACY_UNIQUE_ID_GENERATOR, + .object_id_generator = MQTT_NONE_OBJECT_ID_GENERATOR, }; std::string topic_prefix_{}; MQTTMessage log_message_; From 70a35656e463252ac6bfad44da9e41d5894de3d8 Mon Sep 17 00:00:00 2001 From: rnauber <7414650+rnauber@users.noreply.github.com> Date: Thu, 14 Apr 2022 03:13:51 +0200 Subject: [PATCH 04/56] Add support for Shelly Dimmer 2 (#2954) Co-authored-by: Niclas Larsson Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Co-authored-by: Jernej Kos Co-authored-by: Richard Nauber --- CODEOWNERS | 1 + esphome/components/shelly_dimmer/LICENSE.txt | 2 + esphome/components/shelly_dimmer/__init__.py | 1 + esphome/components/shelly_dimmer/dev_table.h | 158 +++ esphome/components/shelly_dimmer/light.py | 219 ++++ .../shelly_dimmer/shelly_dimmer.cpp | 526 ++++++++ .../components/shelly_dimmer/shelly_dimmer.h | 117 ++ .../components/shelly_dimmer/stm32flash.cpp | 1061 +++++++++++++++++ esphome/components/shelly_dimmer/stm32flash.h | 129 ++ esphome/core/defines.h | 6 + tests/test1.yaml | 12 + 11 files changed, 2232 insertions(+) create mode 100644 esphome/components/shelly_dimmer/LICENSE.txt create mode 100644 esphome/components/shelly_dimmer/__init__.py create mode 100644 esphome/components/shelly_dimmer/dev_table.h create mode 100644 esphome/components/shelly_dimmer/light.py create mode 100644 esphome/components/shelly_dimmer/shelly_dimmer.cpp create mode 100644 esphome/components/shelly_dimmer/shelly_dimmer.h create mode 100644 esphome/components/shelly_dimmer/stm32flash.cpp create mode 100644 esphome/components/shelly_dimmer/stm32flash.h diff --git a/CODEOWNERS b/CODEOWNERS index 7595fc52e2..02945ec0a4 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -173,6 +173,7 @@ esphome/components/select/* @esphome/core esphome/components/sensirion_common/* @martgras esphome/components/sensor/* @esphome/core esphome/components/sgp40/* @SenexCrenshaw +esphome/components/shelly_dimmer/* @edge90 @rnauber esphome/components/sht4x/* @sjtrny esphome/components/shutdown/* @esphome/core @jsuanet esphome/components/sim800l/* @glmnet diff --git a/esphome/components/shelly_dimmer/LICENSE.txt b/esphome/components/shelly_dimmer/LICENSE.txt new file mode 100644 index 0000000000..524fe0d514 --- /dev/null +++ b/esphome/components/shelly_dimmer/LICENSE.txt @@ -0,0 +1,2 @@ +The firmware files for the STM microcontroller (shelly-dimmer-stm32_*.bin) are taken from +https://github.com/jamesturton/shelly-dimmer-stm32 and GPLv3 licensed. diff --git a/esphome/components/shelly_dimmer/__init__.py b/esphome/components/shelly_dimmer/__init__.py new file mode 100644 index 0000000000..accefbbc34 --- /dev/null +++ b/esphome/components/shelly_dimmer/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@rnauber", "@edge90"] diff --git a/esphome/components/shelly_dimmer/dev_table.h b/esphome/components/shelly_dimmer/dev_table.h new file mode 100644 index 0000000000..f4bf7778f2 --- /dev/null +++ b/esphome/components/shelly_dimmer/dev_table.h @@ -0,0 +1,158 @@ +/* + stm32flash - Open Source ST STM32 flash program for Arduino + Copyright (C) 2010 Geoffrey McRae + Copyright (C) 2014-2015 Antonio Borneo + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +*/ +#pragma once + +#include "esphome/core/defines.h" +#ifdef USE_SHD_FIRMWARE_DATA +#include "stm32flash.h" + +namespace esphome { +namespace shelly_dimmer { + +constexpr uint32_t SZ_128 = 0x00000080; +constexpr uint32_t SZ_256 = 0x00000100; +constexpr uint32_t SZ_1K = 0x00000400; +constexpr uint32_t SZ_2K = 0x00000800; +constexpr uint32_t SZ_16K = 0x00004000; +constexpr uint32_t SZ_32K = 0x00008000; +constexpr uint32_t SZ_64K = 0x00010000; +constexpr uint32_t SZ_128K = 0x00020000; +constexpr uint32_t SZ_256K = 0x00040000; + +/* + * Page-size for page-by-page flash erase. + * Arrays are zero terminated; last non-zero value is automatically repeated + */ + +/* fixed size pages */ +constexpr uint32_t p_128[] = {SZ_128, 0}; // NOLINT +constexpr uint32_t p_256[] = {SZ_256, 0}; // NOLINT +constexpr uint32_t p_1k[] = {SZ_1K, 0}; // NOLINT +constexpr uint32_t p_2k[] = {SZ_2K, 0}; // NOLINT +/* F2 and F4 page size */ +constexpr uint32_t f2f4[] = {SZ_16K, SZ_16K, SZ_16K, SZ_16K, SZ_64K, SZ_128K, 0}; // NOLINT +/* F4 dual bank page size */ +constexpr uint32_t f4db[] = {SZ_16K, SZ_16K, SZ_16K, SZ_16K, SZ_64K, SZ_128K, SZ_128K, // NOLINT + SZ_128K, SZ_16K, SZ_16K, SZ_16K, SZ_16K, SZ_64K, SZ_128K, 0}; +/* F7 page size */ +constexpr uint32_t f7[] = {SZ_32K, SZ_32K, SZ_32K, SZ_32K, SZ_128K, SZ_256K, 0}; // NOLINT + +/* + * Device table, corresponds to the "Bootloader device-dependant parameters" + * table in ST document AN2606. + * Note that the option bytes upper range is inclusive! + */ +constexpr stm32_dev_t DEVICES[] = { + /* ID "name" SRAM-address-range FLASH-address-range PPS PSize + Option-byte-addr-range System-mem-addr-range Flags */ + /* F0 */ + {0x440, "STM32F030x8/F05xxx", 0x20000800, 0x20002000, 0x08000000, 0x08010000, 4, p_1k, 0x1FFFF800, 0x1FFFF80F, + 0x1FFFEC00, 0x1FFFF800, 0}, + {0x442, "STM32F030xC/F09xxx", 0x20001800, 0x20008000, 0x08000000, 0x08040000, 2, p_2k, 0x1FFFF800, 0x1FFFF80F, + 0x1FFFC800, 0x1FFFF800, F_OBLL}, + {0x444, "STM32F03xx4/6", 0x20000800, 0x20001000, 0x08000000, 0x08008000, 4, p_1k, 0x1FFFF800, 0x1FFFF80F, + 0x1FFFEC00, 0x1FFFF800, 0}, + {0x445, "STM32F04xxx/F070x6", 0x20001800, 0x20001800, 0x08000000, 0x08008000, 4, p_1k, 0x1FFFF800, 0x1FFFF80F, + 0x1FFFC400, 0x1FFFF800, 0}, + {0x448, "STM32F070xB/F071xx/F72xx", 0x20001800, 0x20004000, 0x08000000, 0x08020000, 2, p_2k, 0x1FFFF800, 0x1FFFF80F, + 0x1FFFC800, 0x1FFFF800, 0}, + /* F1 */ + {0x412, "STM32F10xxx Low-density", 0x20000200, 0x20002800, 0x08000000, 0x08008000, 4, p_1k, 0x1FFFF800, 0x1FFFF80F, + 0x1FFFF000, 0x1FFFF800, 0}, + {0x410, "STM32F10xxx Medium-density", 0x20000200, 0x20005000, 0x08000000, 0x08020000, 4, p_1k, 0x1FFFF800, + 0x1FFFF80F, 0x1FFFF000, 0x1FFFF800, 0}, + {0x414, "STM32F10xxx High-density", 0x20000200, 0x20010000, 0x08000000, 0x08080000, 2, p_2k, 0x1FFFF800, 0x1FFFF80F, + 0x1FFFF000, 0x1FFFF800, 0}, + {0x420, "STM32F10xxx Medium-density VL", 0x20000200, 0x20002000, 0x08000000, 0x08020000, 4, p_1k, 0x1FFFF800, + 0x1FFFF80F, 0x1FFFF000, 0x1FFFF800, 0}, + {0x428, "STM32F10xxx High-density VL", 0x20000200, 0x20008000, 0x08000000, 0x08080000, 2, p_2k, 0x1FFFF800, + 0x1FFFF80F, 0x1FFFF000, 0x1FFFF800, 0}, + {0x418, "STM32F105xx/F107xx", 0x20001000, 0x20010000, 0x08000000, 0x08040000, 2, p_2k, 0x1FFFF800, 0x1FFFF80F, + 0x1FFFB000, 0x1FFFF800, 0}, + {0x430, "STM32F10xxx XL-density", 0x20000800, 0x20018000, 0x08000000, 0x08100000, 2, p_2k, 0x1FFFF800, 0x1FFFF80F, + 0x1FFFE000, 0x1FFFF800, 0}, + /* F2 */ + {0x411, "STM32F2xxxx", 0x20002000, 0x20020000, 0x08000000, 0x08100000, 1, f2f4, 0x1FFFC000, 0x1FFFC00F, 0x1FFF0000, + 0x1FFF7800, 0}, + /* F3 */ + {0x432, "STM32F373xx/F378xx", 0x20001400, 0x20008000, 0x08000000, 0x08040000, 2, p_2k, 0x1FFFF800, 0x1FFFF80F, + 0x1FFFD800, 0x1FFFF800, 0}, + {0x422, "STM32F302xB(C)/F303xB(C)/F358xx", 0x20001400, 0x2000A000, 0x08000000, 0x08040000, 2, p_2k, 0x1FFFF800, + 0x1FFFF80F, 0x1FFFD800, 0x1FFFF800, 0}, + {0x439, "STM32F301xx/F302x4(6/8)/F318xx", 0x20001800, 0x20004000, 0x08000000, 0x08010000, 2, p_2k, 0x1FFFF800, + 0x1FFFF80F, 0x1FFFD800, 0x1FFFF800, 0}, + {0x438, "STM32F303x4(6/8)/F334xx/F328xx", 0x20001800, 0x20003000, 0x08000000, 0x08010000, 2, p_2k, 0x1FFFF800, + 0x1FFFF80F, 0x1FFFD800, 0x1FFFF800, 0}, + {0x446, "STM32F302xD(E)/F303xD(E)/F398xx", 0x20001800, 0x20010000, 0x08000000, 0x08080000, 2, p_2k, 0x1FFFF800, + 0x1FFFF80F, 0x1FFFD800, 0x1FFFF800, 0}, + /* F4 */ + {0x413, "STM32F40xxx/41xxx", 0x20003000, 0x20020000, 0x08000000, 0x08100000, 1, f2f4, 0x1FFFC000, 0x1FFFC00F, + 0x1FFF0000, 0x1FFF7800, 0}, + {0x419, "STM32F42xxx/43xxx", 0x20003000, 0x20030000, 0x08000000, 0x08200000, 1, f4db, 0x1FFEC000, 0x1FFFC00F, + 0x1FFF0000, 0x1FFF7800, 0}, + {0x423, "STM32F401xB(C)", 0x20003000, 0x20010000, 0x08000000, 0x08040000, 1, f2f4, 0x1FFFC000, 0x1FFFC00F, + 0x1FFF0000, 0x1FFF7800, 0}, + {0x433, "STM32F401xD(E)", 0x20003000, 0x20018000, 0x08000000, 0x08080000, 1, f2f4, 0x1FFFC000, 0x1FFFC00F, + 0x1FFF0000, 0x1FFF7800, 0}, + {0x458, "STM32F410xx", 0x20003000, 0x20008000, 0x08000000, 0x08020000, 1, f2f4, 0x1FFFC000, 0x1FFFC00F, 0x1FFF0000, + 0x1FFF7800, 0}, + {0x431, "STM32F411xx", 0x20003000, 0x20020000, 0x08000000, 0x08080000, 1, f2f4, 0x1FFFC000, 0x1FFFC00F, 0x1FFF0000, + 0x1FFF7800, 0}, + {0x421, "STM32F446xx", 0x20003000, 0x20020000, 0x08000000, 0x08080000, 1, f2f4, 0x1FFFC000, 0x1FFFC00F, 0x1FFF0000, + 0x1FFF7800, 0}, + {0x434, "STM32F469xx", 0x20003000, 0x20060000, 0x08000000, 0x08200000, 1, f4db, 0x1FFEC000, 0x1FFFC00F, 0x1FFF0000, + 0x1FFF7800, 0}, + /* F7 */ + {0x449, "STM32F74xxx/75xxx", 0x20004000, 0x20050000, 0x08000000, 0x08100000, 1, f7, 0x1FFF0000, 0x1FFF001F, + 0x1FF00000, 0x1FF0EDC0, 0}, + /* L0 */ + {0x425, "STM32L031xx/041xx", 0x20001000, 0x20002000, 0x08000000, 0x08008000, 32, p_128, 0x1FF80000, 0x1FF8001F, + 0x1FF00000, 0x1FF01000, 0}, + {0x417, "STM32L05xxx/06xxx", 0x20001000, 0x20002000, 0x08000000, 0x08010000, 32, p_128, 0x1FF80000, 0x1FF8001F, + 0x1FF00000, 0x1FF01000, 0}, + {0x447, "STM32L07xxx/08xxx", 0x20002000, 0x20005000, 0x08000000, 0x08030000, 32, p_128, 0x1FF80000, 0x1FF8001F, + 0x1FF00000, 0x1FF02000, 0}, + /* L1 */ + {0x416, "STM32L1xxx6(8/B)", 0x20000800, 0x20004000, 0x08000000, 0x08020000, 16, p_256, 0x1FF80000, 0x1FF8001F, + 0x1FF00000, 0x1FF01000, F_NO_ME}, + {0x429, "STM32L1xxx6(8/B)A", 0x20001000, 0x20008000, 0x08000000, 0x08020000, 16, p_256, 0x1FF80000, 0x1FF8001F, + 0x1FF00000, 0x1FF01000, 0}, + {0x427, "STM32L1xxxC", 0x20001000, 0x20008000, 0x08000000, 0x08040000, 16, p_256, 0x1FF80000, 0x1FF8001F, + 0x1FF00000, 0x1FF02000, 0}, + {0x436, "STM32L1xxxD", 0x20001000, 0x2000C000, 0x08000000, 0x08060000, 16, p_256, 0x1FF80000, 0x1FF8009F, + 0x1FF00000, 0x1FF02000, 0}, + {0x437, "STM32L1xxxE", 0x20001000, 0x20014000, 0x08000000, 0x08080000, 16, p_256, 0x1FF80000, 0x1FF8009F, + 0x1FF00000, 0x1FF02000, F_NO_ME}, + /* L4 */ + {0x415, "STM32L476xx/486xx", 0x20003100, 0x20018000, 0x08000000, 0x08100000, 1, p_2k, 0x1FFF7800, 0x1FFFF80F, + 0x1FFF0000, 0x1FFF7000, 0}, + /* These are not (yet) in AN2606: */ + {0x641, "Medium_Density PL", 0x20000200, 0x20005000, 0x08000000, 0x08020000, 4, p_1k, 0x1FFFF800, 0x1FFFF80F, + 0x1FFFF000, 0x1FFFF800, 0}, + {0x9a8, "STM32W-128K", 0x20000200, 0x20002000, 0x08000000, 0x08020000, 4, p_1k, 0x08040800, 0x0804080F, 0x08040000, + 0x08040800, 0}, + {0x9b0, "STM32W-256K", 0x20000200, 0x20004000, 0x08000000, 0x08040000, 4, p_2k, 0x08040800, 0x0804080F, 0x08040000, + 0x08040800, 0}, + {0x0, "", 0x0, 0x0, 0x0, 0x0, 0x0, nullptr, 0x0, 0x0, 0x0, 0x0, 0x0}, +}; + +} // namespace shelly_dimmer +} // namespace esphome +#endif diff --git a/esphome/components/shelly_dimmer/light.py b/esphome/components/shelly_dimmer/light.py new file mode 100644 index 0000000000..003498c090 --- /dev/null +++ b/esphome/components/shelly_dimmer/light.py @@ -0,0 +1,219 @@ +from pathlib import Path +import hashlib +import re +import requests + + +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.components import light, sensor, uart +from esphome.const import ( + CONF_OUTPUT_ID, + CONF_GAMMA_CORRECT, + CONF_POWER, + CONF_VOLTAGE, + CONF_CURRENT, + CONF_VERSION, + CONF_URL, + CONF_UPDATE_INTERVAL, + UNIT_VOLT, + UNIT_AMPERE, + UNIT_WATT, + DEVICE_CLASS_POWER, + DEVICE_CLASS_VOLTAGE, +) +from esphome.core import HexInt, CORE + +DOMAIN = "shelly_dimmer" +DEPENDENCIES = ["sensor", "uart"] + +shelly_dimmer_ns = cg.esphome_ns.namespace("shelly_dimmer") +ShellyDimmer = shelly_dimmer_ns.class_( + "ShellyDimmer", light.LightOutput, cg.PollingComponent, uart.UARTDevice +) + +CONF_FIRMWARE = "firmware" +CONF_SHA256 = "sha256" +CONF_UPDATE = "update" + +CONF_LEADING_EDGE = "leading_edge" +CONF_WARMUP_BRIGHTNESS = "warmup_brightness" +# CONF_WARMUP_TIME = "warmup_time" +CONF_MIN_BRIGHTNESS = "min_brightness" +CONF_MAX_BRIGHTNESS = "max_brightness" + +CONF_NRST_PIN = "nrst_pin" +CONF_BOOT0_PIN = "boot0_pin" + +KNOWN_FIRMWARE = { + "51.5": ( + "https://github.com/jamesturton/shelly-dimmer-stm32/releases/download/v51.5/shelly-dimmer-stm32_v51.5.bin", + "553fc1d78ed113227af7683eaa9c26189a961c4ea9a48000fb5aa8f8ac5d7b60", + ), + "51.6": ( + "https://github.com/jamesturton/shelly-dimmer-stm32/releases/download/v51.6/shelly-dimmer-stm32_v51.6.bin", + "eda483e111c914723a33f5088f1397d5c0b19333db4a88dc965636b976c16c36", + ), +} + + +def parse_firmware_version(value): + match = re.match(r"(\d+).(\d+)", value) + if match is None: + raise ValueError(f"Not a valid version number {value}") + major = int(match[1]) + minor = int(match[2]) + return major, minor + + +def get_firmware(value): + if not value[CONF_UPDATE]: + return None + + def dl(url): + try: + req = requests.get(url) + req.raise_for_status() + except requests.exceptions.RequestException as e: + raise cv.Invalid(f"Could not download firmware file ({url}): {e}") + + h = hashlib.new("sha256") + h.update(req.content) + return req.content, h.hexdigest() + + url = value[CONF_URL] + + if CONF_SHA256 in value: # we have a hash, enable caching + path = ( + Path(CORE.config_dir) + / ".esphome" + / DOMAIN + / (value[CONF_SHA256] + "_fw_stm.bin") + ) + + if not path.is_file(): + firmware_data, dl_hash = dl(url) + + if dl_hash != value[CONF_SHA256]: + raise cv.Invalid( + f"Hash mismatch for {url}: {dl_hash} != {value[CONF_SHA256]}" + ) + + path.parent.mkdir(exist_ok=True, parents=True) + path.write_bytes(firmware_data) + + else: + firmware_data = path.read_bytes() + else: # no caching, download every time + firmware_data, dl_hash = dl(url) + + return [HexInt(x) for x in firmware_data] + + +def validate_firmware(value): + config = value.copy() + if CONF_URL not in config: + try: + config[CONF_URL], config[CONF_SHA256] = KNOWN_FIRMWARE[config[CONF_VERSION]] + except KeyError as e: + raise cv.Invalid( + f"Firmware {config[CONF_VERSION]} is unknown, please specify an '{CONF_URL}' ..." + ) from e + get_firmware(config) + return config + + +def validate_sha256(value): + value = cv.string(value) + if not value.isalnum() or not len(value) == 64: + raise ValueError(f"Not a valid SHA256 hex string: {value}") + return value + + +def validate_version(value): + parse_firmware_version(value) + return value + + +CONFIG_SCHEMA = ( + light.BRIGHTNESS_ONLY_LIGHT_SCHEMA.extend( + { + cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(ShellyDimmer), + cv.Optional(CONF_FIRMWARE, default="51.6"): cv.maybe_simple_value( + { + cv.Optional(CONF_URL): cv.url, + cv.Optional(CONF_SHA256): validate_sha256, + cv.Required(CONF_VERSION): validate_version, + cv.Optional(CONF_UPDATE, default=False): cv.boolean, + }, + validate_firmware, # converts a simple version key to generate the full url + key=CONF_VERSION, + ), + cv.Optional(CONF_NRST_PIN, default="GPIO5"): pins.gpio_output_pin_schema, + cv.Optional(CONF_BOOT0_PIN, default="GPIO4"): pins.gpio_output_pin_schema, + cv.Optional(CONF_LEADING_EDGE, default=False): cv.boolean, + cv.Optional(CONF_WARMUP_BRIGHTNESS, default=100): cv.uint16_t, + # cv.Optional(CONF_WARMUP_TIME, default=20): cv.uint16_t, + cv.Optional(CONF_MIN_BRIGHTNESS, default=0): cv.uint16_t, + cv.Optional(CONF_MAX_BRIGHTNESS, default=1000): cv.uint16_t, + cv.Optional(CONF_POWER): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_POWER, + ), + cv.Optional(CONF_VOLTAGE): sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_VOLTAGE, + ), + cv.Optional(CONF_CURRENT): sensor.sensor_schema( + unit_of_measurement=UNIT_AMPERE, + device_class=DEVICE_CLASS_POWER, + accuracy_decimals=2, + ), + # Change the default gamma_correct setting. + cv.Optional(CONF_GAMMA_CORRECT, default=1.0): cv.positive_float, + } + ) + .extend(cv.polling_component_schema("10s")) + .extend(uart.UART_DEVICE_SCHEMA) +) + + +def to_code(config): + fw_hex = get_firmware(config[CONF_FIRMWARE]) + fw_major, fw_minor = parse_firmware_version(config[CONF_FIRMWARE][CONF_VERSION]) + + if fw_hex is not None: + cg.add_define("USE_SHD_FIRMWARE_DATA", fw_hex) + cg.add_define("USE_SHD_FIRMWARE_MAJOR_VERSION", fw_major) + cg.add_define("USE_SHD_FIRMWARE_MINOR_VERSION", fw_minor) + + var = cg.new_Pvariable(config[CONF_OUTPUT_ID]) + yield cg.register_component(var, config) + config.pop( + CONF_UPDATE_INTERVAL + ) # drop UPDATE_INTERVAL as it does not apply to the light component + + yield light.register_light(var, config) + yield uart.register_uart_device(var, config) + + nrst_pin = yield cg.gpio_pin_expression(config[CONF_NRST_PIN]) + cg.add(var.set_nrst_pin(nrst_pin)) + boot0_pin = yield cg.gpio_pin_expression(config[CONF_BOOT0_PIN]) + cg.add(var.set_boot0_pin(boot0_pin)) + + cg.add(var.set_leading_edge(config[CONF_LEADING_EDGE])) + cg.add(var.set_warmup_brightness(config[CONF_WARMUP_BRIGHTNESS])) + # cg.add(var.set_warmup_time(config[CONF_WARMUP_TIME])) + cg.add(var.set_min_brightness(config[CONF_MIN_BRIGHTNESS])) + cg.add(var.set_max_brightness(config[CONF_MAX_BRIGHTNESS])) + + for key in [CONF_POWER, CONF_VOLTAGE, CONF_CURRENT]: + if key not in config: + continue + + conf = config[key] + sens = yield sensor.new_sensor(conf) + cg.add(getattr(var, f"set_{key}_sensor")(sens)) diff --git a/esphome/components/shelly_dimmer/shelly_dimmer.cpp b/esphome/components/shelly_dimmer/shelly_dimmer.cpp new file mode 100644 index 0000000000..3b79d0bf57 --- /dev/null +++ b/esphome/components/shelly_dimmer/shelly_dimmer.cpp @@ -0,0 +1,526 @@ +#include "esphome/core/defines.h" +#include "esphome/core/helpers.h" + +#include "shelly_dimmer.h" +#ifdef USE_SHD_FIRMWARE_DATA +#include "stm32flash.h" +#endif + +#ifndef USE_ESP_IDF +#include +#endif + +#include +#include +#include +#include + +namespace { + +constexpr char TAG[] = "shelly_dimmer"; + +constexpr uint8_t SHELLY_DIMMER_ACK_TIMEOUT = 200; // ms +constexpr uint8_t SHELLY_DIMMER_MAX_RETRIES = 3; +constexpr uint16_t SHELLY_DIMMER_MAX_BRIGHTNESS = 1000; // 100% + +// Protocol framing. +constexpr uint8_t SHELLY_DIMMER_PROTO_START_BYTE = 0x01; +constexpr uint8_t SHELLY_DIMMER_PROTO_END_BYTE = 0x04; + +// Supported commands. +constexpr uint8_t SHELLY_DIMMER_PROTO_CMD_SWITCH = 0x01; +constexpr uint8_t SHELLY_DIMMER_PROTO_CMD_POLL = 0x10; +constexpr uint8_t SHELLY_DIMMER_PROTO_CMD_VERSION = 0x11; +constexpr uint8_t SHELLY_DIMMER_PROTO_CMD_SETTINGS = 0x20; + +// Command payload sizes. +constexpr uint8_t SHELLY_DIMMER_PROTO_CMD_SWITCH_SIZE = 2; +constexpr uint8_t SHELLY_DIMMER_PROTO_CMD_SETTINGS_SIZE = 10; +constexpr uint8_t SHELLY_DIMMER_PROTO_MAX_FRAME_SIZE = 4 + 72 + 3; + +// STM Firmware +#ifdef USE_SHD_FIRMWARE_DATA +constexpr uint8_t STM_FIRMWARE[] PROGMEM = USE_SHD_FIRMWARE_DATA; +constexpr uint32_t STM_FIRMWARE_SIZE_IN_BYTES = sizeof(STM_FIRMWARE); +#endif + +// Scaling Constants +constexpr float POWER_SCALING_FACTOR = 880373; +constexpr float VOLTAGE_SCALING_FACTOR = 347800; +constexpr float CURRENT_SCALING_FACTOR = 1448; + +// Esentially std::size() for pre c++17 +template constexpr size_t size(const T (&/*unused*/)[N]) noexcept { return N; } + +} // Anonymous namespace + +namespace esphome { +namespace shelly_dimmer { + +/// Computes a crappy checksum as defined by the Shelly Dimmer protocol. +uint16_t shelly_dimmer_checksum(const uint8_t *buf, int len) { + return std::accumulate(buf, buf + len, 0); +} + +void ShellyDimmer::setup() { + this->pin_nrst_->setup(); + this->pin_boot0_->setup(); + + ESP_LOGI(TAG, "Initializing Shelly Dimmer..."); + + // Reset the STM32 and check the firmware version. + for (int i = 0; i < 2; i++) { + this->reset_normal_boot_(); + this->send_command_(SHELLY_DIMMER_PROTO_CMD_VERSION, nullptr, 0); + ESP_LOGI(TAG, "STM32 current firmware version: %d.%d, desired version: %d.%d", this->version_major_, + this->version_minor_, USE_SHD_FIRMWARE_MAJOR_VERSION, USE_SHD_FIRMWARE_MINOR_VERSION); + if (this->version_major_ != USE_SHD_FIRMWARE_MAJOR_VERSION || + this->version_minor_ != USE_SHD_FIRMWARE_MINOR_VERSION) { +#ifdef USE_SHD_FIRMWARE_DATA + // Update firmware if needed. + ESP_LOGW(TAG, "Unsupported STM32 firmware version, flashing"); + if (i > 0) { + // Upgrade was already performed but the reported version is still not right. + ESP_LOGE(TAG, "STM32 firmware upgrade already performed, but version is still incorrect"); + this->mark_failed(); + return; + } + + if (!this->upgrade_firmware_()) { + ESP_LOGW(TAG, "Failed to upgrade firmware"); + this->mark_failed(); + return; + } + + // Firmware upgrade completed, do the checks again. + continue; +#else + ESP_LOGW(TAG, "Firmware version mismatch, put 'update: true' in the yaml to flash an update."); + this->mark_failed(); + return; +#endif + } + break; + } + + this->send_settings_(); + // Do an immediate poll to refresh current state. + this->send_command_(SHELLY_DIMMER_PROTO_CMD_POLL, nullptr, 0); + + this->ready_ = true; +} + +void ShellyDimmer::update() { this->send_command_(SHELLY_DIMMER_PROTO_CMD_POLL, nullptr, 0); } + +void ShellyDimmer::dump_config() { + ESP_LOGCONFIG(TAG, "ShellyDimmer:"); + LOG_PIN(" NRST Pin: ", this->pin_nrst_); + LOG_PIN(" BOOT0 Pin: ", this->pin_boot0_); + + ESP_LOGCONFIG(TAG, " Leading Edge: %s", YESNO(this->leading_edge_)); + ESP_LOGCONFIG(TAG, " Warmup Brightness: %d", this->warmup_brightness_); + // ESP_LOGCONFIG(TAG, " Warmup Time: %d", this->warmup_time_); + // ESP_LOGCONFIG(TAG, " Fade Rate: %d", this->fade_rate_); + ESP_LOGCONFIG(TAG, " Minimum Brightness: %d", this->min_brightness_); + ESP_LOGCONFIG(TAG, " Maximum Brightness: %d", this->max_brightness_); + + LOG_UPDATE_INTERVAL(this); + + ESP_LOGCONFIG(TAG, " STM32 current firmware version: %d.%d ", this->version_major_, this->version_minor_); + ESP_LOGCONFIG(TAG, " STM32 required firmware version: %d.%d", USE_SHD_FIRMWARE_MAJOR_VERSION, + USE_SHD_FIRMWARE_MINOR_VERSION); + + if (this->version_major_ != USE_SHD_FIRMWARE_MAJOR_VERSION || + this->version_minor_ != USE_SHD_FIRMWARE_MINOR_VERSION) { + ESP_LOGE(TAG, " Firmware version mismatch, put 'update: true' in the yaml to flash an update."); + } +} + +void ShellyDimmer::write_state(light::LightState *state) { + if (!this->ready_) { + return; + } + + float brightness; + state->current_values_as_brightness(&brightness); + + const uint16_t brightness_int = this->convert_brightness_(brightness); + if (brightness_int == this->brightness_) { + ESP_LOGV(TAG, "Not sending unchanged value"); + return; + } + ESP_LOGD(TAG, "Brightness update: %d (raw: %f)", brightness_int, brightness); + + this->send_brightness_(brightness_int); +} +#ifdef USE_SHD_FIRMWARE_DATA +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}; + + if (!stm32) { + ESP_LOGW(TAG, "Failed to initialize STM32"); + return false; + } + + // Erase STM32 flash. + if (stm32_erase_memory(stm32.get(), 0, STM32_MASS_ERASE) != STM32_ERR_OK) { + ESP_LOGW(TAG, "Failed to erase STM32 flash memory"); + return false; + } + + static constexpr uint32_t BUFFER_SIZE = 256; + + // Copy the STM32 firmware over in 256-byte chunks. Note that the firmware is stored + // in flash memory so all accesses need to be 4-byte aligned. + uint8_t buffer[BUFFER_SIZE]; + const uint8_t *p = STM_FIRMWARE; + uint32_t offset = 0; + uint32_t addr = stm32->dev->fl_start; + const uint32_t end = addr + STM_FIRMWARE_SIZE_IN_BYTES; + + while (addr < end && offset < STM_FIRMWARE_SIZE_IN_BYTES) { + const uint32_t left_of_buffer = std::min(end - addr, BUFFER_SIZE); + const uint32_t len = std::min(left_of_buffer, STM_FIRMWARE_SIZE_IN_BYTES - offset); + + if (len == 0) { + break; + } + + std::memcpy(buffer, p, BUFFER_SIZE); + p += BUFFER_SIZE; + + if (stm32_write_memory(stm32.get(), addr, buffer, len) != STM32_ERR_OK) { + ESP_LOGW(TAG, "Failed to write to STM32 flash memory"); + return false; + } + + addr += len; + offset += len; + } + + ESP_LOGI(TAG, "STM32 firmware upgrade successful"); + + return true; +} +#endif + +uint16_t ShellyDimmer::convert_brightness_(float brightness) { + // Special case for zero as only zero means turn off completely. + if (brightness == 0.0) { + return 0; + } + + return remap(brightness, 0.0f, 1.0f, this->min_brightness_, this->max_brightness_); +} + +void ShellyDimmer::send_brightness_(uint16_t brightness) { + const uint8_t payload[] = { + // Brightness (%) * 10. + static_cast(brightness & 0xff), + static_cast(brightness >> 8), + }; + static_assert(size(payload) == SHELLY_DIMMER_PROTO_CMD_SWITCH_SIZE, "Invalid payload size"); + + this->send_command_(SHELLY_DIMMER_PROTO_CMD_SWITCH, payload, SHELLY_DIMMER_PROTO_CMD_SWITCH_SIZE); + + this->brightness_ = brightness; +} + +void ShellyDimmer::send_settings_() { + const uint16_t fade_rate = std::min(uint16_t{100}, this->fade_rate_); + + float brightness = 0.0; + if (this->state_ != nullptr) { + this->state_->current_values_as_brightness(&brightness); + } + const uint16_t brightness_int = this->convert_brightness_(brightness); + ESP_LOGD(TAG, "Brightness update: %d (raw: %f)", brightness_int, brightness); + + const uint8_t payload[] = { + // Brightness (%) * 10. + static_cast(brightness_int & 0xff), + static_cast(brightness_int >> 8), + // Leading / trailing edge [0x01 = leading, 0x02 = trailing]. + this->leading_edge_ ? uint8_t{0x01} : uint8_t{0x02}, + 0x00, + // Fade rate. + static_cast(fade_rate & 0xff), + static_cast(fade_rate >> 8), + // Warmup brightness. + static_cast(this->warmup_brightness_ & 0xff), + static_cast(this->warmup_brightness_ >> 8), + // Warmup time. + static_cast(this->warmup_time_ & 0xff), + static_cast(this->warmup_time_ >> 8), + }; + static_assert(size(payload) == SHELLY_DIMMER_PROTO_CMD_SETTINGS_SIZE, "Invalid payload size"); + + this->send_command_(SHELLY_DIMMER_PROTO_CMD_SETTINGS, payload, SHELLY_DIMMER_PROTO_CMD_SETTINGS_SIZE); + + // Also send brightness separately as it is ignored above. + this->send_brightness_(brightness_int); +} + +bool ShellyDimmer::send_command_(uint8_t cmd, const uint8_t *const payload, uint8_t len) { + ESP_LOGD(TAG, "Sending command: 0x%02x (%d bytes) payload 0x%s", cmd, len, format_hex(payload, len).c_str()); + + // Prepare a command frame. + uint8_t frame[SHELLY_DIMMER_PROTO_MAX_FRAME_SIZE]; + const size_t frame_len = this->frame_command_(frame, cmd, payload, len); + + // Write the frame and wait for acknowledgement. + int retries = SHELLY_DIMMER_MAX_RETRIES; + while (retries--) { + this->write_array(frame, frame_len); + this->flush(); + + ESP_LOGD(TAG, "Command sent, waiting for reply"); + const uint32_t tx_time = millis(); + while (millis() - tx_time < SHELLY_DIMMER_ACK_TIMEOUT) { + if (this->read_frame_()) { + return true; + } + delay(1); + } + ESP_LOGW(TAG, "Timeout while waiting for reply"); + } + ESP_LOGW(TAG, "Failed to send command"); + return false; +} + +size_t ShellyDimmer::frame_command_(uint8_t *data, uint8_t cmd, const uint8_t *const payload, size_t len) { + size_t pos = 0; + + // Generate a frame. + data[0] = SHELLY_DIMMER_PROTO_START_BYTE; + data[1] = ++this->seq_; + data[2] = cmd; + data[3] = len; + pos += 4; + + if (payload != nullptr) { + std::memcpy(data + 4, payload, len); + pos += len; + } + + // Calculate checksum for the payload. + const uint16_t csum = shelly_dimmer_checksum(data + 1, 3 + len); + data[pos++] = static_cast(csum >> 8); + data[pos++] = static_cast(csum & 0xff); + data[pos++] = SHELLY_DIMMER_PROTO_END_BYTE; + return pos; +} + +int ShellyDimmer::handle_byte_(uint8_t c) { + const uint8_t pos = this->buffer_pos_; + + if (pos == 0) { + // Must be start byte. + return c == SHELLY_DIMMER_PROTO_START_BYTE ? 1 : -1; + } else if (pos < 4) { + // Header. + return 1; + } + + // Decode payload length from header. + const uint8_t payload_len = this->buffer_[3]; + if ((4 + payload_len + 3) > SHELLY_DIMMER_BUFFER_SIZE) { + return -1; + } + + if (pos < 4 + payload_len + 1) { + // Payload. + return 1; + } + + if (pos == 4 + payload_len + 1) { + // Verify checksum. + const uint16_t csum = (this->buffer_[pos - 1] << 8 | c); + const uint16_t csum_verify = shelly_dimmer_checksum(&this->buffer_[1], 3 + payload_len); + if (csum != csum_verify) { + return -1; + } + return 1; + } + + if (pos == 4 + payload_len + 2) { + // Must be end byte. + return c == SHELLY_DIMMER_PROTO_END_BYTE ? 0 : -1; + } + return -1; +} + +bool ShellyDimmer::read_frame_() { + while (this->available()) { + const uint8_t c = this->read(); + this->buffer_[this->buffer_pos_] = c; + + ESP_LOGV(TAG, "Read byte: 0x%02x (pos %d)", c, this->buffer_pos_); + + switch (this->handle_byte_(c)) { + case 0: { + // Frame successfully received. + this->handle_frame_(); + this->buffer_pos_ = 0; + return true; + } + case -1: { + // Failure. + this->buffer_pos_ = 0; + break; + } + case 1: { + // Need more data. + this->buffer_pos_++; + break; + } + } + } + return false; +} + +bool ShellyDimmer::handle_frame_() { + const uint8_t seq = this->buffer_[1]; + const uint8_t cmd = this->buffer_[2]; + const uint8_t payload_len = this->buffer_[3]; + + ESP_LOGD(TAG, "Got frame: 0x%02x", cmd); + + // Compare with expected identifier as the frame is always a response to + // our previously sent command. + if (seq != this->seq_) { + return false; + } + + const uint8_t *payload = &this->buffer_[4]; + + // Handle response. + switch (cmd) { + case SHELLY_DIMMER_PROTO_CMD_POLL: { + if (payload_len < 16) { + return false; + } + + const uint8_t hw_version = payload[0]; + // payload[1] is unused. + const uint16_t brightness = encode_uint16(payload[3], payload[2]); + + const uint32_t power_raw = encode_uint32(payload[7], payload[6], payload[5], payload[4]); + + const uint32_t voltage_raw = encode_uint32(payload[11], payload[10], payload[9], payload[8]); + + const uint32_t current_raw = encode_uint32(payload[15], payload[14], payload[13], payload[12]); + + const uint16_t fade_rate = payload[16]; + + float power = 0; + if (power_raw > 0) { + power = POWER_SCALING_FACTOR / static_cast(power_raw); + } + + float voltage = 0; + if (voltage_raw > 0) { + voltage = VOLTAGE_SCALING_FACTOR / static_cast(voltage_raw); + } + + float current = 0; + if (current_raw > 0) { + current = CURRENT_SCALING_FACTOR / static_cast(current_raw); + } + + ESP_LOGI(TAG, "Got dimmer data:"); + ESP_LOGI(TAG, " HW version: %d", hw_version); + ESP_LOGI(TAG, " Brightness: %d", brightness); + ESP_LOGI(TAG, " Fade rate: %d", fade_rate); + ESP_LOGI(TAG, " Power: %f W", power); + ESP_LOGI(TAG, " Voltage: %f V", voltage); + ESP_LOGI(TAG, " Current: %f A", current); + + // Update sensors. + if (this->power_sensor_ != nullptr) { + this->power_sensor_->publish_state(power); + } + if (this->voltage_sensor_ != nullptr) { + this->voltage_sensor_->publish_state(voltage); + } + if (this->current_sensor_ != nullptr) { + this->current_sensor_->publish_state(current); + } + + return true; + } + case SHELLY_DIMMER_PROTO_CMD_VERSION: { + if (payload_len < 2) { + return false; + } + + this->version_minor_ = payload[0]; + this->version_major_ = payload[1]; + return true; + } + case SHELLY_DIMMER_PROTO_CMD_SWITCH: + case SHELLY_DIMMER_PROTO_CMD_SETTINGS: { + return !(payload_len < 1 || payload[0] != 0x01); + } + default: { + return false; + } + } +} + +void ShellyDimmer::reset_(bool boot0) { + ESP_LOGD(TAG, "Reset STM32, boot0=%d", boot0); + + this->pin_boot0_->digital_write(boot0); + this->pin_nrst_->digital_write(false); + + // Wait 50ms for the STM32 to reset. + delay(50); // NOLINT + + // Clear receive buffer. + while (this->available()) { + this->read(); + } + + this->pin_nrst_->digital_write(true); + // Wait 50ms for the STM32 to boot. + delay(50); // NOLINT + + ESP_LOGD(TAG, "Reset STM32 done"); +} + +void ShellyDimmer::reset_normal_boot_() { + // set NONE parity in normal mode + +#ifndef USE_ESP_IDF // workaround for reconfiguring the uart + Serial.end(); + Serial.begin(115200, SERIAL_8N1); + Serial.flush(); +#endif + + this->flush(); + this->reset_(false); +} + +void ShellyDimmer::reset_dfu_boot_() { + // set EVEN parity in bootloader mode + +#ifndef USE_ESP_IDF // workaround for reconfiguring the uart + Serial.end(); + Serial.begin(115200, SERIAL_8E1); + Serial.flush(); +#endif + + this->flush(); + this->reset_(true); +} + +} // namespace shelly_dimmer +} // namespace esphome diff --git a/esphome/components/shelly_dimmer/shelly_dimmer.h b/esphome/components/shelly_dimmer/shelly_dimmer.h new file mode 100644 index 0000000000..b7d476279e --- /dev/null +++ b/esphome/components/shelly_dimmer/shelly_dimmer.h @@ -0,0 +1,117 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/log.h" +#include "esphome/components/light/light_output.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/uart/uart.h" + +#include + +namespace esphome { +namespace shelly_dimmer { + +class ShellyDimmer : public PollingComponent, public light::LightOutput, public uart::UARTDevice { + private: + static constexpr uint16_t SHELLY_DIMMER_BUFFER_SIZE = 256; + + public: + float get_setup_priority() const override { return setup_priority::LATE; } + + void setup() override; + void update() override; + void dump_config() override; + + light::LightTraits get_traits() override { + auto traits = light::LightTraits(); + traits.set_supported_color_modes({light::ColorMode::BRIGHTNESS}); + return traits; + } + + void setup_state(light::LightState *state) override { this->state_ = state; } + void write_state(light::LightState *state) override; + + void set_nrst_pin(GPIOPin *nrst_pin) { this->pin_nrst_ = nrst_pin; } + void set_boot0_pin(GPIOPin *boot0_pin) { this->pin_boot0_ = boot0_pin; } + + void set_leading_edge(bool leading_edge) { this->leading_edge_ = leading_edge; } + void set_warmup_brightness(uint16_t warmup_brightness) { this->warmup_brightness_ = warmup_brightness; } + void set_warmup_time(uint16_t warmup_time) { this->warmup_time_ = warmup_time; } + void set_fade_rate(uint16_t fade_rate) { this->fade_rate_ = fade_rate; } + void set_min_brightness(uint16_t min_brightness) { this->min_brightness_ = min_brightness; } + void set_max_brightness(uint16_t max_brightness) { this->max_brightness_ = max_brightness; } + + void set_power_sensor(sensor::Sensor *power_sensor) { this->power_sensor_ = power_sensor; } + void set_voltage_sensor(sensor::Sensor *voltage_sensor) { this->voltage_sensor_ = voltage_sensor; } + void set_current_sensor(sensor::Sensor *current_sensor) { this->current_sensor_ = current_sensor; } + + protected: + GPIOPin *pin_nrst_; + GPIOPin *pin_boot0_; + + // Frame parser state. + uint8_t seq_{0}; + std::array buffer_; + uint8_t buffer_pos_{0}; + + // Firmware version. + uint8_t version_major_; + uint8_t version_minor_; + + // Configuration. + bool leading_edge_{false}; + uint16_t warmup_brightness_{100}; + uint16_t warmup_time_{20}; + uint16_t fade_rate_{0}; + uint16_t min_brightness_{0}; + uint16_t max_brightness_{1000}; + + light::LightState *state_{nullptr}; + sensor::Sensor *power_sensor_{nullptr}; + sensor::Sensor *voltage_sensor_{nullptr}; + sensor::Sensor *current_sensor_{nullptr}; + + bool ready_{false}; + uint16_t brightness_; + + /// Convert relative brightness into a dimmer brightness value. + uint16_t convert_brightness_(float brightness); + + /// Sends the given brightness value. + void send_brightness_(uint16_t brightness); + + /// Sends dimmer configuration. + void send_settings_(); + + /// Performs a firmware upgrade. + bool upgrade_firmware_(); + + /// Sends a command and waits for an acknowledgement. + bool send_command_(uint8_t cmd, const uint8_t *payload, uint8_t len); + + /// Frames a given command payload. + size_t frame_command_(uint8_t *data, uint8_t cmd, const uint8_t *payload, size_t len); + + /// Handles a single byte as part of a protocol frame. + /// + /// Returns -1 on failure, 0 when finished and 1 when more bytes needed. + int handle_byte_(uint8_t c); + + /// Reads a response frame. + bool read_frame_(); + + /// Handles a complete frame. + bool handle_frame_(); + + /// Reset STM32 with the BOOT0 pin set to the given value. + void reset_(bool boot0); + + /// Reset STM32 to boot the regular firmware. + void reset_normal_boot_(); + + /// Reset STM32 to boot into DFU mode to enable firmware upgrades. + void reset_dfu_boot_(); +}; + +} // namespace shelly_dimmer +} // namespace esphome diff --git a/esphome/components/shelly_dimmer/stm32flash.cpp b/esphome/components/shelly_dimmer/stm32flash.cpp new file mode 100644 index 0000000000..4c777776fb --- /dev/null +++ b/esphome/components/shelly_dimmer/stm32flash.cpp @@ -0,0 +1,1061 @@ +/* + stm32flash - Open Source ST STM32 flash program for Arduino + Copyright 2010 Geoffrey McRae + Copyright 2012-2014 Tormod Volden + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +*/ + +#include "esphome/core/defines.h" +#ifdef USE_SHD_FIRMWARE_DATA + +#include + +#include "stm32flash.h" +#include "debug.h" + +#include "dev_table.h" +#include "esphome/core/log.h" + +#include +#include + +namespace { + +constexpr uint8_t STM32_ACK = 0x79; +constexpr uint8_t STM32_NACK = 0x1F; +constexpr uint8_t STM32_BUSY = 0x76; + +constexpr uint8_t STM32_CMD_INIT = 0x7F; +constexpr uint8_t STM32_CMD_GET = 0x00; /* get the version and command supported */ +constexpr uint8_t STM32_CMD_GVR = 0x01; /* get version and read protection status */ +constexpr uint8_t STM32_CMD_GID = 0x02; /* get ID */ +constexpr uint8_t STM32_CMD_RM = 0x11; /* read memory */ +constexpr uint8_t STM32_CMD_GO = 0x21; /* go */ +constexpr uint8_t STM32_CMD_WM = 0x31; /* write memory */ +constexpr uint8_t STM32_CMD_WM_NS = 0x32; /* no-stretch write memory */ +constexpr uint8_t STM32_CMD_ER = 0x43; /* erase */ +constexpr uint8_t STM32_CMD_EE = 0x44; /* extended erase */ +constexpr uint8_t STM32_CMD_EE_NS = 0x45; /* extended erase no-stretch */ +constexpr uint8_t STM32_CMD_WP = 0x63; /* write protect */ +constexpr uint8_t STM32_CMD_WP_NS = 0x64; /* write protect no-stretch */ +constexpr uint8_t STM32_CMD_UW = 0x73; /* write unprotect */ +constexpr uint8_t STM32_CMD_UW_NS = 0x74; /* write unprotect no-stretch */ +constexpr uint8_t STM32_CMD_RP = 0x82; /* readout protect */ +constexpr uint8_t STM32_CMD_RP_NS = 0x83; /* readout protect no-stretch */ +constexpr uint8_t STM32_CMD_UR = 0x92; /* readout unprotect */ +constexpr uint8_t STM32_CMD_UR_NS = 0x93; /* readout unprotect no-stretch */ +constexpr uint8_t STM32_CMD_CRC = 0xA1; /* compute CRC */ +constexpr uint8_t STM32_CMD_ERR = 0xFF; /* not a valid command */ + +constexpr uint32_t STM32_RESYNC_TIMEOUT = 35 * 1000; /* milliseconds */ +constexpr uint32_t STM32_MASSERASE_TIMEOUT = 35 * 1000; /* milliseconds */ +constexpr uint32_t STM32_PAGEERASE_TIMEOUT = 5 * 1000; /* milliseconds */ +constexpr uint32_t STM32_BLKWRITE_TIMEOUT = 1 * 1000; /* milliseconds */ +constexpr uint32_t STM32_WUNPROT_TIMEOUT = 1 * 1000; /* milliseconds */ +constexpr uint32_t STM32_WPROT_TIMEOUT = 1 * 1000; /* milliseconds */ +constexpr uint32_t STM32_RPROT_TIMEOUT = 1 * 1000; /* milliseconds */ +constexpr uint32_t DEFAULT_TIMEOUT = 5 * 1000; /* milliseconds */ + +constexpr uint8_t STM32_CMD_GET_LENGTH = 17; /* bytes in the reply */ + +/* Reset code for ARMv7-M (Cortex-M3) and ARMv6-M (Cortex-M0) + * see ARMv7-M or ARMv6-M Architecture Reference Manual (table B3-8) + * or "The definitive guide to the ARM Cortex-M3", section 14.4. + */ +constexpr uint8_t STM_RESET_CODE[] = { + 0x01, 0x49, // ldr r1, [pc, #4] ; () + 0x02, 0x4A, // ldr r2, [pc, #8] ; () + 0x0A, 0x60, // str r2, [r1, #0] + 0xfe, 0xe7, // endless: b endless + 0x0c, 0xed, 0x00, 0xe0, // .word 0xe000ed0c = NVIC AIRCR register address + 0x04, 0x00, 0xfa, 0x05 // .word 0x05fa0004 = VECTKEY | SYSRESETREQ +}; + +constexpr uint32_t STM_RESET_CODE_SIZE = sizeof(STM_RESET_CODE); + +/* RM0360, Empty check + * On STM32F070x6 and STM32F030xC devices only, internal empty check flag is + * implemented to allow easy programming of the virgin devices by the boot loader. This flag is + * used when BOOT0 pin is defining Main Flash memory as the target boot space. When the + * flag is set, the device is considered as empty and System memory (boot loader) is selected + * instead of the Main Flash as a boot space to allow user to program the Flash memory. + * This flag is updated only during Option bytes loading: it is set when the content of the + * address 0x08000 0000 is read as 0xFFFF FFFF, otherwise it is cleared. It means a power + * on or setting of OBL_LAUNCH bit in FLASH_CR register is needed to clear this flag after + * programming of a virgin device to execute user code after System reset. + */ +constexpr uint8_t STM_OBL_LAUNCH_CODE[] = { + 0x01, 0x49, // ldr r1, [pc, #4] ; () + 0x02, 0x4A, // ldr r2, [pc, #8] ; () + 0x0A, 0x60, // str r2, [r1, #0] + 0xfe, 0xe7, // endless: b endless + 0x10, 0x20, 0x02, 0x40, // address: FLASH_CR = 40022010 + 0x00, 0x20, 0x00, 0x00 // value: OBL_LAUNCH = 00002000 +}; + +constexpr uint32_t STM_OBL_LAUNCH_CODE_SIZE = sizeof(STM_OBL_LAUNCH_CODE); + +constexpr char TAG[] = "stm32flash"; + +} // Anonymous namespace + +namespace esphome { +namespace shelly_dimmer { + +namespace { + +int flash_addr_to_page_ceil(const stm32_t *stm, uint32_t addr) { + if (!(addr >= stm->dev->fl_start && addr <= stm->dev->fl_end)) + return 0; + + int page = 0; + addr -= stm->dev->fl_start; + const auto *psize = stm->dev->fl_ps; + + while (addr >= psize[0]) { + addr -= psize[0]; + page++; + if (psize[1]) + psize++; + } + + return addr ? page + 1 : page; +} + +stm32_err_t stm32_get_ack_timeout(const stm32_t *stm, uint32_t timeout) { + auto *stream = stm->stream; + uint8_t rxbyte; + + if (!(stm->flags & STREAM_OPT_RETRY)) + timeout = 0; + + if (timeout == 0) + timeout = DEFAULT_TIMEOUT; + + const uint32_t start_time = millis(); + do { + yield(); + if (!stream->available()) { + if (millis() < start_time + timeout) + continue; + ESP_LOGD(TAG, "Failed to read ACK timeout=%i", timeout); + return STM32_ERR_UNKNOWN; + } + + stream->read_byte(&rxbyte); + + if (rxbyte == STM32_ACK) + return STM32_ERR_OK; + if (rxbyte == STM32_NACK) + return STM32_ERR_NACK; + if (rxbyte != STM32_BUSY) { + ESP_LOGD(TAG, "Got byte 0x%02x instead of ACK", rxbyte); + return STM32_ERR_UNKNOWN; + } + } while (true); +} + +stm32_err_t stm32_get_ack(const stm32_t *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) { + auto *const stream = stm->stream; + + static constexpr auto BUFFER_SIZE = 2; + const uint8_t buf[] = { + cmd, + static_cast(cmd ^ 0xFF), + }; + static_assert(sizeof(buf) == BUFFER_SIZE, "Buf expected to be 2 bytes"); + + stream->write_array(buf, BUFFER_SIZE); + stream->flush(); + + stm32_err_t s_err = stm32_get_ack_timeout(stm, timeout); + if (s_err == STM32_ERR_OK) + return STM32_ERR_OK; + if (s_err == STM32_ERR_NACK) { + ESP_LOGD(TAG, "Got NACK from device on command 0x%02x", cmd); + } else { + ESP_LOGD(TAG, "Unexpected reply from device on command 0x%02x", cmd); + } + return STM32_ERR_UNKNOWN; +} + +stm32_err_t stm32_send_command(const stm32_t *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) { + auto *const stream = stm->stream; + uint32_t t0 = millis(); + auto t1 = t0; + + static constexpr auto BUFFER_SIZE = 2; + const uint8_t buf[] = { + STM32_CMD_ERR, + static_cast(STM32_CMD_ERR ^ 0xFF), + }; + static_assert(sizeof(buf) == BUFFER_SIZE, "Buf expected to be 2 bytes"); + + uint8_t ack; + while (t1 < t0 + STM32_RESYNC_TIMEOUT) { + stream->write_array(buf, BUFFER_SIZE); + stream->flush(); + if (!stream->read_array(&ack, 1)) { + t1 = millis(); + continue; + } + if (ack == STM32_NACK) + return STM32_ERR_OK; + t1 = millis(); + } + return STM32_ERR_UNKNOWN; +} + +/* + * some command receive reply frame with variable length, and length is + * embedded in reply frame itself. + * We can guess the length, but if we guess wrong the protocol gets out + * of sync. + * Use resync for frame oriented interfaces (e.g. I2C) and byte-by-byte + * read for byte oriented interfaces (e.g. UART). + * + * to run safely, data buffer should be allocated for 256+1 bytes + * + * 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) { + auto *const stream = stm->stream; + + if (stm32_send_command(stm, cmd) != STM32_ERR_OK) + return STM32_ERR_UNKNOWN; + if (stm->flags & STREAM_OPT_BYTE) { + /* interface is UART-like */ + if (!stream->read_array(data, 1)) + return STM32_ERR_UNKNOWN; + len = data[0]; + if (!stream->read_array(data + 1, len + 1)) + return STM32_ERR_UNKNOWN; + return STM32_ERR_OK; + } + + const auto ret = stream->read_array(data, len + 2); + if (ret && len == data[0]) + return STM32_ERR_OK; + if (!ret) { + /* restart with only one byte */ + if (stm32_resync(stm) != STM32_ERR_OK) + return STM32_ERR_UNKNOWN; + if (stm32_send_command(stm, cmd) != STM32_ERR_OK) + return STM32_ERR_UNKNOWN; + if (!stream->read_array(data, 1)) + return STM32_ERR_UNKNOWN; + } + + ESP_LOGD(TAG, "Re sync (len = %d)", data[0]); + if (stm32_resync(stm) != STM32_ERR_OK) + return STM32_ERR_UNKNOWN; + + len = data[0]; + if (stm32_send_command(stm, cmd) != STM32_ERR_OK) + return STM32_ERR_UNKNOWN; + + if (!stream->read_array(data, len + 2)) + return STM32_ERR_UNKNOWN; + return STM32_ERR_OK; +} + +/* + * Some interface, e.g. UART, requires a specific init sequence to let STM32 + * autodetect the interface speed. + * The sequence is only required one time after reset. + * 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) { + auto *const stream = stm->stream; + + stream->write_array(&STM32_CMD_INIT, 1); + stream->flush(); + + uint8_t byte; + bool ret = stream->read_array(&byte, 1); + if (ret && byte == STM32_ACK) + return STM32_ERR_OK; + if (ret && byte == STM32_NACK) { + /* We could get error later, but let's continue, for now. */ + ESP_LOGD(TAG, "Warning: the interface was not closed properly."); + return STM32_ERR_OK; + } + if (!ret) { + ESP_LOGD(TAG, "Failed to init device."); + return STM32_ERR_UNKNOWN; + } + + /* + * Check if previous STM32_CMD_INIT was taken as first byte + * of a command. Send a new byte, we should get back a NACK. + */ + stream->write_array(&STM32_CMD_INIT, 1); + stream->flush(); + + ret = stream->read_array(&byte, 1); + if (ret && byte == STM32_NACK) + return STM32_ERR_OK; + ESP_LOGD(TAG, "Failed to init device."); + return STM32_ERR_UNKNOWN; +} + +stm32_err_t stm32_mass_erase(const stm32_t *stm) { + auto *const stream = stm->stream; + + if (stm32_send_command(stm, stm->cmd->er) != STM32_ERR_OK) { + ESP_LOGD(TAG, "Can't initiate chip mass erase!"); + return STM32_ERR_UNKNOWN; + } + + /* regular erase (0x43) */ + if (stm->cmd->er == STM32_CMD_ER) { + const auto s_err = stm32_send_command_timeout(stm, 0xFF, STM32_MASSERASE_TIMEOUT); + if (s_err != STM32_ERR_OK) { + return STM32_ERR_UNKNOWN; + } + return STM32_ERR_OK; + } + + /* extended erase */ + static constexpr auto BUFFER_SIZE = 3; + const uint8_t buf[] = { + 0xFF, /* 0xFFFF the magic number for mass erase */ + 0xFF, 0x00, /* checksum */ + }; + static_assert(sizeof(buf) == BUFFER_SIZE, "Expected the buffer to be 3 bytes"); + stream->write_array(buf, 3); + stream->flush(); + + const auto s_err = stm32_get_ack_timeout(stm, STM32_MASSERASE_TIMEOUT); + if (s_err != STM32_ERR_OK) { + ESP_LOGD(TAG, "Mass erase failed. Try specifying the number of pages to be erased."); + return STM32_ERR_UNKNOWN; + } + return STM32_ERR_OK; +} + +template std::unique_ptr malloc_array_raii(size_t size) { + // Could be constexpr in c++17 + static const auto DELETOR = [](T *memory) { + free(memory); // NOLINT + }; + return std::unique_ptr{static_cast(malloc(size)), // NOLINT + DELETOR}; +} + +stm32_err_t stm32_pages_erase(const stm32_t *stm, const uint32_t spage, const uint32_t pages) { + auto *const stream = stm->stream; + uint8_t cs = 0; + int i = 0; + + /* The erase command reported by the bootloader is either 0x43, 0x44 or 0x45 */ + /* 0x44 is Extended Erase, a 2 byte based protocol and needs to be handled differently. */ + /* 0x45 is clock no-stretching version of Extended Erase for I2C port. */ + if (stm32_send_command(stm, stm->cmd->er) != STM32_ERR_OK) { + ESP_LOGD(TAG, "Can't initiate chip mass erase!"); + return STM32_ERR_UNKNOWN; + } + + /* regular erase (0x43) */ + if (stm->cmd->er == STM32_CMD_ER) { + // Free memory with RAII + auto buf = malloc_array_raii(1 + pages + 1); + + if (!buf) + return STM32_ERR_UNKNOWN; + + buf[i++] = pages - 1; + cs ^= (pages - 1); + for (auto pg_num = spage; pg_num < (pages + spage); pg_num++) { + buf[i++] = pg_num; + cs ^= pg_num; + } + buf[i++] = cs; + stream->write_array(&buf[0], i); + stream->flush(); + + const auto s_err = stm32_get_ack_timeout(stm, pages * STM32_PAGEERASE_TIMEOUT); + if (s_err != STM32_ERR_OK) { + return STM32_ERR_UNKNOWN; + } + return STM32_ERR_OK; + } + + /* extended erase */ + + // Free memory with RAII + auto buf = malloc_array_raii(2 + 2 * pages + 1); + + if (!buf) + return STM32_ERR_UNKNOWN; + + /* Number of pages to be erased - 1, two bytes, MSB first */ + uint8_t pg_byte = (pages - 1) >> 8; + buf[i++] = pg_byte; + cs ^= pg_byte; + pg_byte = (pages - 1) & 0xFF; + buf[i++] = pg_byte; + cs ^= pg_byte; + + for (auto pg_num = spage; pg_num < spage + pages; pg_num++) { + pg_byte = pg_num >> 8; + cs ^= pg_byte; + buf[i++] = pg_byte; + pg_byte = pg_num & 0xFF; + cs ^= pg_byte; + buf[i++] = pg_byte; + } + buf[i++] = cs; + stream->write_array(&buf[0], i); + stream->flush(); + + const auto s_err = stm32_get_ack_timeout(stm, pages * STM32_PAGEERASE_TIMEOUT); + if (s_err != STM32_ERR_OK) { + ESP_LOGD(TAG, "Page-by-page erase failed. Check the maximum pages your device supports."); + return STM32_ERR_UNKNOWN; + } + + return STM32_ERR_OK; +} + +template stm32_err_t stm32_check_ack_timeout(const stm32_err_t s_err, const T &&log) { + switch (s_err) { + case STM32_ERR_OK: + return STM32_ERR_OK; + case STM32_ERR_NACK: + log(); + // TODO: c++17 [[fallthrough]] + /* fallthrough */ + default: + return STM32_ERR_UNKNOWN; + } +} + +/* detect CPU endian */ +bool cpu_le() { + static constexpr int N = 1; + + // returns true if little endian + return *reinterpret_cast(&N) == 1; +} + +uint32_t le_u32(const uint32_t v) { + if (!cpu_le()) + return ((v & 0xFF000000) >> 24) | ((v & 0x00FF0000) >> 8) | ((v & 0x0000FF00) << 8) | ((v & 0x000000FF) << 24); + return v; +} + +template void populate_buffer_with_address(uint8_t (&buffer)[N], uint32_t address) { + buffer[0] = static_cast(address >> 24); + buffer[1] = static_cast((address >> 16) & 0xFF); + buffer[2] = static_cast((address >> 8) & 0xFF); + buffer[3] = static_cast(address & 0xFF); + buffer[4] = static_cast(buffer[0] ^ buffer[1] ^ buffer[2] ^ buffer[3]); +} + +} // Anonymous namespace + +} // namespace shelly_dimmer +} // namespace esphome + +namespace esphome { +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) { + 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}; + + if (!stm) { + return nullptr; + } + stm->stream = stream; + stm->flags = flags; + + stm->cmd = static_cast(malloc(sizeof(stm32_cmd_t))); // NOLINT + if (!stm->cmd) { + return 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 + } + + /* get the version and read protection status */ + if (stm32_send_command(stm.get(), STM32_CMD_GVR) != STM32_ERR_OK) { + return nullptr; // NOLINT + } + + /* 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 + 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; + } + } + + { + const auto len = ([&]() { + /* get the bootloader information */ + if (stm->cmd_get_reply) { + for (auto i = 0; stm->cmd_get_reply[i].length; ++i) { + if (stm->version == stm->cmd_get_reply[i].version) { + return stm->cmd_get_reply[i].length; + } + } + } + + return STM32_CMD_GET_LENGTH; + })(); + + if (stm32_guess_len_cmd(stm.get(), STM32_CMD_GET, buf, len) != STM32_ERR_OK) + return nullptr; + } + + const auto stop = buf[0] + 1; + stm->bl_version = buf[1]; + int new_cmds = 0; + for (auto i = 1; i < stop; ++i) { + const auto val = buf[i + 1]; + switch (val) { + case STM32_CMD_GET: + stm->cmd->get = val; + break; + case STM32_CMD_GVR: + stm->cmd->gvr = val; + break; + case STM32_CMD_GID: + stm->cmd->gid = val; + break; + case STM32_CMD_RM: + stm->cmd->rm = val; + break; + case STM32_CMD_GO: + stm->cmd->go = val; + break; + case STM32_CMD_WM: + case STM32_CMD_WM_NS: + stm->cmd->wm = newer(stm->cmd->wm, val); + break; + case STM32_CMD_ER: + case STM32_CMD_EE: + case STM32_CMD_EE_NS: + stm->cmd->er = newer(stm->cmd->er, val); + break; + case STM32_CMD_WP: + case STM32_CMD_WP_NS: + stm->cmd->wp = newer(stm->cmd->wp, val); + break; + case STM32_CMD_UW: + case STM32_CMD_UW_NS: + stm->cmd->uw = newer(stm->cmd->uw, val); + break; + case STM32_CMD_RP: + case STM32_CMD_RP_NS: + stm->cmd->rp = newer(stm->cmd->rp, val); + break; + case STM32_CMD_UR: + case STM32_CMD_UR_NS: + stm->cmd->ur = newer(stm->cmd->ur, val); + break; + case STM32_CMD_CRC: + stm->cmd->crc = newer(stm->cmd->crc, val); + break; + default: + if (new_cmds++ == 0) { + ESP_LOGD(TAG, "GET returns unknown commands (0x%2x", val); + } else { + ESP_LOGD(TAG, ", 0x%2x", val); + } + } + } + if (new_cmds) + ESP_LOGD(TAG, ")"); + if (stm32_get_ack(stm.get()) != STM32_ERR_OK) { + return 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; + } + + /* get the device ID */ + if (stm32_guess_len_cmd(stm.get(), stm->cmd->gid, buf, 1) != STM32_ERR_OK) { + return 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; + } + stm->pid = (buf[1] << 8) | buf[2]; + if (returned > 2) { + ESP_LOGD(TAG, "This bootloader returns %d extra bytes in PID:", returned); + for (auto i = 2; i <= returned; i++) + ESP_LOGD(TAG, " %02x", buf[i]); + } + if (stm32_get_ack(stm.get()) != STM32_ERR_OK) { + return nullptr; + } + + stm->dev = DEVICES; + while (stm->dev->id != 0x00 && stm->dev->id != stm->pid) + ++stm->dev; + + if (!stm->dev->id) { + ESP_LOGD(TAG, "Unknown/unsupported device (Device ID: 0x%03x)", stm->pid); + return nullptr; + } + + // TODO: Would be much better if the unique_ptr was returned from this function + // Release ownership of unique_ptr + return stm.release(); // NOLINT +} + +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) { + auto *const stream = stm->stream; + + if (!len) + return STM32_ERR_OK; + + if (len > 256) { + ESP_LOGD(TAG, "Error: READ length limit at 256 bytes"); + return STM32_ERR_UNKNOWN; + } + + if (stm->cmd->rm == STM32_CMD_ERR) { + ESP_LOGD(TAG, "Error: READ command not implemented in bootloader."); + return STM32_ERR_NO_CMD; + } + + if (stm32_send_command(stm, stm->cmd->rm) != STM32_ERR_OK) + return STM32_ERR_UNKNOWN; + + static constexpr auto BUFFER_SIZE = 5; + uint8_t buf[BUFFER_SIZE]; + populate_buffer_with_address(buf, address); + + stream->write_array(buf, BUFFER_SIZE); + stream->flush(); + + if (stm32_get_ack(stm) != STM32_ERR_OK) + return STM32_ERR_UNKNOWN; + + if (stm32_send_command(stm, len - 1) != STM32_ERR_OK) + return STM32_ERR_UNKNOWN; + + if (!stream->read_array(data, len)) + return STM32_ERR_UNKNOWN; + + 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) { + auto *const stream = stm->stream; + + if (!len) + return STM32_ERR_OK; + + if (len > 256) { + ESP_LOGD(TAG, "Error: READ length limit at 256 bytes"); + return STM32_ERR_UNKNOWN; + } + + /* must be 32bit aligned */ + if (address & 0x3) { + ESP_LOGD(TAG, "Error: WRITE address must be 4 byte aligned"); + return STM32_ERR_UNKNOWN; + } + + if (stm->cmd->wm == STM32_CMD_ERR) { + ESP_LOGD(TAG, "Error: WRITE command not implemented in bootloader."); + return STM32_ERR_NO_CMD; + } + + /* send the address and checksum */ + if (stm32_send_command(stm, stm->cmd->wm) != STM32_ERR_OK) + return STM32_ERR_UNKNOWN; + + static constexpr auto BUFFER_SIZE = 5; + uint8_t buf1[BUFFER_SIZE]; + populate_buffer_with_address(buf1, address); + + stream->write_array(buf1, BUFFER_SIZE); + stream->flush(); + if (stm32_get_ack(stm) != STM32_ERR_OK) + return STM32_ERR_UNKNOWN; + + const unsigned int aligned_len = (len + 3) & ~3; + uint8_t cs = aligned_len - 1; + uint8_t buf[256 + 2]; + + buf[0] = aligned_len - 1; + for (auto i = 0; i < len; i++) { + cs ^= data[i]; + buf[i + 1] = data[i]; + } + /* padding data */ + for (auto i = len; i < aligned_len; i++) { + cs ^= 0xFF; + buf[i + 1] = 0xFF; + } + buf[aligned_len + 1] = cs; + stream->write_array(buf, aligned_len + 2); + stream->flush(); + + const auto s_err = stm32_get_ack_timeout(stm, STM32_BLKWRITE_TIMEOUT); + if (s_err != STM32_ERR_OK) { + return STM32_ERR_UNKNOWN; + } + return STM32_ERR_OK; +} + +stm32_err_t stm32_wunprot_memory(const stm32_t *stm) { + if (stm->cmd->uw == STM32_CMD_ERR) { + ESP_LOGD(TAG, "Error: WRITE UNPROTECT command not implemented in bootloader."); + return STM32_ERR_NO_CMD; + } + + if (stm32_send_command(stm, stm->cmd->uw) != STM32_ERR_OK) + return STM32_ERR_UNKNOWN; + + return stm32_check_ack_timeout(stm32_get_ack_timeout(stm, STM32_WUNPROT_TIMEOUT), + []() { ESP_LOGD(TAG, "Error: Failed to WRITE UNPROTECT"); }); +} + +stm32_err_t stm32_wprot_memory(const stm32_t *stm) { + if (stm->cmd->wp == STM32_CMD_ERR) { + ESP_LOGD(TAG, "Error: WRITE PROTECT command not implemented in bootloader."); + return STM32_ERR_NO_CMD; + } + + if (stm32_send_command(stm, stm->cmd->wp) != STM32_ERR_OK) + return STM32_ERR_UNKNOWN; + + return stm32_check_ack_timeout(stm32_get_ack_timeout(stm, STM32_WPROT_TIMEOUT), + []() { ESP_LOGD(TAG, "Error: Failed to WRITE PROTECT"); }); +} + +stm32_err_t stm32_runprot_memory(const stm32_t *stm) { + if (stm->cmd->ur == STM32_CMD_ERR) { + ESP_LOGD(TAG, "Error: READOUT UNPROTECT command not implemented in bootloader."); + return STM32_ERR_NO_CMD; + } + + if (stm32_send_command(stm, stm->cmd->ur) != STM32_ERR_OK) + return STM32_ERR_UNKNOWN; + + return stm32_check_ack_timeout(stm32_get_ack_timeout(stm, STM32_MASSERASE_TIMEOUT), + []() { ESP_LOGD(TAG, "Error: Failed to READOUT UNPROTECT"); }); +} + +stm32_err_t stm32_readprot_memory(const stm32_t *stm) { + if (stm->cmd->rp == STM32_CMD_ERR) { + ESP_LOGD(TAG, "Error: READOUT PROTECT command not implemented in bootloader."); + return STM32_ERR_NO_CMD; + } + + if (stm32_send_command(stm, stm->cmd->rp) != STM32_ERR_OK) + return STM32_ERR_UNKNOWN; + + return stm32_check_ack_timeout(stm32_get_ack_timeout(stm, STM32_RPROT_TIMEOUT), + []() { ESP_LOGD(TAG, "Error: Failed to READOUT PROTECT"); }); +} + +stm32_err_t stm32_erase_memory(const stm32_t *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; + + if (stm->cmd->er == STM32_CMD_ERR) { + ESP_LOGD(TAG, "Error: ERASE command not implemented in bootloader."); + return STM32_ERR_NO_CMD; + } + + if (pages == STM32_MASS_ERASE) { + /* + * Not all chips support mass erase. + * Mass erase can be obtained executing a "readout protect" + * followed by "readout un-protect". This method is not + * suggested because can hang the target if a debug SWD/JTAG + * is connected. When the target enters in "readout + * protection" mode it will consider the debug connection as + * a tentative of intrusion and will hang. + * Erasing the flash page-by-page is the safer way to go. + */ + if (!(stm->dev->flags & F_NO_ME)) + return stm32_mass_erase(stm); + + pages = flash_addr_to_page_ceil(stm, stm->dev->fl_end); + } + + /* + * Some device, like STM32L152, cannot erase more than 512 pages in + * one command. Split the call. + */ + static constexpr uint32_t MAX_PAGE_SIZE = 512; + while (pages) { + const auto n = std::min(pages, MAX_PAGE_SIZE); + const auto s_err = stm32_pages_erase(stm, spage, n); + if (s_err != STM32_ERR_OK) + return s_err; + spage += n; + pages -= n; + } + return STM32_ERR_OK; +} + +static stm32_err_t stm32_run_raw_code(const stm32_t *stm, uint32_t target_address, const uint8_t *code, + uint32_t code_size) { + static constexpr uint32_t BUFFER_SIZE = 256; + + const auto stack_le = le_u32(0x20002000); + const auto code_address_le = le_u32(target_address + 8 + 1); // thumb mode address (!) + uint32_t length = code_size + 8; + + /* Must be 32-bit aligned */ + if (target_address & 0x3) { + ESP_LOGD(TAG, "Error: code address must be 4 byte aligned"); + return STM32_ERR_UNKNOWN; + } + + // Could be constexpr in c++17 + static const auto DELETOR = [](uint8_t *memory) { + free(memory); // NOLINT + }; + + // Free memory with RAII + std::unique_ptr mem{static_cast(malloc(length)), // NOLINT + DELETOR}; + + if (!mem) + return STM32_ERR_UNKNOWN; + + memcpy(mem.get(), &stack_le, sizeof(stack_le)); + memcpy(mem.get() + 4, &code_address_le, sizeof(code_address_le)); + memcpy(mem.get() + 8, code, code_size); + + auto *pos = mem.get(); + auto address = target_address; + while (length > 0) { + const auto w = std::min(length, BUFFER_SIZE); + if (stm32_write_memory(stm, address, pos, w) != STM32_ERR_OK) { + return STM32_ERR_UNKNOWN; + } + + address += w; + pos += w; + length -= w; + } + + return stm32_go(stm, target_address); +} + +stm32_err_t stm32_go(const stm32_t *stm, const uint32_t address) { + auto *const stream = stm->stream; + + if (stm->cmd->go == STM32_CMD_ERR) { + ESP_LOGD(TAG, "Error: GO command not implemented in bootloader."); + return STM32_ERR_NO_CMD; + } + + if (stm32_send_command(stm, stm->cmd->go) != STM32_ERR_OK) + return STM32_ERR_UNKNOWN; + + static constexpr auto BUFFER_SIZE = 5; + uint8_t buf[BUFFER_SIZE]; + populate_buffer_with_address(buf, address); + + stream->write_array(buf, BUFFER_SIZE); + stream->flush(); + + if (stm32_get_ack(stm) != STM32_ERR_OK) + return STM32_ERR_UNKNOWN; + return STM32_ERR_OK; +} + +stm32_err_t stm32_reset_device(const stm32_t *stm) { + const auto target_address = stm->dev->ram_start; + + if (stm->dev->flags & F_OBLL) { + /* set the OBL_LAUNCH bit to reset device (see RM0360, 2.5) */ + return stm32_run_raw_code(stm, target_address, STM_OBL_LAUNCH_CODE, STM_OBL_LAUNCH_CODE_SIZE); + } else { + return stm32_run_raw_code(stm, target_address, STM_RESET_CODE, STM_RESET_CODE_SIZE); + } +} + +stm32_err_t stm32_crc_memory(const stm32_t *stm, const uint32_t address, const uint32_t length, uint32_t *const crc) { + static constexpr auto BUFFER_SIZE = 5; + auto *const stream = stm->stream; + + if (address & 0x3 || length & 0x3) { + ESP_LOGD(TAG, "Start and end addresses must be 4 byte aligned"); + return STM32_ERR_UNKNOWN; + } + + if (stm->cmd->crc == STM32_CMD_ERR) { + ESP_LOGD(TAG, "Error: CRC command not implemented in bootloader."); + return STM32_ERR_NO_CMD; + } + + if (stm32_send_command(stm, stm->cmd->crc) != STM32_ERR_OK) + return STM32_ERR_UNKNOWN; + + { + static constexpr auto BUFFER_SIZE = 5; + uint8_t buf[BUFFER_SIZE]; + populate_buffer_with_address(buf, address); + + stream->write_array(buf, BUFFER_SIZE); + stream->flush(); + } + + if (stm32_get_ack(stm) != STM32_ERR_OK) + return STM32_ERR_UNKNOWN; + + { + static constexpr auto BUFFER_SIZE = 5; + uint8_t buf[BUFFER_SIZE]; + populate_buffer_with_address(buf, address); + + stream->write_array(buf, BUFFER_SIZE); + stream->flush(); + } + + if (stm32_get_ack(stm) != STM32_ERR_OK) + return STM32_ERR_UNKNOWN; + + if (stm32_get_ack(stm) != STM32_ERR_OK) + return STM32_ERR_UNKNOWN; + + { + uint8_t buf[BUFFER_SIZE]; + if (!stream->read_array(buf, BUFFER_SIZE)) + return STM32_ERR_UNKNOWN; + + if (buf[4] != (buf[0] ^ buf[1] ^ buf[2] ^ buf[3])) + return STM32_ERR_UNKNOWN; + + *crc = (buf[0] << 24) | (buf[1] << 16) | (buf[2] << 8) | buf[3]; + } + + return STM32_ERR_OK; +} + +/* + * CRC computed by STM32 is similar to the standard crc32_be() + * implemented, for example, in Linux kernel in ./lib/crc32.c + * But STM32 computes it on units of 32 bits word and swaps the + * bytes of the word before the computation. + * Due to byte swap, I cannot use any CRC available in existing + * libraries, so here is a simple not optimized implementation. + */ +uint32_t stm32_sw_crc(uint32_t crc, uint8_t *buf, unsigned int len) { + static constexpr uint32_t CRCPOLY_BE = 0x04c11db7; + static constexpr uint32_t CRC_MSBMASK = 0x80000000; + + if (len & 0x3) { + ESP_LOGD(TAG, "Buffer length must be multiple of 4 bytes"); + return 0; + } + + while (len) { + uint32_t data = *buf++; + data |= *buf++ << 8; + data |= *buf++ << 16; + data |= *buf++ << 24; + len -= 4; + + crc ^= data; + + for (size_t i = 0; i < 32; ++i) { + if (crc & CRC_MSBMASK) { + crc = (crc << 1) ^ CRCPOLY_BE; + } else { + crc = (crc << 1); + } + } + } + return crc; +} + +stm32_err_t stm32_crc_wrapper(const stm32_t *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; + + uint8_t buf[BUFFER_SIZE]; + + if (address & 0x3 || length & 0x3) { + ESP_LOGD(TAG, "Start and end addresses must be 4 byte aligned"); + return STM32_ERR_UNKNOWN; + } + + if (stm->cmd->crc != STM32_CMD_ERR) + return stm32_crc_memory(stm, address, length, crc); + + const auto start = address; + const auto total_len = length; + uint32_t current_crc = CRC_INIT_VALUE; + while (length) { + const auto len = std::min(BUFFER_SIZE, length); + if (stm32_read_memory(stm, address, buf, len) != STM32_ERR_OK) { + ESP_LOGD(TAG, "Failed to read memory at address 0x%08x, target write-protected?", address); + return STM32_ERR_UNKNOWN; + } + current_crc = stm32_sw_crc(current_crc, buf, len); + length -= len; + address += len; + + ESP_LOGD(TAG, "\rCRC address 0x%08x (%.2f%%) ", address, (100.0f / (float) total_len) * (float) (address - start)); + } + ESP_LOGD(TAG, "Done."); + *crc = current_crc; + return STM32_ERR_OK; +} + +} // namespace shelly_dimmer +} // namespace esphome +#endif diff --git a/esphome/components/shelly_dimmer/stm32flash.h b/esphome/components/shelly_dimmer/stm32flash.h new file mode 100644 index 0000000000..c561375c38 --- /dev/null +++ b/esphome/components/shelly_dimmer/stm32flash.h @@ -0,0 +1,129 @@ +/* + stm32flash - Open Source ST STM32 flash program for Arduino + Copyright (C) 2010 Geoffrey McRae + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +*/ + +#pragma once + +#include "esphome/core/defines.h" +#ifdef USE_SHD_FIRMWARE_DATA + +#include +#include "esphome/components/uart/uart.h" + +namespace esphome { +namespace shelly_dimmer { + +/* flags */ +constexpr auto STREAM_OPT_BYTE = (1 << 0); /* byte (not frame) oriented */ +constexpr auto STREAM_OPT_GVR_ETX = (1 << 1); /* cmd GVR returns protection status */ +constexpr auto STREAM_OPT_CMD_INIT = (1 << 2); /* use INIT cmd to autodetect speed */ +constexpr auto STREAM_OPT_RETRY = (1 << 3); /* allowed read() retry after timeout */ +constexpr auto STREAM_OPT_I2C = (1 << 4); /* i2c */ +constexpr auto STREAM_OPT_STRETCH_W = (1 << 5); /* warning for no-stretching commands */ + +constexpr auto STREAM_SERIAL = (STREAM_OPT_BYTE | STREAM_OPT_GVR_ETX | STREAM_OPT_CMD_INIT | STREAM_OPT_RETRY); +constexpr auto STREAM_I2C = (STREAM_OPT_I2C | STREAM_OPT_STRETCH_W); + +constexpr auto STM32_MAX_RX_FRAME = 256; /* cmd read memory */ +constexpr auto STM32_MAX_TX_FRAME = (1 + 256 + 1); /* cmd write memory */ + +constexpr auto STM32_MAX_PAGES = 0x0000ffff; +constexpr auto STM32_MASS_ERASE = 0x00100000; /* > 2 x max_pages */ + +using stm32_err_t = enum Stm32Err { + STM32_ERR_OK = 0, + STM32_ERR_UNKNOWN, /* Generic error */ + STM32_ERR_NACK, + STM32_ERR_NO_CMD, /* Command not available in bootloader */ +}; + +using flags_t = enum Flags { + F_NO_ME = 1 << 0, /* Mass-Erase not supported */ + F_OBLL = 1 << 1, /* OBL_LAUNCH required */ +}; + +using stm32_cmd_t = struct Stm32Cmd { + uint8_t get; + uint8_t gvr; + uint8_t gid; + uint8_t rm; + uint8_t go; + uint8_t wm; + uint8_t er; /* this may be extended erase */ + uint8_t wp; + uint8_t uw; + uint8_t rp; + uint8_t ur; + uint8_t crc; +}; + +using stm32_dev_t = struct Stm32Dev { // NOLINT + const uint16_t id; + const char *name; + const uint32_t ram_start, ram_end; + const uint32_t fl_start, fl_end; + const uint16_t fl_pps; // pages per sector + const uint32_t *fl_ps; // page size + const uint32_t opt_start, opt_end; + const uint32_t mem_start, mem_end; + const uint32_t flags; +}; + +using stm32_t = struct Stm32 { + uart::UARTDevice *stream; + uint8_t flags; + struct VarlenCmd *cmd_get_reply; + uint8_t bl_version; + uint8_t version; + uint8_t option1, option2; + uint16_t pid; + stm32_cmd_t *cmd; + const stm32_dev_t *dev; +}; + +/* + * Specify the length of reply for command GET + * This is helpful for frame-oriented protocols, e.g. i2c, to avoid time + * consuming try-fail-timeout-retry operation. + * On byte-oriented protocols, i.e. UART, this information would be skipped + * after read the first byte, so not needed. + */ +struct VarlenCmd { + uint8_t version; + 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); +uint32_t stm32_sw_crc(uint32_t crc, uint8_t *buf, unsigned int len); + +} // namespace shelly_dimmer +} // namespace esphome + +#endif // USE_SHD_FIRMWARE_DATA diff --git a/esphome/core/defines.h b/esphome/core/defines.h index f304f847a5..c854e2b987 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -93,3 +93,9 @@ //#define USE_BSEC // Requires a library with proprietary license. #define USE_DASHBOARD_IMPORT + +// Dummy firmware payload for shelly_dimmer +#define USE_SHD_FIRMWARE_MAJOR_VERSION 56 +#define USE_SHD_FIRMWARE_MINOR_VERSION 5 +#define USE_SHD_FIRMWARE_DATA \ + {} diff --git a/tests/test1.yaml b/tests/test1.yaml index 77c4a76bda..98a3ffcf4b 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -1751,6 +1751,18 @@ light: to: 25 - single_light_id: ${roomname}_lights + - platform: shelly_dimmer + name: "Shelly Dimmer Light" + power: + name: "Shelly Dimmer Power" + voltage: + name: "Shelly Dimmer Voltage" + current: + name: "Shelly Dimmer Current" + max_brightness: 500 + firmware: "51.6" + uart_id: uart0 + remote_transmitter: - pin: 32 carrier_duty_percent: 100% From 6bac551d9fb10731841e7270840fe45015a66bb3 Mon Sep 17 00:00:00 2001 From: Joe Date: Wed, 13 Apr 2022 21:16:13 -0400 Subject: [PATCH 05/56] Add BedJet BLE climate component (#2452) --- CODEOWNERS | 1 + esphome/components/bedjet/__init__.py | 1 + esphome/components/bedjet/bedjet.cpp | 642 ++++++++++++++++++++++ esphome/components/bedjet/bedjet.h | 121 ++++ esphome/components/bedjet/bedjet_base.cpp | 123 +++++ esphome/components/bedjet/bedjet_base.h | 159 ++++++ esphome/components/bedjet/bedjet_const.h | 78 +++ esphome/components/bedjet/climate.py | 42 ++ tests/test1.yaml | 5 + 9 files changed, 1172 insertions(+) create mode 100644 esphome/components/bedjet/__init__.py create mode 100644 esphome/components/bedjet/bedjet.cpp create mode 100644 esphome/components/bedjet/bedjet.h create mode 100644 esphome/components/bedjet/bedjet_base.cpp create mode 100644 esphome/components/bedjet/bedjet_base.h create mode 100644 esphome/components/bedjet/bedjet_const.h create mode 100644 esphome/components/bedjet/climate.py diff --git a/CODEOWNERS b/CODEOWNERS index 02945ec0a4..c9df669f03 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -28,6 +28,7 @@ esphome/components/atc_mithermometer/* @ahpohl esphome/components/b_parasite/* @rbaron esphome/components/ballu/* @bazuchan esphome/components/bang_bang/* @OttoWinter +esphome/components/bedjet/* @jhansche esphome/components/bh1750/* @OttoWinter esphome/components/binary_sensor/* @esphome/core esphome/components/bl0940/* @tobias- diff --git a/esphome/components/bedjet/__init__.py b/esphome/components/bedjet/__init__.py new file mode 100644 index 0000000000..16821fc016 --- /dev/null +++ b/esphome/components/bedjet/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@jhansche"] diff --git a/esphome/components/bedjet/bedjet.cpp b/esphome/components/bedjet/bedjet.cpp new file mode 100644 index 0000000000..1a932da0c5 --- /dev/null +++ b/esphome/components/bedjet/bedjet.cpp @@ -0,0 +1,642 @@ +#include "bedjet.h" +#include "esphome/core/log.h" + +#ifdef USE_ESP32 + +namespace esphome { +namespace bedjet { + +using namespace esphome::climate; + +/// Converts a BedJet temp step into degrees Celsius. +float bedjet_temp_to_c(const uint8_t temp) { + // BedJet temp is "C*2"; to get C, divide by 2. + return temp / 2.0f; +} + +/// Converts a BedJet fan step to a speed percentage, in the range of 5% to 100%. +uint8_t bedjet_fan_step_to_speed(const uint8_t fan) { + // 0 = 5% + // 19 = 100% + return 5 * fan + 5; +} + +static const std::string *bedjet_fan_step_to_fan_mode(const uint8_t fan_step) { + if (fan_step >= 0 && fan_step <= 19) + return &BEDJET_FAN_STEP_NAME_STRINGS[fan_step]; + return nullptr; +} + +static uint8_t bedjet_fan_speed_to_step(const std::string &fan_step_percent) { + for (int i = 0; i < sizeof(BEDJET_FAN_STEP_NAME_STRINGS); i++) { + if (fan_step_percent == BEDJET_FAN_STEP_NAME_STRINGS[i]) { + return i; + } + } + return -1; +} + +void Bedjet::upgrade_firmware() { + auto *pkt = this->codec_->get_button_request(MAGIC_UPDATE); + auto status = this->write_bedjet_packet_(pkt); + + if (status) { + ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status); + } +} + +void Bedjet::dump_config() { + LOG_CLIMATE("", "BedJet Climate", this); + auto traits = this->get_traits(); + + ESP_LOGCONFIG(TAG, " Supported modes:"); + for (auto mode : traits.get_supported_modes()) { + ESP_LOGCONFIG(TAG, " - %s", LOG_STR_ARG(climate_mode_to_string(mode))); + } + + ESP_LOGCONFIG(TAG, " Supported fan modes:"); + for (const auto &mode : traits.get_supported_fan_modes()) { + ESP_LOGCONFIG(TAG, " - %s", LOG_STR_ARG(climate_fan_mode_to_string(mode))); + } + for (const auto &mode : traits.get_supported_custom_fan_modes()) { + ESP_LOGCONFIG(TAG, " - %s (c)", mode.c_str()); + } + + ESP_LOGCONFIG(TAG, " Supported presets:"); + for (auto preset : traits.get_supported_presets()) { + ESP_LOGCONFIG(TAG, " - %s", LOG_STR_ARG(climate_preset_to_string(preset))); + } + for (const auto &preset : traits.get_supported_custom_presets()) { + ESP_LOGCONFIG(TAG, " - %s (c)", preset.c_str()); + } +} + +void Bedjet::setup() { + this->codec_ = make_unique(); + + // restore set points + auto restore = this->restore_state_(); + if (restore.has_value()) { + ESP_LOGI(TAG, "Restored previous saved state."); + restore->apply(this); + } else { + // Initial status is unknown until we connect + this->reset_state_(); + } + +#ifdef USE_TIME + this->setup_time_(); +#endif +} + +/** Resets states to defaults. */ +void Bedjet::reset_state_() { + this->mode = climate::CLIMATE_MODE_OFF; + this->action = climate::CLIMATE_ACTION_IDLE; + this->target_temperature = NAN; + this->current_temperature = NAN; + this->preset.reset(); + this->custom_preset.reset(); + this->publish_state(); +} + +void Bedjet::loop() {} + +void Bedjet::control(const ClimateCall &call) { + ESP_LOGD(TAG, "Received Bedjet::control"); + if (this->node_state != espbt::ClientState::ESTABLISHED) { + ESP_LOGW(TAG, "Not connected, cannot handle control call yet."); + return; + } + + if (call.get_mode().has_value()) { + ClimateMode mode = *call.get_mode(); + BedjetPacket *pkt; + switch (mode) { + case climate::CLIMATE_MODE_OFF: + pkt = this->codec_->get_button_request(BTN_OFF); + break; + case climate::CLIMATE_MODE_HEAT: + pkt = this->codec_->get_button_request(BTN_EXTHT); + break; + case climate::CLIMATE_MODE_FAN_ONLY: + pkt = this->codec_->get_button_request(BTN_COOL); + break; + case climate::CLIMATE_MODE_DRY: + pkt = this->codec_->get_button_request(BTN_DRY); + break; + default: + ESP_LOGW(TAG, "Unsupported mode: %d", mode); + return; + } + + auto status = this->write_bedjet_packet_(pkt); + + if (status) { + ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status); + } 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 + this->custom_preset.reset(); + this->preset.reset(); + } + } + + if (call.get_target_temperature().has_value()) { + auto target_temp = *call.get_target_temperature(); + auto *pkt = this->codec_->get_set_target_temp_request(target_temp); + auto status = this->write_bedjet_packet_(pkt); + + if (status) { + ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status); + } else { + this->target_temperature = target_temp; + } + } + + if (call.get_preset().has_value()) { + ClimatePreset preset = *call.get_preset(); + BedjetPacket *pkt; + + if (preset == climate::CLIMATE_PRESET_BOOST) { + pkt = this->codec_->get_button_request(BTN_TURBO); + } else { + ESP_LOGW(TAG, "Unsupported preset: %d", preset); + return; + } + + auto status = this->write_bedjet_packet_(pkt); + if (status) { + ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status); + } else { + // We use BOOST preset for TURBO mode, which is a short-lived/high-heat mode. + this->mode = climate::CLIMATE_MODE_HEAT; + this->preset = preset; + this->custom_preset.reset(); + this->force_refresh_ = true; + } + } else if (call.get_custom_preset().has_value()) { + std::string preset = *call.get_custom_preset(); + BedjetPacket *pkt; + + if (preset == "M1") { + pkt = this->codec_->get_button_request(BTN_M1); + } else if (preset == "M2") { + pkt = this->codec_->get_button_request(BTN_M2); + } else if (preset == "M3") { + pkt = this->codec_->get_button_request(BTN_M3); + } else { + ESP_LOGW(TAG, "Unsupported preset: %s", preset.c_str()); + return; + } + + auto status = this->write_bedjet_packet_(pkt); + if (status) { + ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status); + } else { + this->force_refresh_ = true; + this->custom_preset = preset; + this->preset.reset(); + } + } + + if (call.get_fan_mode().has_value()) { + // Climate fan mode only supports low/med/high, but the BedJet supports 5-100% increments. + // We can still support a ClimateCall that requests low/med/high, and just translate it to a step increment here. + auto fan_mode = *call.get_fan_mode(); + BedjetPacket *pkt; + if (fan_mode == climate::CLIMATE_FAN_LOW) { + pkt = this->codec_->get_set_fan_speed_request(3 /* = 20% */); + } else if (fan_mode == climate::CLIMATE_FAN_MEDIUM) { + pkt = this->codec_->get_set_fan_speed_request(9 /* = 50% */); + } else if (fan_mode == climate::CLIMATE_FAN_HIGH) { + pkt = this->codec_->get_set_fan_speed_request(14 /* = 75% */); + } else { + ESP_LOGW(TAG, "[%s] Unsupported fan mode: %s", this->get_name().c_str(), + LOG_STR_ARG(climate_fan_mode_to_string(fan_mode))); + return; + } + + auto status = this->write_bedjet_packet_(pkt); + if (status) { + ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status); + } else { + this->force_refresh_ = true; + } + } else if (call.get_custom_fan_mode().has_value()) { + auto fan_mode = *call.get_custom_fan_mode(); + auto fan_step = bedjet_fan_speed_to_step(fan_mode); + if (fan_step >= 0 && fan_step <= 19) { + ESP_LOGV(TAG, "[%s] Converted fan mode %s to bedjet fan step %d", this->get_name().c_str(), fan_mode.c_str(), + fan_step); + // The index should represent the fan_step index. + BedjetPacket *pkt = this->codec_->get_set_fan_speed_request(fan_step); + auto status = this->write_bedjet_packet_(pkt); + if (status) { + ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status); + } else { + this->force_refresh_ = true; + } + } + } +} + +void Bedjet::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) { + switch (event) { + case ESP_GATTC_DISCONNECT_EVT: { + ESP_LOGV(TAG, "Disconnected: reason=%d", param->disconnect.reason); + this->status_set_warning(); + break; + } + case ESP_GATTC_SEARCH_CMPL_EVT: { + auto *chr = this->parent_->get_characteristic(BEDJET_SERVICE_UUID, BEDJET_COMMAND_UUID); + if (chr == nullptr) { + ESP_LOGW(TAG, "[%s] No control service found at device, not a BedJet..?", this->get_name().c_str()); + break; + } + this->char_handle_cmd_ = chr->handle; + + chr = this->parent_->get_characteristic(BEDJET_SERVICE_UUID, BEDJET_STATUS_UUID); + if (chr == nullptr) { + ESP_LOGW(TAG, "[%s] No status service found at device, not a BedJet..?", this->get_name().c_str()); + break; + } + + this->char_handle_status_ = chr->handle; + // We also need to obtain the config descriptor for this handle. + // Otherwise once we set node_state=Established, the parent will flush all handles/descriptors, and we won't be + // able to look it up. + auto *descr = this->parent_->get_config_descriptor(this->char_handle_status_); + if (descr == nullptr) { + ESP_LOGW(TAG, "No config descriptor for status handle 0x%x. Will not be able to receive status notifications", + this->char_handle_status_); + } else if (descr->uuid.get_uuid().len != ESP_UUID_LEN_16 || + descr->uuid.get_uuid().uuid.uuid16 != ESP_GATT_UUID_CHAR_CLIENT_CONFIG) { + ESP_LOGW(TAG, "Config descriptor 0x%x (uuid %s) is not a client config char uuid", this->char_handle_status_, + descr->uuid.to_string().c_str()); + } else { + this->config_descr_status_ = descr->handle; + } + + chr = this->parent_->get_characteristic(BEDJET_SERVICE_UUID, BEDJET_NAME_UUID); + if (chr != nullptr) { + this->char_handle_name_ = chr->handle; + auto status = esp_ble_gattc_read_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_name_, + ESP_GATT_AUTH_REQ_NONE); + if (status) { + ESP_LOGI(TAG, "[%s] Unable to read name characteristic: %d", this->get_name().c_str(), status); + } + } + + ESP_LOGD(TAG, "Services complete: obtained char handles."); + this->node_state = espbt::ClientState::ESTABLISHED; + + this->set_notify_(true); + +#ifdef USE_TIME + if (this->time_id_.has_value()) { + this->send_local_time_(); + } +#endif + break; + } + case ESP_GATTC_WRITE_DESCR_EVT: { + if (param->write.status != ESP_GATT_OK) { + // ESP_GATT_INVALID_ATTR_LEN + ESP_LOGW(TAG, "Error writing descr at handle 0x%04d, status=%d", param->write.handle, param->write.status); + break; + } + // [16:44:44][V][bedjet:279]: [JOENJET] Register for notify event success: h=0x002a s=0 + // This might be the enable-notify descriptor? (or disable-notify) + ESP_LOGV(TAG, "[%s] Write to handle 0x%04x status=%d", this->get_name().c_str(), param->write.handle, + param->write.status); + break; + } + case ESP_GATTC_WRITE_CHAR_EVT: { + if (param->write.status != ESP_GATT_OK) { + ESP_LOGW(TAG, "Error writing char at handle 0x%04d, status=%d", param->write.handle, param->write.status); + break; + } + if (param->write.handle == this->char_handle_cmd_) { + if (this->force_refresh_) { + // Command write was successful. Publish the pending state, hoping that notify will kick in. + this->publish_state(); + } + } + break; + } + case ESP_GATTC_READ_CHAR_EVT: { + if (param->read.conn_id != this->parent_->conn_id) + break; + if (param->read.status != ESP_GATT_OK) { + ESP_LOGW(TAG, "Error reading char at handle %d, status=%d", param->read.handle, param->read.status); + break; + } + if (param->read.handle == this->char_handle_status_) { + // This is the additional packet that doesn't fit in the notify packet. + this->codec_->decode_extra(param->read.value, param->read.value_len); + } else if (param->read.handle == this->char_handle_name_) { + // The data should represent the name. + if (param->read.status == ESP_GATT_OK && param->read.value_len > 0) { + std::string bedjet_name(reinterpret_cast(param->read.value), param->read.value_len); + // this->set_name(bedjet_name); + ESP_LOGV(TAG, "[%s] Got BedJet name: '%s'", this->get_name().c_str(), bedjet_name.c_str()); + } + } + break; + } + case ESP_GATTC_REG_FOR_NOTIFY_EVT: { + // This event means that ESP received the request to enable notifications on the client side. But we also have to + // tell the server that we want it to send notifications. Normally BLEClient parent would handle this + // automatically, but as soon as we set our status to Established, the parent is going to purge all the + // service/char/descriptor handles, and then get_config_descriptor() won't work anymore. There's no way to disable + // the BLEClient parent behavior, so our only option is to write the handle anyway, and hope a double-write + // doesn't break anything. + + if (param->reg_for_notify.handle != this->char_handle_status_) { + ESP_LOGW(TAG, "[%s] Register for notify on unexpected handle 0x%04x, expecting 0x%04x", + this->get_name().c_str(), param->reg_for_notify.handle, this->char_handle_status_); + break; + } + + this->write_notify_config_descriptor_(true); + this->last_notify_ = 0; + this->force_refresh_ = true; + break; + } + case ESP_GATTC_UNREG_FOR_NOTIFY_EVT: { + // This event is not handled by the parent BLEClient, so we need to do this either way. + if (param->unreg_for_notify.handle != this->char_handle_status_) { + ESP_LOGW(TAG, "[%s] Unregister for notify on unexpected handle 0x%04x, expecting 0x%04x", + this->get_name().c_str(), param->unreg_for_notify.handle, this->char_handle_status_); + break; + } + + this->write_notify_config_descriptor_(false); + this->last_notify_ = 0; + // Now we wait until the next update() poll to re-register notify... + break; + } + case ESP_GATTC_NOTIFY_EVT: { + if (param->notify.handle != this->char_handle_status_) { + ESP_LOGW(TAG, "[%s] Unexpected notify handle, wanted %04X, got %04X", this->get_name().c_str(), + this->char_handle_status_, param->notify.handle); + break; + } + + // FIXME: notify events come in every ~200-300 ms, which is too fast to be helpful. So we + // throttle the updates to once every MIN_NOTIFY_THROTTLE (5 seconds). + // Another idea would be to keep notify off by default, and use update() as an opportunity to turn on + // notify to get enough data to update status, then turn off notify again. + + uint32_t now = millis(); + auto delta = now - this->last_notify_; + + if (this->last_notify_ == 0 || delta > MIN_NOTIFY_THROTTLE || this->force_refresh_) { + bool needs_extra = this->codec_->decode_notify(param->notify.value, param->notify.value_len); + this->last_notify_ = now; + + if (needs_extra) { + // this means the packet was partial, so read the status characteristic to get the second part. + auto status = esp_ble_gattc_read_char(this->parent_->gattc_if, this->parent_->conn_id, + this->char_handle_status_, ESP_GATT_AUTH_REQ_NONE); + if (status) { + ESP_LOGI(TAG, "[%s] Unable to read extended status packet", this->get_name().c_str()); + } + } + + if (this->force_refresh_) { + // If we requested an immediate update, do that now. + this->update(); + this->force_refresh_ = false; + } + } + break; + } + default: + ESP_LOGVV(TAG, "[%s] gattc unhandled event: enum=%d", this->get_name().c_str(), event); + break; + } +} + +/** Reimplementation of BLEClient.gattc_event_handler() for ESP_GATTC_REG_FOR_NOTIFY_EVT. + * + * This is a copy of ble_client's automatic handling of `ESP_GATTC_REG_FOR_NOTIFY_EVT`, in order + * to undo the same on unregister. It also allows us to maintain the config descriptor separately, + * since the parent BLEClient is going to purge all descriptors once we set our connection status + * to `Established`. + */ +uint8_t Bedjet::write_notify_config_descriptor_(bool enable) { + auto handle = this->config_descr_status_; + if (handle == 0) { + ESP_LOGW(TAG, "No descriptor found for notify of handle 0x%x", this->char_handle_status_); + return -1; + } + + // NOTE: BLEClient uses `uint8_t*` of length 1, but BLE spec requires 16 bits. + uint8_t notify_en[] = {0, 0}; + notify_en[0] = enable; + auto status = + esp_ble_gattc_write_char_descr(this->parent_->gattc_if, this->parent_->conn_id, handle, sizeof(notify_en), + ¬ify_en[0], ESP_GATT_WRITE_TYPE_RSP, ESP_GATT_AUTH_REQ_NONE); + if (status) { + ESP_LOGW(TAG, "esp_ble_gattc_write_char_descr error, status=%d", status); + return status; + } + ESP_LOGD(TAG, "[%s] wrote notify=%s to status config 0x%04x", this->get_name().c_str(), enable ? "true" : "false", + handle); + return ESP_GATT_OK; +} + +#ifdef USE_TIME +/** Attempts to sync the local time (via `time_id`) to the BedJet device. */ +void Bedjet::send_local_time_() { + if (this->node_state != espbt::ClientState::ESTABLISHED) { + ESP_LOGV(TAG, "[%s] Not connected, cannot send time.", this->get_name().c_str()); + return; + } + auto *time_id = *this->time_id_; + time::ESPTime now = time_id->now(); + if (now.is_valid()) { + uint8_t hour = now.hour; + uint8_t minute = now.minute; + BedjetPacket *pkt = this->codec_->get_set_time_request(hour, minute); + auto status = this->write_bedjet_packet_(pkt); + if (status) { + ESP_LOGW(TAG, "Failed setting BedJet clock: %d", status); + } else { + ESP_LOGD(TAG, "[%s] BedJet clock set to: %d:%02d", this->get_name().c_str(), hour, minute); + } + } +} + +/** Initializes time sync callbacks to support syncing current time to the BedJet. */ +void Bedjet::setup_time_() { + if (this->time_id_.has_value()) { + this->send_local_time_(); + auto *time_id = *this->time_id_; + time_id->add_on_time_sync_callback([this] { this->send_local_time_(); }); + time::ESPTime now = time_id->now(); + ESP_LOGD(TAG, "Using time component to set BedJet clock: %d:%02d", now.hour, now.minute); + } else { + ESP_LOGI(TAG, "`time_id` is not configured: will not sync BedJet clock."); + } +} +#endif + +/** Writes one BedjetPacket to the BLE client on the BEDJET_COMMAND_UUID. */ +uint8_t Bedjet::write_bedjet_packet_(BedjetPacket *pkt) { + if (this->node_state != espbt::ClientState::ESTABLISHED) { + if (!this->parent_->enabled) { + ESP_LOGI(TAG, "[%s] Cannot write packet: Not connected, enabled=false", this->get_name().c_str()); + } else { + ESP_LOGW(TAG, "[%s] Cannot write packet: Not connected", this->get_name().c_str()); + } + return -1; + } + auto status = esp_ble_gattc_write_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_cmd_, + pkt->data_length + 1, (uint8_t *) &pkt->command, ESP_GATT_WRITE_TYPE_NO_RSP, + ESP_GATT_AUTH_REQ_NONE); + return status; +} + +/** Configures the local ESP BLE client to register (`true`) or unregister (`false`) for status notifications. */ +uint8_t Bedjet::set_notify_(const bool enable) { + uint8_t status; + if (enable) { + status = esp_ble_gattc_register_for_notify(this->parent_->gattc_if, this->parent_->remote_bda, + this->char_handle_status_); + if (status) { + ESP_LOGW(TAG, "[%s] esp_ble_gattc_register_for_notify failed, status=%d", this->get_name().c_str(), status); + } + } else { + status = esp_ble_gattc_unregister_for_notify(this->parent_->gattc_if, this->parent_->remote_bda, + this->char_handle_status_); + if (status) { + ESP_LOGW(TAG, "[%s] esp_ble_gattc_unregister_for_notify failed, status=%d", this->get_name().c_str(), status); + } + } + ESP_LOGV(TAG, "[%s] set_notify: enable=%d; result=%d", this->get_name().c_str(), enable, status); + return status; +} + +/** Attempts to update the climate device from the last received BedjetStatusPacket. + * + * @return `true` if the status has been applied; `false` if there is nothing to apply. + */ +bool Bedjet::update_status_() { + if (!this->codec_->has_status()) + return false; + + BedjetStatusPacket status = *this->codec_->get_status_packet(); + + auto converted_temp = bedjet_temp_to_c(status.target_temp_step); + if (converted_temp > 0) + this->target_temperature = converted_temp; + converted_temp = bedjet_temp_to_c(status.ambient_temp_step); + if (converted_temp > 0) + this->current_temperature = converted_temp; + + const auto *fan_mode_name = bedjet_fan_step_to_fan_mode(status.fan_step); + if (fan_mode_name != nullptr) { + this->custom_fan_mode = *fan_mode_name; + } + + // TODO: Get biorhythm data to determine which preset (M1-3) is running, if any. + switch (status.mode) { + case MODE_WAIT: // Biorhythm "wait" step: device is idle + case MODE_STANDBY: + this->mode = climate::CLIMATE_MODE_OFF; + this->action = climate::CLIMATE_ACTION_IDLE; + this->fan_mode = climate::CLIMATE_FAN_OFF; + this->custom_preset.reset(); + this->preset.reset(); + break; + + case MODE_HEAT: + case MODE_EXTHT: + this->mode = climate::CLIMATE_MODE_HEAT; + this->action = climate::CLIMATE_ACTION_HEATING; + this->custom_preset.reset(); + this->preset.reset(); + break; + + case MODE_COOL: + this->mode = climate::CLIMATE_MODE_FAN_ONLY; + this->action = climate::CLIMATE_ACTION_COOLING; + this->custom_preset.reset(); + this->preset.reset(); + break; + + case MODE_DRY: + this->mode = climate::CLIMATE_MODE_DRY; + this->action = climate::CLIMATE_ACTION_DRYING; + this->custom_preset.reset(); + this->preset.reset(); + break; + + case MODE_TURBO: + this->preset = climate::CLIMATE_PRESET_BOOST; + this->custom_preset.reset(); + this->mode = climate::CLIMATE_MODE_HEAT; + this->action = climate::CLIMATE_ACTION_HEATING; + break; + + default: + ESP_LOGW(TAG, "[%s] Unexpected mode: 0x%02X", this->get_name().c_str(), status.mode); + break; + } + + if (this->is_valid_()) { + this->publish_state(); + this->codec_->clear_status(); + this->status_clear_warning(); + } + + return true; +} + +void Bedjet::update() { + ESP_LOGV(TAG, "[%s] update()", this->get_name().c_str()); + + if (this->node_state != espbt::ClientState::ESTABLISHED) { + if (!this->parent()->enabled) { + ESP_LOGD(TAG, "[%s] Not connected, because enabled=false", this->get_name().c_str()); + } else { + // Possibly still trying to connect. + ESP_LOGD(TAG, "[%s] Not connected, enabled=true", this->get_name().c_str()); + } + + return; + } + + auto result = this->update_status_(); + if (!result) { + uint32_t now = millis(); + uint32_t diff = now - this->last_notify_; + + if (this->last_notify_ == 0) { + // This means we're connected and haven't received a notification, so it likely means that the BedJet is off. + // However, it could also mean that it's running, but failing to send notifications. + // We can try to unregister for notifications now, and then re-register, hoping to clear it up... + // But how do we know for sure which state we're in, and how do we actually clear out the buggy state? + + ESP_LOGI(TAG, "[%s] Still waiting for first GATT notify event.", this->get_name().c_str()); + this->set_notify_(false); + } else if (diff > NOTIFY_WARN_THRESHOLD) { + ESP_LOGW(TAG, "[%s] Last GATT notify was %d seconds ago.", this->get_name().c_str(), diff / 1000); + } + + if (this->timeout_ > 0 && diff > this->timeout_ && this->parent()->enabled) { + ESP_LOGW(TAG, "[%s] Timed out after %d sec. Retrying...", this->get_name().c_str(), this->timeout_); + this->parent()->set_enabled(false); + this->parent()->set_enabled(true); + } + } +} + +} // namespace bedjet +} // namespace esphome + +#endif diff --git a/esphome/components/bedjet/bedjet.h b/esphome/components/bedjet/bedjet.h new file mode 100644 index 0000000000..b061d2b5ec --- /dev/null +++ b/esphome/components/bedjet/bedjet.h @@ -0,0 +1,121 @@ +#pragma once + +#include "esphome/components/ble_client/ble_client.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "esphome/components/climate/climate.h" +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "bedjet_base.h" + +#ifdef USE_TIME +#include "esphome/components/time/real_time_clock.h" +#endif + +#ifdef USE_ESP32 + +#include + +namespace esphome { +namespace bedjet { + +namespace espbt = esphome::esp32_ble_tracker; + +static const espbt::ESPBTUUID BEDJET_SERVICE_UUID = espbt::ESPBTUUID::from_raw("00001000-bed0-0080-aa55-4265644a6574"); +static const espbt::ESPBTUUID BEDJET_STATUS_UUID = espbt::ESPBTUUID::from_raw("00002000-bed0-0080-aa55-4265644a6574"); +static const espbt::ESPBTUUID BEDJET_COMMAND_UUID = espbt::ESPBTUUID::from_raw("00002004-bed0-0080-aa55-4265644a6574"); +static const espbt::ESPBTUUID BEDJET_NAME_UUID = espbt::ESPBTUUID::from_raw("00002001-bed0-0080-aa55-4265644a6574"); + +class Bedjet : public climate::Climate, public esphome::ble_client::BLEClientNode, public PollingComponent { + public: + void setup() override; + void loop() override; + void update() override; + void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } + +#ifdef USE_TIME + void set_time_id(time::RealTimeClock *time_id) { this->time_id_ = time_id; } +#endif + void set_status_timeout(uint32_t timeout) { this->timeout_ = timeout; } + + /** Attempts to check for and apply firmware updates. */ + void upgrade_firmware(); + + climate::ClimateTraits traits() override { + auto traits = climate::ClimateTraits(); + traits.set_supports_action(true); + traits.set_supports_current_temperature(true); + traits.set_supported_modes({ + climate::CLIMATE_MODE_OFF, + climate::CLIMATE_MODE_HEAT, + // climate::CLIMATE_MODE_TURBO // Not supported by Climate: see presets instead + climate::CLIMATE_MODE_FAN_ONLY, + climate::CLIMATE_MODE_DRY, + }); + + // It would be better if we had a slider for the fan modes. + traits.set_supported_custom_fan_modes(BEDJET_FAN_STEP_NAMES_SET); + traits.set_supported_presets({ + // If we support NONE, then have to decide what happens if the user switches to it (turn off?) + // climate::CLIMATE_PRESET_NONE, + // Climate doesn't have a "TURBO" mode, but we can use the BOOST preset instead. + climate::CLIMATE_PRESET_BOOST, + }); + traits.set_supported_custom_presets({ + // 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. + "M1", + "M2", + "M3", + }); + traits.set_visual_min_temperature(19.0); + traits.set_visual_max_temperature(43.0); + traits.set_visual_temperature_step(1.0); + return traits; + } + + protected: + void control(const climate::ClimateCall &call) override; + +#ifdef USE_TIME + void setup_time_(); + void send_local_time_(); + optional time_id_{}; +#endif + + uint32_t timeout_{DEFAULT_STATUS_TIMEOUT}; + + static const uint32_t MIN_NOTIFY_THROTTLE = 5000; + static const uint32_t NOTIFY_WARN_THRESHOLD = 300000; + static const uint32_t DEFAULT_STATUS_TIMEOUT = 900000; + + uint8_t set_notify_(bool enable); + uint8_t write_bedjet_packet_(BedjetPacket *pkt); + void reset_state_(); + bool update_status_(); + + bool is_valid_() { + // FIXME: find a better way to check this? + return !std::isnan(this->current_temperature) && !std::isnan(this->target_temperature) && + this->current_temperature > 1 && this->target_temperature > 1; + } + + uint32_t last_notify_ = 0; + bool force_refresh_ = false; + + std::unique_ptr codec_; + uint16_t char_handle_cmd_; + uint16_t char_handle_name_; + uint16_t char_handle_status_; + uint16_t config_descr_status_; + + uint8_t write_notify_config_descriptor_(bool enable); +}; + +} // namespace bedjet +} // namespace esphome + +#endif diff --git a/esphome/components/bedjet/bedjet_base.cpp b/esphome/components/bedjet/bedjet_base.cpp new file mode 100644 index 0000000000..99f1df96d3 --- /dev/null +++ b/esphome/components/bedjet/bedjet_base.cpp @@ -0,0 +1,123 @@ +#include "bedjet_base.h" +#include +#include + +namespace esphome { +namespace bedjet { + +/// Converts a BedJet temp step into degrees Fahrenheit. +float bedjet_temp_to_f(const uint8_t temp) { + // BedJet temp is "C*2"; to get F, multiply by 0.9 (half 1.8) and add 32. + return 0.9f * temp + 32.0f; +} + +/** Cleans up the packet before sending. */ +BedjetPacket *BedjetCodec::clean_packet_() { + // So far no commands require more than 2 bytes of data. + assert(this->packet_.data_length <= 2); + for (int i = this->packet_.data_length; i < 2; i++) { + this->packet_.data[i] = '\0'; + } + ESP_LOGV(TAG, "Created packet: %02X, %02X %02X", this->packet_.command, this->packet_.data[0], this->packet_.data[1]); + return &this->packet_; +} + +/** Returns a BedjetPacket that will initiate a BedjetButton press. */ +BedjetPacket *BedjetCodec::get_button_request(BedjetButton button) { + this->packet_.command = CMD_BUTTON; + this->packet_.data_length = 1; + this->packet_.data[0] = button; + return this->clean_packet_(); +} + +/** Returns a BedjetPacket that will set the device's target `temperature`. */ +BedjetPacket *BedjetCodec::get_set_target_temp_request(float temperature) { + this->packet_.command = CMD_SET_TEMP; + this->packet_.data_length = 1; + this->packet_.data[0] = temperature * 2; + return this->clean_packet_(); +} + +/** Returns a BedjetPacket that will set the device's target fan speed. */ +BedjetPacket *BedjetCodec::get_set_fan_speed_request(const uint8_t fan_step) { + this->packet_.command = CMD_SET_FAN; + this->packet_.data_length = 1; + this->packet_.data[0] = fan_step; + return this->clean_packet_(); +} + +/** Returns a BedjetPacket that will set the device's current time. */ +BedjetPacket *BedjetCodec::get_set_time_request(const uint8_t hour, const uint8_t minute) { + this->packet_.command = CMD_SET_TIME; + this->packet_.data_length = 2; + this->packet_.data[0] = hour; + this->packet_.data[1] = minute; + return this->clean_packet_(); +} + +/** Decodes the extra bytes that were received after being notified with a partial packet. */ +void BedjetCodec::decode_extra(const uint8_t *data, uint16_t length) { + ESP_LOGV(TAG, "Received extra: %d bytes: %d %d %d %d", length, data[1], data[2], data[3], data[4]); + uint8_t offset = this->last_buffer_size_; + if (offset > 0 && length + offset <= sizeof(BedjetStatusPacket)) { + memcpy(((uint8_t *) (&this->buf_)) + offset, data, length); + ESP_LOGV(TAG, + "Extra bytes: skip1=0x%08x, skip2=0x%04x, skip3=0x%02x; update phase=0x%02x, " + "flags=BedjetFlags ", + this->buf_._skip_1_, this->buf_._skip_2_, this->buf_._skip_3_, this->buf_.update_phase, + this->buf_.flags & 0x20 ? '1' : '0', this->buf_.flags & 0x10 ? '1' : '0', + this->buf_.flags & 0x04 ? '1' : '0', this->buf_.flags & 0x01 ? '1' : '0', + this->buf_.flags & ~(0x20 | 0x10 | 0x04 | 0x01)); + } else { + ESP_LOGI(TAG, "Could not determine where to append to, last offset=%d, max size=%u, new size would be %d", offset, + sizeof(BedjetStatusPacket), length + offset); + } +} + +/** Decodes the incoming status packet received on the BEDJET_STATUS_UUID. + * + * @return `true` if the packet was decoded and represents a "partial" packet; `false` otherwise. + */ +bool BedjetCodec::decode_notify(const uint8_t *data, uint16_t length) { + ESP_LOGV(TAG, "Received: %d bytes: %d %d %d %d", length, data[1], data[2], data[3], data[4]); + + if (data[1] == PACKET_FORMAT_V3_HOME && data[3] == PACKET_TYPE_STATUS) { + this->status_packet_.reset(); + + // Clear old buffer + memset(&this->buf_, 0, sizeof(BedjetStatusPacket)); + // Copy new data into buffer + memcpy(&this->buf_, data, length); + this->last_buffer_size_ = length; + + // TODO: validate the packet checksum? + if (this->buf_.mode >= 0 && this->buf_.mode < 7 && this->buf_.target_temp_step >= 38 && + this->buf_.target_temp_step <= 86 && this->buf_.actual_temp_step > 1 && this->buf_.actual_temp_step <= 100 && + this->buf_.ambient_temp_step > 1 && this->buf_.ambient_temp_step <= 100) { + // and save it for the update() loop + this->status_packet_ = this->buf_; + return this->buf_.is_partial == 1; + } else { + // TODO: log a warning if we detect that we connected to a non-V3 device. + ESP_LOGW(TAG, "Received potentially invalid packet (len %d):", length); + } + } else if (data[1] == PACKET_FORMAT_DEBUG || data[3] == PACKET_TYPE_DEBUG) { + // We don't actually know the packet format for this. Dump packets to log, in case a pattern presents itself. + ESP_LOGV(TAG, + "received DEBUG packet: set1=%01fF, set2=%01fF, air=%01fF; [7]=%d, [8]=%d, [9]=%d, [10]=%d, [11]=%d, " + "[12]=%d, [-1]=%d", + bedjet_temp_to_f(data[4]), bedjet_temp_to_f(data[5]), bedjet_temp_to_f(data[6]), data[7], data[8], data[9], + data[10], data[11], data[12], data[length - 1]); + + if (this->has_status()) { + this->status_packet_->ambient_temp_step = data[6]; + } + } else { + // TODO: log a warning if we detect that we connected to a non-V3 device. + } + + return false; +} + +} // namespace bedjet +} // namespace esphome diff --git a/esphome/components/bedjet/bedjet_base.h b/esphome/components/bedjet/bedjet_base.h new file mode 100644 index 0000000000..c63b70cb9a --- /dev/null +++ b/esphome/components/bedjet/bedjet_base.h @@ -0,0 +1,159 @@ +#pragma once + +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +#include "bedjet_const.h" + +namespace esphome { +namespace bedjet { + +struct BedjetPacket { + uint8_t data_length; + BedjetCommand command; + uint8_t data[2]; +}; + +struct BedjetFlags { + /* uint8_t */ + int a_ : 1; // 0x80 + int b_ : 1; // 0x40 + int conn_test_passed : 1; ///< (0x20) Bit is set `1` if the last connection test passed. + int leds_enabled : 1; ///< (0x10) Bit is set `1` if the LEDs on the device are enabled. + int c_ : 1; // 0x08 + int units_setup : 1; ///< (0x04) Bit is set `1` if the device's units have been configured. + int d_ : 1; // 0x02 + int beeps_muted : 1; ///< (0x01) Bit is set `1` if the device's sound output is muted. +} __attribute__((packed)); + +enum BedjetPacketFormat : uint8_t { + PACKET_FORMAT_DEBUG = 0x05, // 5 + PACKET_FORMAT_V3_HOME = 0x56, // 86 +}; + +enum BedjetPacketType : uint8_t { + PACKET_TYPE_STATUS = 0x1, + PACKET_TYPE_DEBUG = 0x2, +}; + +/** The format of a BedJet V3 status packet. */ +struct BedjetStatusPacket { + // [0] + uint8_t is_partial : 8; ///< `1` indicates that this is a partial packet, and more data can be read directly from the + ///< characteristic. + BedjetPacketFormat packet_format : 8; ///< BedjetPacketFormat::PACKET_FORMAT_V3_HOME for BedJet V3 status packet + ///< format. BedjetPacketFormat::PACKET_FORMAT_DEBUG for debugging packets. + uint8_t + expecting_length : 8; ///< The expected total length of the status packet after merging the additional packet. + BedjetPacketType packet_type : 8; ///< Typically BedjetPacketType::PACKET_TYPE_STATUS for BedJet V3 status packet. + + // [4] + uint8_t time_remaining_hrs : 8; ///< Hours remaining in program runtime + uint8_t time_remaining_mins : 8; ///< Minutes remaining in program runtime + uint8_t time_remaining_secs : 8; ///< Seconds remaining in program runtime + + // [7] + uint8_t actual_temp_step : 8; ///< Actual temp of the air blown by the BedJet fan; value represents `2 * + ///< degrees_celsius`. See #bedjet_temp_to_c and #bedjet_temp_to_f + uint8_t target_temp_step : 8; ///< Target temp that the BedJet will try to heat to. See #actual_temp_step. + + // [9] + BedjetMode mode : 8; ///< BedJet operating mode. + + // [10] + uint8_t fan_step : 8; ///< BedJet fan speed; value is in the 0-19 range, representing 5% increments (5%-100%): `5 + 5 + ///< * fan_step` + uint8_t max_hrs : 8; ///< Max hours of mode runtime + uint8_t max_mins : 8; ///< Max minutes of mode runtime + uint8_t min_temp_step : 8; ///< Min temp allowed in mode. See #actual_temp_step. + uint8_t max_temp_step : 8; ///< Max temp allowed in mode. See #actual_temp_step. + + // [15-16] + uint16_t turbo_time : 16; ///< Time remaining in BedjetMode::MODE_TURBO. + + // [17] + uint8_t ambient_temp_step : 8; ///< Current ambient air temp. This is the coldest air the BedJet can blow. See + ///< #actual_temp_step. + uint8_t shutdown_reason : 8; ///< The reason for the last device shutdown. + + // [19-25]; the initial partial packet cuts off here after [19] + // Skip 7 bytes? + uint32_t _skip_1_ : 32; // Unknown 19-22 = 0x01810112 + + uint16_t _skip_2_ : 16; // Unknown 23-24 = 0x1310 + uint8_t _skip_3_ : 8; // Unknown 25 = 0x00 + + // [26] + // 0x18(24) = "Connection test has completed OK" + // 0x1a(26) = "Firmware update is not needed" + uint8_t update_phase : 8; ///< The current status/phase of a firmware update. + + // [27] + // FIXME: cannot nest packed struct of matching length here? + /* BedjetFlags */ uint8_t flags : 8; /// See BedjetFlags for the packed byte flags. + // [28-31]; 20+11 bytes + uint32_t _skip_4_ : 32; // Unknown + +} __attribute__((packed)); + +/** This class is responsible for encoding command packets and decoding status packets. + * + * Status Packets + * ============== + * The BedJet protocol depends on registering for notifications on the esphome::BedJet::BEDJET_SERVICE_UUID + * characteristic. If the BedJet is on, it will send rapid updates as notifications. If it is off, + * it generally will not notify of any status. + * + * As the BedJet V3's BedjetStatusPacket exceeds the buffer size allowed for BLE notification packets, + * the notification packet will contain `BedjetStatusPacket::is_partial == 1`. When that happens, an additional + * read of the esphome::BedJet::BEDJET_SERVICE_UUID characteristic will contain the second portion of the + * full status packet. + * + * Command Packets + * =============== + * This class supports encoding a number of BedjetPacket commands: + * - Button press + * This simulates a press of one of the BedjetButton values. + * - BedjetPacket#command = BedjetCommand::CMD_BUTTON + * - BedjetPacket#data [0] contains the BedjetButton value + * - Set target temp + * This sets the BedJet's target temp to a concrete temperature value. + * - BedjetPacket#command = BedjetCommand::CMD_SET_TEMP + * - BedjetPacket#data [0] contains the BedJet temp value; see BedjetStatusPacket#actual_temp_step + * - Set fan speed + * This sets the BedJet fan speed. + * - BedjetPacket#command = BedjetCommand::CMD_SET_FAN + * - BedjetPacket#data [0] contains the BedJet fan step in the range 0-19. + * - Set current time + * The BedJet needs to have its clock set properly in order to run the biorhythm programs, which might + * contain time-of-day based step rules. + * - BedjetPacket#command = BedjetCommand::CMD_SET_TIME + * - BedjetPacket#data [0] is hours, [1] is minutes + */ +class BedjetCodec { + public: + BedjetPacket *get_button_request(BedjetButton button); + BedjetPacket *get_set_target_temp_request(float temperature); + BedjetPacket *get_set_fan_speed_request(uint8_t fan_step); + BedjetPacket *get_set_time_request(uint8_t hour, uint8_t minute); + + bool decode_notify(const uint8_t *data, uint16_t length); + void decode_extra(const uint8_t *data, uint16_t length); + + inline bool has_status() { return this->status_packet_.has_value(); } + const optional &get_status_packet() const { return this->status_packet_; } + void clear_status() { this->status_packet_.reset(); } + + protected: + BedjetPacket *clean_packet_(); + + uint8_t last_buffer_size_ = 0; + + BedjetPacket packet_; + + optional status_packet_; + BedjetStatusPacket buf_; +}; + +} // namespace bedjet +} // namespace esphome diff --git a/esphome/components/bedjet/bedjet_const.h b/esphome/components/bedjet/bedjet_const.h new file mode 100644 index 0000000000..e6bfa45d3a --- /dev/null +++ b/esphome/components/bedjet/bedjet_const.h @@ -0,0 +1,78 @@ +#pragma once + +#include + +namespace esphome { +namespace bedjet { + +static const char *const TAG = "bedjet"; + +enum BedjetMode : uint8_t { + /// BedJet is Off + MODE_STANDBY = 0, + /// BedJet is in Heat mode (limited to 4 hours) + MODE_HEAT = 1, + /// BedJet is in Turbo mode (high heat, limited time) + MODE_TURBO = 2, + /// BedJet is in Extended Heat mode (limited to 10 hours) + MODE_EXTHT = 3, + /// BedJet is in Cool mode (actually "Fan only" mode) + MODE_COOL = 4, + /// BedJet is in Dry mode (high speed, no heat) + MODE_DRY = 5, + /// BedJet is in "wait" mode, a step during a biorhythm program + MODE_WAIT = 6, +}; + +enum BedjetButton : uint8_t { + /// Turn BedJet off + BTN_OFF = 0x1, + /// Enter Cool mode (fan only) + BTN_COOL = 0x2, + /// Enter Heat mode (limited to 4 hours) + BTN_HEAT = 0x3, + /// Enter Turbo mode (high heat, limited to 10 minutes) + BTN_TURBO = 0x4, + /// Enter Dry mode (high speed, no heat) + BTN_DRY = 0x5, + /// Enter Extended Heat mode (limited to 10 hours) + BTN_EXTHT = 0x6, + + /// Start the M1 biorhythm/preset program + BTN_M1 = 0x20, + /// Start the M2 biorhythm/preset program + BTN_M2 = 0x21, + /// Start the M3 biorhythm/preset program + BTN_M3 = 0x22, + + /* These are "MAGIC" buttons */ + + /// Turn debug mode on/off + MAGIC_DEBUG_ON = 0x40, + MAGIC_DEBUG_OFF = 0x41, + /// Perform a connection test. + MAGIC_CONNTEST = 0x42, + /// Request a firmware update. This will also restart the Bedjet. + MAGIC_UPDATE = 0x43, +}; + +enum BedjetCommand : uint8_t { + CMD_BUTTON = 0x1, + CMD_SET_TEMP = 0x3, + CMD_STATUS = 0x6, + CMD_SET_FAN = 0x7, + CMD_SET_TIME = 0x8, +}; + +#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%" \ + } + +static const char *const BEDJET_FAN_STEP_NAMES[20] = BEDJET_FAN_STEP_NAMES_; +static const std::string BEDJET_FAN_STEP_NAME_STRINGS[20] = BEDJET_FAN_STEP_NAMES_; +static const std::set BEDJET_FAN_STEP_NAMES_SET BEDJET_FAN_STEP_NAMES_; + +} // namespace bedjet +} // namespace esphome diff --git a/esphome/components/bedjet/climate.py b/esphome/components/bedjet/climate.py new file mode 100644 index 0000000000..49353934f6 --- /dev/null +++ b/esphome/components/bedjet/climate.py @@ -0,0 +1,42 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import climate, ble_client, time +from esphome.const import ( + CONF_ID, + CONF_RECEIVE_TIMEOUT, + CONF_TIME_ID, +) + +CODEOWNERS = ["@jhansche"] +DEPENDENCIES = ["ble_client"] + +bedjet_ns = cg.esphome_ns.namespace("bedjet") +Bedjet = bedjet_ns.class_( + "Bedjet", climate.Climate, ble_client.BLEClientNode, cg.PollingComponent +) + +CONFIG_SCHEMA = ( + climate.CLIMATE_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(Bedjet), + cv.Optional(CONF_TIME_ID): cv.use_id(time.RealTimeClock), + cv.Optional( + CONF_RECEIVE_TIMEOUT, default="0s" + ): cv.positive_time_period_milliseconds, + } + ) + .extend(ble_client.BLE_CLIENT_SCHEMA) + .extend(cv.polling_component_schema("30s")) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await climate.register_climate(var, config) + await ble_client.register_ble_node(var, 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_RECEIVE_TIMEOUT in config: + cg.add(var.set_status_timeout(config[CONF_RECEIVE_TIMEOUT])) diff --git a/tests/test1.yaml b/tests/test1.yaml index 98a3ffcf4b..375499942b 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -291,6 +291,8 @@ ble_client: on_disconnect: then: - switch.turn_on: ble1_status + - mac_address: C4:4F:33:11:22:33 + id: my_bedjet_ble_client mcp23s08: - id: "mcp23s08_hub" cs_pin: GPIO12 @@ -1870,6 +1872,9 @@ climate: ble_client_id: ble_blah unit_of_measurement: c icon: mdi:stove + - platform: bedjet + name: My Bedjet + ble_client_id: my_bedjet_ble_client script: - id: climate_custom From 93b628d9a856461e570b9e195f8dbd4c04346cde Mon Sep 17 00:00:00 2001 From: Janez Troha <239513+dz0ny@users.noreply.github.com> Date: Thu, 14 Apr 2022 03:42:43 +0200 Subject: [PATCH 06/56] Allocate smaller amount of buffer for JSON (#3384) --- esphome/components/json/json_util.cpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/esphome/components/json/json_util.cpp b/esphome/components/json/json_util.cpp index 10179c9954..2bd8112255 100644 --- a/esphome/components/json/json_util.cpp +++ b/esphome/components/json/json_util.cpp @@ -23,13 +23,13 @@ std::string build_json(const json_build_t &f) { #ifdef USE_ESP8266 const size_t free_heap = ESP.getMaxFreeBlockSize(); // NOLINT(readability-static-accessed-through-instance) #elif defined(USE_ESP32) - const size_t free_heap = heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL); + const size_t free_heap = heap_caps_get_largest_free_block(MALLOC_CAP_8BIT); #endif - const size_t request_size = std::min(free_heap - 2048, (size_t) 5120); + const size_t request_size = std::min(free_heap, (size_t) 512); DynamicJsonDocument json_document(request_size); - if (json_document.memoryPool().buffer() == nullptr) { + 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 "{}"; @@ -37,7 +37,7 @@ std::string build_json(const json_build_t &f) { 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; @@ -51,13 +51,13 @@ void parse_json(const std::string &data, const json_parse_t &f) { #ifdef USE_ESP8266 const size_t free_heap = ESP.getMaxFreeBlockSize(); // NOLINT(readability-static-accessed-through-instance) #elif defined(USE_ESP32) - const size_t free_heap = heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL); + const size_t free_heap = heap_caps_get_largest_free_block(MALLOC_CAP_8BIT); #endif bool pass = false; - size_t request_size = std::min(free_heap - 2048, (size_t)(data.size() * 1.5)); + size_t request_size = std::min(free_heap, (size_t)(data.size() * 1.5)); do { DynamicJsonDocument json_document(request_size); - if (json_document.memoryPool().buffer() == nullptr) { + if (json_document.capacity() == 0) { ESP_LOGE(TAG, "Could not allocate memory for JSON document! Requested %u bytes, free heap: %u", request_size, free_heap); return; From b605982f940f4ac831f179c7645a7dd3b034622a Mon Sep 17 00:00:00 2001 From: Michel van de Wetering Date: Mon, 18 Apr 2022 22:42:02 +0200 Subject: [PATCH 07/56] Fix power_delivered/produced_phase sensor deviceclass in DSMR (#3395) --- esphome/components/dsmr/sensor.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/esphome/components/dsmr/sensor.py b/esphome/components/dsmr/sensor.py index bb4722655c..0b0439baa4 100644 --- a/esphome/components/dsmr/sensor.py +++ b/esphome/components/dsmr/sensor.py @@ -143,37 +143,37 @@ CONFIG_SCHEMA = cv.Schema( cv.Optional("power_delivered_l1"): sensor.sensor_schema( unit_of_measurement=UNIT_KILOWATT, accuracy_decimals=3, - device_class=DEVICE_CLASS_CURRENT, + device_class=DEVICE_CLASS_POWER, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional("power_delivered_l2"): sensor.sensor_schema( unit_of_measurement=UNIT_KILOWATT, accuracy_decimals=3, - device_class=DEVICE_CLASS_CURRENT, + device_class=DEVICE_CLASS_POWER, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional("power_delivered_l3"): sensor.sensor_schema( unit_of_measurement=UNIT_KILOWATT, accuracy_decimals=3, - device_class=DEVICE_CLASS_CURRENT, + device_class=DEVICE_CLASS_POWER, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional("power_returned_l1"): sensor.sensor_schema( unit_of_measurement=UNIT_KILOWATT, accuracy_decimals=3, - device_class=DEVICE_CLASS_CURRENT, + device_class=DEVICE_CLASS_POWER, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional("power_returned_l2"): sensor.sensor_schema( unit_of_measurement=UNIT_KILOWATT, accuracy_decimals=3, - device_class=DEVICE_CLASS_CURRENT, + device_class=DEVICE_CLASS_POWER, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional("power_returned_l3"): sensor.sensor_schema( unit_of_measurement=UNIT_KILOWATT, accuracy_decimals=3, - device_class=DEVICE_CLASS_CURRENT, + device_class=DEVICE_CLASS_POWER, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional("reactive_power_delivered_l1"): sensor.sensor_schema( From 2064abe16de0dc90b108839cce551af3af21e17a Mon Sep 17 00:00:00 2001 From: rnauber <7414650+rnauber@users.noreply.github.com> Date: Mon, 18 Apr 2022 22:43:34 +0200 Subject: [PATCH 08/56] Shelly Dimmer: Delete obsolete LICENSE.txt (#3394) --- esphome/components/shelly_dimmer/LICENSE.txt | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 esphome/components/shelly_dimmer/LICENSE.txt diff --git a/esphome/components/shelly_dimmer/LICENSE.txt b/esphome/components/shelly_dimmer/LICENSE.txt deleted file mode 100644 index 524fe0d514..0000000000 --- a/esphome/components/shelly_dimmer/LICENSE.txt +++ /dev/null @@ -1,2 +0,0 @@ -The firmware files for the STM microcontroller (shelly-dimmer-stm32_*.bin) are taken from -https://github.com/jamesturton/shelly-dimmer-stm32 and GPLv3 licensed. From 0767b92b62d8b54d2233f86bac2e23f8ab52cf94 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 20 Apr 2022 06:56:09 +1200 Subject: [PATCH 09/56] Dont require {} for wifi ap with defaults (#3404) --- esphome/components/wifi/__init__.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 20f43cb450..b56902df2f 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -126,6 +126,13 @@ WIFI_NETWORK_AP = WIFI_NETWORK_BASE.extend( } ) + +def wifi_network_ap(value): + if value is None: + value = {} + return WIFI_NETWORK_AP(value) + + WIFI_NETWORK_STA = WIFI_NETWORK_BASE.extend( { cv.Optional(CONF_BSSID): cv.mac_address, @@ -252,7 +259,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_PASSWORD): validate_password, cv.Optional(CONF_MANUAL_IP): STA_MANUAL_IP_SCHEMA, cv.Optional(CONF_EAP): EAP_AUTH_SCHEMA, - cv.Optional(CONF_AP): WIFI_NETWORK_AP, + cv.Optional(CONF_AP): wifi_network_ap, cv.Optional(CONF_DOMAIN, default=".local"): cv.domain_name, cv.Optional( CONF_REBOOT_TIMEOUT, default="15min" From 988d3ea8baf755e69525bc2d96f94424ea5b4b08 Mon Sep 17 00:00:00 2001 From: parats15 <72889410+parats15@users.noreply.github.com> Date: Wed, 20 Apr 2022 02:46:55 +0200 Subject: [PATCH 10/56] Multi conf for Teleinfo component (#3401) --- esphome/components/teleinfo/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/teleinfo/__init__.py b/esphome/components/teleinfo/__init__.py index 33b748a031..e289e42c81 100644 --- a/esphome/components/teleinfo/__init__.py +++ b/esphome/components/teleinfo/__init__.py @@ -4,6 +4,7 @@ from esphome.components import uart from esphome.const import CONF_ID CODEOWNERS = ["@0hax"] +MULTI_CONF = True teleinfo_ns = cg.esphome_ns.namespace("teleinfo") TeleInfo = teleinfo_ns.class_("TeleInfo", cg.PollingComponent, uart.UARTDevice) From 9576d246eec5fa4a5d0b0d0b82261ad0aa539275 Mon Sep 17 00:00:00 2001 From: James Duke Date: Tue, 19 Apr 2022 17:50:24 -0700 Subject: [PATCH 11/56] Add support for Mopeka Pro+ Residential sensor (#3393) * Add support for Pro+ Residential sensor (enum) The Mopeka Pro+ Residential sensor is very similar to the Pro sensor, but includes a longer range antenna, and maybe hardware? The Pro+ identifies itself with 0x08 sensor type. * Add logic to support Pro+ Residential sensor * Fix formatting --- esphome/components/mopeka_pro_check/mopeka_pro_check.cpp | 3 ++- esphome/components/mopeka_pro_check/mopeka_pro_check.h | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/esphome/components/mopeka_pro_check/mopeka_pro_check.cpp b/esphome/components/mopeka_pro_check/mopeka_pro_check.cpp index bcfe0a80ce..fc57318a81 100644 --- a/esphome/components/mopeka_pro_check/mopeka_pro_check.cpp +++ b/esphome/components/mopeka_pro_check/mopeka_pro_check.cpp @@ -52,7 +52,8 @@ bool MopekaProCheck::parse_device(const esp32_ble_tracker::ESPBTDevice &device) // Now parse the data - See Datasheet for definition - if (static_cast(manu_data.data[0]) != STANDARD_BOTTOM_UP) { + if (static_cast(manu_data.data[0]) != STANDARD_BOTTOM_UP && + static_cast(manu_data.data[0]) != PLUS_BOTTOM_UP) { ESP_LOGE(TAG, "Unsupported Sensor Type (0x%X)", manu_data.data[0]); return false; } diff --git a/esphome/components/mopeka_pro_check/mopeka_pro_check.h b/esphome/components/mopeka_pro_check/mopeka_pro_check.h index 59d33f7763..dfdce9353e 100644 --- a/esphome/components/mopeka_pro_check/mopeka_pro_check.h +++ b/esphome/components/mopeka_pro_check/mopeka_pro_check.h @@ -12,7 +12,8 @@ namespace mopeka_pro_check { enum SensorType { STANDARD_BOTTOM_UP = 0x03, TOP_DOWN_AIR_ABOVE = 0x04, - BOTTOM_UP_WATER = 0x05 + BOTTOM_UP_WATER = 0x05, + PLUS_BOTTOM_UP = 0x08 // all other values are reserved }; From 7a778f3f330ca607f2772501eac82a043d0346d3 Mon Sep 17 00:00:00 2001 From: "I. Tomita" Date: Thu, 21 Apr 2022 01:11:25 +0300 Subject: [PATCH 12/56] Add support for BL0939 (Sonoff Dual R3 V2 powermeter) (#3300) --- CODEOWNERS | 1 + esphome/components/bl0939/__init__.py | 1 + esphome/components/bl0939/bl0939.cpp | 144 ++++++++++++++++++++++++++ esphome/components/bl0939/bl0939.h | 107 +++++++++++++++++++ esphome/components/bl0939/sensor.py | 123 ++++++++++++++++++++++ tests/test3.yaml | 24 +++++ 6 files changed, 400 insertions(+) create mode 100644 esphome/components/bl0939/__init__.py create mode 100644 esphome/components/bl0939/bl0939.cpp create mode 100644 esphome/components/bl0939/bl0939.h create mode 100644 esphome/components/bl0939/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index c9df669f03..7fd049f46e 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -31,6 +31,7 @@ esphome/components/bang_bang/* @OttoWinter esphome/components/bedjet/* @jhansche esphome/components/bh1750/* @OttoWinter esphome/components/binary_sensor/* @esphome/core +esphome/components/bl0939/* @ziceva esphome/components/bl0940/* @tobias- esphome/components/ble_client/* @buxtronix esphome/components/bme680_bsec/* @trvrnrth diff --git a/esphome/components/bl0939/__init__.py b/esphome/components/bl0939/__init__.py new file mode 100644 index 0000000000..9bd4598dd2 --- /dev/null +++ b/esphome/components/bl0939/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@ziceva"] diff --git a/esphome/components/bl0939/bl0939.cpp b/esphome/components/bl0939/bl0939.cpp new file mode 100644 index 0000000000..61d7835a4b --- /dev/null +++ b/esphome/components/bl0939/bl0939.cpp @@ -0,0 +1,144 @@ +#include "bl0939.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace bl0939 { + +static const char *const TAG = "bl0939"; + +// https://www.belling.com.cn/media/file_object/bel_product/BL0939/datasheet/BL0939_V1.2_cn.pdf +// (unfortunatelly chinese, but the protocol can be understood with some translation tool) +static const uint8_t BL0939_READ_COMMAND = 0x55; // 0x5{A4,A3,A2,A1} +static const uint8_t BL0939_FULL_PACKET = 0xAA; +static const uint8_t BL0939_PACKET_HEADER = 0x55; + +static const uint8_t BL0939_WRITE_COMMAND = 0xA5; // 0xA{A4,A3,A2,A1} +static const uint8_t BL0939_REG_IA_FAST_RMS_CTRL = 0x10; +static const uint8_t BL0939_REG_IB_FAST_RMS_CTRL = 0x1E; +static const uint8_t BL0939_REG_MODE = 0x18; +static const uint8_t BL0939_REG_SOFT_RESET = 0x19; +static const uint8_t BL0939_REG_USR_WRPROT = 0x1A; +static const uint8_t BL0939_REG_TPS_CTRL = 0x1B; + +const uint8_t BL0939_INIT[6][6] = { + // Reset to default + {BL0939_WRITE_COMMAND, BL0939_REG_SOFT_RESET, 0x5A, 0x5A, 0x5A, 0x33}, + // Enable User Operation Write + {BL0939_WRITE_COMMAND, BL0939_REG_USR_WRPROT, 0x55, 0x00, 0x00, 0xEB}, + // 0x0100 = CF_UNABLE energy pulse, AC_FREQ_SEL 50Hz, RMS_UPDATE_SEL 800mS + {BL0939_WRITE_COMMAND, BL0939_REG_MODE, 0x00, 0x10, 0x00, 0x32}, + // 0x47FF = Over-current and leakage alarm on, Automatic temperature measurement, Interval 100mS + {BL0939_WRITE_COMMAND, BL0939_REG_TPS_CTRL, 0xFF, 0x47, 0x00, 0xF9}, + // 0x181C = Half cycle, Fast RMS threshold 6172 + {BL0939_WRITE_COMMAND, BL0939_REG_IA_FAST_RMS_CTRL, 0x1C, 0x18, 0x00, 0x16}, + // 0x181C = Half cycle, Fast RMS threshold 6172 + {BL0939_WRITE_COMMAND, BL0939_REG_IB_FAST_RMS_CTRL, 0x1C, 0x18, 0x00, 0x08}}; + +void BL0939::loop() { + DataPacket buffer; + if (!this->available()) { + return; + } + if (read_array((uint8_t *) &buffer, sizeof(buffer))) { + if (validate_checksum(&buffer)) { + received_package_(&buffer); + } + } else { + ESP_LOGW(TAG, "Junk on wire. Throwing away partial message"); + while (read() >= 0) + ; + } +} + +bool BL0939::validate_checksum(const DataPacket *data) { + uint8_t checksum = BL0939_READ_COMMAND; + // Whole package but checksum + for (uint32_t i = 0; i < sizeof(data->raw) - 1; i++) { + checksum += data->raw[i]; + } + checksum ^= 0xFF; + if (checksum != data->checksum) { + ESP_LOGW(TAG, "BL0939 invalid checksum! 0x%02X != 0x%02X", checksum, data->checksum); + } + return checksum == data->checksum; +} + +void BL0939::update() { + this->flush(); + this->write_byte(BL0939_READ_COMMAND); + this->write_byte(BL0939_FULL_PACKET); +} + +void BL0939::setup() { + for (auto *i : BL0939_INIT) { + this->write_array(i, 6); + delay(1); + } + this->flush(); +} + +void BL0939::received_package_(const DataPacket *data) const { + // Bad header + if (data->frame_header != BL0939_PACKET_HEADER) { + ESP_LOGI("bl0939", "Invalid data. Header mismatch: %d", data->frame_header); + return; + } + + float v_rms = (float) to_uint32_t(data->v_rms) / voltage_reference_; + float ia_rms = (float) to_uint32_t(data->ia_rms) / current_reference_; + float ib_rms = (float) to_uint32_t(data->ib_rms) / current_reference_; + float a_watt = (float) to_int32_t(data->a_watt) / power_reference_; + float b_watt = (float) to_int32_t(data->b_watt) / power_reference_; + int32_t cfa_cnt = to_int32_t(data->cfa_cnt); + int32_t cfb_cnt = to_int32_t(data->cfb_cnt); + float a_energy_consumption = (float) cfa_cnt / energy_reference_; + float b_energy_consumption = (float) cfb_cnt / energy_reference_; + float total_energy_consumption = a_energy_consumption + b_energy_consumption; + + if (voltage_sensor_ != nullptr) { + voltage_sensor_->publish_state(v_rms); + } + if (current_sensor_1_ != nullptr) { + current_sensor_1_->publish_state(ia_rms); + } + if (current_sensor_2_ != nullptr) { + current_sensor_2_->publish_state(ib_rms); + } + if (power_sensor_1_ != nullptr) { + power_sensor_1_->publish_state(a_watt); + } + if (power_sensor_2_ != nullptr) { + power_sensor_2_->publish_state(b_watt); + } + if (energy_sensor_1_ != nullptr) { + energy_sensor_1_->publish_state(a_energy_consumption); + } + if (energy_sensor_2_ != nullptr) { + energy_sensor_2_->publish_state(b_energy_consumption); + } + if (energy_sensor_sum_ != nullptr) { + energy_sensor_sum_->publish_state(total_energy_consumption); + } + + ESP_LOGV("bl0939", "BL0939: U %fV, I1 %fA, I2 %fA, P1 %fW, P2 %fW, CntA %d, CntB %d, ∫P1 %fkWh, ∫P2 %fkWh", v_rms, + ia_rms, ib_rms, a_watt, b_watt, cfa_cnt, cfb_cnt, a_energy_consumption, b_energy_consumption); +} + +void BL0939::dump_config() { // NOLINT(readability-function-cognitive-complexity) + ESP_LOGCONFIG(TAG, "BL0939:"); + LOG_SENSOR("", "Voltage", this->voltage_sensor_); + LOG_SENSOR("", "Current 1", this->current_sensor_1_); + LOG_SENSOR("", "Current 2", this->current_sensor_2_); + LOG_SENSOR("", "Power 1", this->power_sensor_1_); + LOG_SENSOR("", "Power 2", this->power_sensor_2_); + LOG_SENSOR("", "Energy 1", this->energy_sensor_1_); + LOG_SENSOR("", "Energy 2", this->energy_sensor_2_); + LOG_SENSOR("", "Energy sum", this->energy_sensor_sum_); +} + +uint32_t BL0939::to_uint32_t(ube24_t input) { return input.h << 16 | input.m << 8 | input.l; } + +int32_t BL0939::to_int32_t(sbe24_t input) { return input.h << 16 | input.m << 8 | input.l; } + +} // namespace bl0939 +} // namespace esphome diff --git a/esphome/components/bl0939/bl0939.h b/esphome/components/bl0939/bl0939.h new file mode 100644 index 0000000000..5221ae26e7 --- /dev/null +++ b/esphome/components/bl0939/bl0939.h @@ -0,0 +1,107 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/uart/uart.h" +#include "esphome/components/sensor/sensor.h" + +namespace esphome { +namespace bl0939 { + +// https://datasheet.lcsc.com/lcsc/2108071830_BL-Shanghai-Belling-BL0939_C2841044.pdf +// (unfortunatelly chinese, but the formulas can be easily understood) +// Sonoff Dual R3 V2 has the exact same resistor values for the current shunts (RL=1miliOhm) +// and for the voltage divider (R1=0.51kOhm, R2=5*390kOhm) +// as in the manufacturer's reference circuit, so the same formulas were used here (Vref=1.218V) +static const float BL0939_IREF = 324004 * 1 / 1.218; +static const float BL0939_UREF = 79931 * 0.51 * 1000 / (1.218 * (5 * 390 + 0.51)); +static const float BL0939_PREF = 4046 * 1 * 0.51 * 1000 / (1.218 * 1.218 * (5 * 390 + 0.51)); +static const float BL0939_EREF = 3.6e6 * 4046 * 1 * 0.51 * 1000 / (1638.4 * 256 * 1.218 * 1.218 * (5 * 390 + 0.51)); + +struct ube24_t { // NOLINT(readability-identifier-naming,altera-struct-pack-align) + uint8_t l; + uint8_t m; + uint8_t h; +} __attribute__((packed)); + +struct ube16_t { // NOLINT(readability-identifier-naming,altera-struct-pack-align) + uint8_t l; + uint8_t h; +} __attribute__((packed)); + +struct sbe24_t { // NOLINT(readability-identifier-naming,altera-struct-pack-align) + uint8_t l; + uint8_t m; + int8_t h; +} __attribute__((packed)); + +// Caveat: All these values are big endian (low - middle - high) + +union DataPacket { // NOLINT(altera-struct-pack-align) + uint8_t raw[35]; + struct { + uint8_t frame_header; // 0x55 according to docs + ube24_t ia_fast_rms; + ube24_t ia_rms; + ube24_t ib_rms; + ube24_t v_rms; + ube24_t ib_fast_rms; + sbe24_t a_watt; + sbe24_t b_watt; + sbe24_t cfa_cnt; + sbe24_t cfb_cnt; + ube16_t tps1; + uint8_t RESERVED1; // value of 0x00 + ube16_t tps2; + uint8_t RESERVED2; // value of 0x00 + uint8_t checksum; // checksum + }; +} __attribute__((packed)); + +class BL0939 : public PollingComponent, public uart::UARTDevice { + public: + void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; } + void set_current_sensor_1(sensor::Sensor *current_sensor_1) { current_sensor_1_ = current_sensor_1; } + void set_current_sensor_2(sensor::Sensor *current_sensor_2) { current_sensor_2_ = current_sensor_2; } + void set_power_sensor_1(sensor::Sensor *power_sensor_1) { power_sensor_1_ = power_sensor_1; } + void set_power_sensor_2(sensor::Sensor *power_sensor_2) { power_sensor_2_ = power_sensor_2; } + void set_energy_sensor_1(sensor::Sensor *energy_sensor_1) { energy_sensor_1_ = energy_sensor_1; } + void set_energy_sensor_2(sensor::Sensor *energy_sensor_2) { energy_sensor_2_ = energy_sensor_2; } + void set_energy_sensor_sum(sensor::Sensor *energy_sensor_sum) { energy_sensor_sum_ = energy_sensor_sum; } + + void loop() override; + + void update() override; + void setup() override; + void dump_config() override; + + protected: + sensor::Sensor *voltage_sensor_; + sensor::Sensor *current_sensor_1_; + sensor::Sensor *current_sensor_2_; + // NB This may be negative as the circuits is seemingly able to measure + // power in both directions + sensor::Sensor *power_sensor_1_; + sensor::Sensor *power_sensor_2_; + sensor::Sensor *energy_sensor_1_; + sensor::Sensor *energy_sensor_2_; + sensor::Sensor *energy_sensor_sum_; + + // Divide by this to turn into Watt + float power_reference_ = BL0939_PREF; + // Divide by this to turn into Volt + float voltage_reference_ = BL0939_UREF; + // Divide by this to turn into Ampere + float current_reference_ = BL0939_IREF; + // Divide by this to turn into kWh + float energy_reference_ = BL0939_EREF; + + static uint32_t to_uint32_t(ube24_t input); + + static int32_t to_int32_t(sbe24_t input); + + static bool validate_checksum(const DataPacket *data); + + void received_package_(const DataPacket *data) const; +}; +} // namespace bl0939 +} // namespace esphome diff --git a/esphome/components/bl0939/sensor.py b/esphome/components/bl0939/sensor.py new file mode 100644 index 0000000000..bcc72ad61a --- /dev/null +++ b/esphome/components/bl0939/sensor.py @@ -0,0 +1,123 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, uart +from esphome.const import ( + CONF_ID, + CONF_VOLTAGE, + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_POWER, + DEVICE_CLASS_VOLTAGE, + STATE_CLASS_MEASUREMENT, + UNIT_AMPERE, + UNIT_KILOWATT_HOURS, + UNIT_VOLT, + UNIT_WATT, +) + +DEPENDENCIES = ["uart"] + +CONF_CURRENT_1 = "current_1" +CONF_CURRENT_2 = "current_2" +CONF_ACTIVE_POWER_1 = "active_power_1" +CONF_ACTIVE_POWER_2 = "active_power_2" +CONF_ENERGY_1 = "energy_1" +CONF_ENERGY_2 = "energy_2" +CONF_ENERGY_TOTAL = "energy_total" + +bl0939_ns = cg.esphome_ns.namespace("bl0939") +BL0939 = bl0939_ns.class_("BL0939", cg.PollingComponent, uart.UARTDevice) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(BL0939), + cv.Optional(CONF_VOLTAGE): sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_CURRENT_1): sensor.sensor_schema( + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=2, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_CURRENT_2): sensor.sensor_schema( + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=2, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_ACTIVE_POWER_1): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_ACTIVE_POWER_2): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_ENERGY_1): sensor.sensor_schema( + unit_of_measurement=UNIT_KILOWATT_HOURS, + accuracy_decimals=3, + device_class=DEVICE_CLASS_ENERGY, + ), + cv.Optional(CONF_ENERGY_2): sensor.sensor_schema( + unit_of_measurement=UNIT_KILOWATT_HOURS, + accuracy_decimals=3, + device_class=DEVICE_CLASS_ENERGY, + ), + cv.Optional(CONF_ENERGY_TOTAL): sensor.sensor_schema( + unit_of_measurement=UNIT_KILOWATT_HOURS, + accuracy_decimals=3, + device_class=DEVICE_CLASS_ENERGY, + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(uart.UART_DEVICE_SCHEMA) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await uart.register_uart_device(var, config) + + if CONF_VOLTAGE in config: + conf = config[CONF_VOLTAGE] + sens = await sensor.new_sensor(conf) + cg.add(var.set_voltage_sensor(sens)) + if CONF_CURRENT_1 in config: + conf = config[CONF_CURRENT_1] + sens = await sensor.new_sensor(conf) + cg.add(var.set_current_sensor_1(sens)) + if CONF_CURRENT_2 in config: + conf = config[CONF_CURRENT_2] + sens = await sensor.new_sensor(conf) + cg.add(var.set_current_sensor_2(sens)) + if CONF_ACTIVE_POWER_1 in config: + conf = config[CONF_ACTIVE_POWER_1] + sens = await sensor.new_sensor(conf) + cg.add(var.set_power_sensor_1(sens)) + if CONF_ACTIVE_POWER_2 in config: + conf = config[CONF_ACTIVE_POWER_2] + sens = await sensor.new_sensor(conf) + cg.add(var.set_power_sensor_2(sens)) + if CONF_ENERGY_1 in config: + conf = config[CONF_ENERGY_1] + sens = await sensor.new_sensor(conf) + cg.add(var.set_energy_sensor_1(sens)) + if CONF_ENERGY_2 in config: + conf = config[CONF_ENERGY_2] + sens = await sensor.new_sensor(conf) + cg.add(var.set_energy_sensor_2(sens)) + if CONF_ENERGY_TOTAL in config: + conf = config[CONF_ENERGY_TOTAL] + sens = await sensor.new_sensor(conf) + cg.add(var.set_energy_sensor_sum(sens)) diff --git a/tests/test3.yaml b/tests/test3.yaml index 58cb14740f..29a70d3cc3 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -256,6 +256,12 @@ uart: tx_pin: GPIO4 rx_pin: GPIO5 baud_rate: 38400 + - id: uart8 + tx_pin: GPIO4 + rx_pin: GPIO5 + baud_rate: 4800 + parity: NONE + stop_bits: 2 # Specifically added for testing debug with no options at all. debug: @@ -477,6 +483,24 @@ sensor: active_power_b: name: ADE7953 Active Power B id: ade7953_active_power_b + - platform: bl0939 + uart_id: uart8 + voltage: + name: 'BL0939 Voltage' + current_1: + name: 'BL0939 Current 1' + current_2: + name: 'BL0939 Current 2' + active_power_1: + name: 'BL0939 Active Power 1' + active_power_2: + name: 'BL0939 Active Power 2' + energy_1: + name: 'BL0939 Energy 1' + energy_2: + name: 'BL0939 Energy 2' + energy_total: + name: 'BL0939 Total energy' - platform: bl0940 uart_id: uart3 voltage: From 757b98748b7e6d13cf21ecf379870eb4efe94fa4 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 22 Apr 2022 17:08:01 +1200 Subject: [PATCH 13/56] Add "esphome rename" command (#3403) * Add "esphome rename" command * Only open file once * Update esphome/__main__.py Co-authored-by: Paulus Schoutsen * Add final return * Use match.group consistently * Validate name characters * Add whitespace to regex so it is only replacing exact match * Validate yaml config file after manipulation Co-authored-by: Paulus Schoutsen --- esphome/__main__.py | 101 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) diff --git a/esphome/__main__.py b/esphome/__main__.py index 85cf4ede85..00770d6f05 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -2,6 +2,7 @@ import argparse import functools import logging import os +import re import sys from datetime import datetime @@ -9,15 +10,18 @@ from esphome import const, writer, yaml_util import esphome.codegen as cg from esphome.config import iter_components, read_config, strip_default_ids from esphome.const import ( + ALLOWED_NAME_CHARS, CONF_BAUD_RATE, CONF_BROKER, CONF_DEASSERT_RTS_DTR, CONF_LOGGER, + CONF_NAME, CONF_OTA, CONF_PASSWORD, CONF_PORT, CONF_ESPHOME, CONF_PLATFORMIO_OPTIONS, + CONF_SUBSTITUTIONS, SECRETS_FILES, ) from esphome.core import CORE, EsphomeError, coroutine @@ -481,6 +485,96 @@ def command_idedata(args, config): return 0 +def command_rename(args, config): + for c in args.name: + if c not in ALLOWED_NAME_CHARS: + print( + color( + Fore.BOLD_RED, + f"'{c}' is an invalid character for names. Valid characters are: " + f"{ALLOWED_NAME_CHARS} (lowercase, no spaces)", + ) + ) + return 1 + with open(CORE.config_path, mode="r+", encoding="utf-8") as raw_file: + raw_contents = raw_file.read() + yaml = yaml_util.load_yaml(CORE.config_path) + if CONF_ESPHOME not in yaml or CONF_NAME not in yaml[CONF_ESPHOME]: + print( + color( + Fore.BOLD_RED, "Complex YAML files cannot be automatically renamed." + ) + ) + return 1 + old_name = yaml[CONF_ESPHOME][CONF_NAME] + match = re.match(r"^\$\{?([a-zA-Z0-9_]+)\}?$", old_name) + if match is None: + new_raw = re.sub( + rf"name:\s+[\"']?{old_name}[\"']?", + f'name: "{args.name}"', + raw_contents, + ) + else: + old_name = yaml[CONF_SUBSTITUTIONS][match.group(1)] + if ( + len( + re.findall( + rf"^\s+{match.group(1)}:\s+[\"']?{old_name}[\"']?", + raw_contents, + flags=re.MULTILINE, + ) + ) + > 1 + ): + print(color(Fore.BOLD_RED, "Too many matches in YAML to safely rename")) + return 1 + + new_raw = re.sub( + rf"^(\s+{match.group(1)}):\s+[\"']?{old_name}[\"']?", + f'\\1: "{args.name}"', + raw_contents, + flags=re.MULTILINE, + ) + + raw_file.seek(0) + raw_file.write(new_raw) + raw_file.flush() + + print(f"Updating {color(Fore.CYAN, CORE.config_path)}") + print() + + rc = run_external_process("esphome", "config", CORE.config_path) + if rc != 0: + raw_file.seek(0) + raw_file.write(raw_contents) + print(color(Fore.BOLD_RED, "Rename failed. Reverting changes.")) + return 1 + + cli_args = [ + "run", + CORE.config_path, + "--no-logs", + "--device", + CORE.address, + ] + + if args.dashboard: + cli_args.insert(0, "--dashboard") + + try: + rc = run_external_process("esphome", *cli_args) + except KeyboardInterrupt: + rc = 1 + if rc != 0: + raw_file.seek(0) + raw_file.write(raw_contents) + return 1 + + print(color(Fore.BOLD_GREEN, "SUCCESS")) + print() + return 0 + + PRE_CONFIG_ACTIONS = { "wizard": command_wizard, "version": command_version, @@ -499,6 +593,7 @@ POST_CONFIG_ACTIONS = { "mqtt-fingerprint": command_mqtt_fingerprint, "clean": command_clean, "idedata": command_idedata, + "rename": command_rename, } @@ -681,6 +776,12 @@ def parse_args(argv): "configuration", help="Your YAML configuration file(s).", nargs=1 ) + parser_rename = subparsers.add_parser("rename") + parser_rename.add_argument( + "configuration", help="Your YAML configuration file.", nargs=1 + ) + parser_rename.add_argument("name", help="The new name for the device.", type=str) + # Keep backward compatibility with the old command line format of # esphome . # From 6fe22a7e629edf9243ba822d77a75f4587e6afd2 Mon Sep 17 00:00:00 2001 From: Martin <25747549+martgras@users.noreply.github.com> Date: Mon, 25 Apr 2022 23:50:36 +0200 Subject: [PATCH 14/56] SPS30: Add fan action (#3410) * Add fan action to SPS30 * add codeowner --- CODEOWNERS | 1 + esphome/components/sps30/automation.h | 21 +++++++++++++++++++ esphome/components/sps30/sensor.py | 27 ++++++++++++++++++++++++ esphome/components/sps30/sps30.cpp | 30 ++++++++++++++++++++++++++- esphome/components/sps30/sps30.h | 5 ++++- 5 files changed, 82 insertions(+), 2 deletions(-) create mode 100644 esphome/components/sps30/automation.h diff --git a/CODEOWNERS b/CODEOWNERS index 7fd049f46e..e2a356360a 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -183,6 +183,7 @@ esphome/components/sm2135/* @BoukeHaarsma23 esphome/components/socket/* @esphome/core esphome/components/sonoff_d1/* @anatoly-savchenkov esphome/components/spi/* @esphome/core +esphome/components/sps30/* @martgras esphome/components/ssd1322_base/* @kbx81 esphome/components/ssd1322_spi/* @kbx81 esphome/components/ssd1325_base/* @kbx81 diff --git a/esphome/components/sps30/automation.h b/esphome/components/sps30/automation.h new file mode 100644 index 0000000000..443aafb575 --- /dev/null +++ b/esphome/components/sps30/automation.h @@ -0,0 +1,21 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "sps30.h" + +namespace esphome { +namespace sps30 { + +template class StartFanAction : public Action { + public: + explicit StartFanAction(SPS30Component *sps30) : sps30_(sps30) {} + + void play(Ts... x) override { this->sps30_->start_fan_cleaning(); } + + protected: + SPS30Component *sps30_; +}; + +} // namespace sps30 +} // namespace esphome diff --git a/esphome/components/sps30/sensor.py b/esphome/components/sps30/sensor.py index 89cb25c24f..ff8d5a3594 100644 --- a/esphome/components/sps30/sensor.py +++ b/esphome/components/sps30/sensor.py @@ -1,6 +1,8 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import i2c, sensor, sensirion_common +from esphome import automation +from esphome.automation import maybe_simple_id from esphome.const import ( CONF_ID, CONF_PM_1_0, @@ -25,6 +27,7 @@ from esphome.const import ( ICON_RULER, ) +CODEOWNERS = ["@martgras"] DEPENDENCIES = ["i2c"] AUTO_LOAD = ["sensirion_common"] @@ -33,6 +36,11 @@ SPS30Component = sps30_ns.class_( "SPS30Component", cg.PollingComponent, sensirion_common.SensirionI2CDevice ) +# Actions +StartFanAction = sps30_ns.class_("StartFanAction", automation.Action) + +CONF_AUTO_CLEANING_INTERVAL = "auto_cleaning_interval" + CONFIG_SCHEMA = ( cv.Schema( { @@ -100,6 +108,7 @@ CONFIG_SCHEMA = ( accuracy_decimals=0, state_class=STATE_CLASS_MEASUREMENT, ), + cv.Optional(CONF_AUTO_CLEANING_INTERVAL): cv.update_interval, } ) .extend(cv.polling_component_schema("60s")) @@ -151,3 +160,21 @@ async def to_code(config): if CONF_PM_SIZE in config: sens = await sensor.new_sensor(config[CONF_PM_SIZE]) cg.add(var.set_pm_size_sensor(sens)) + + if CONF_AUTO_CLEANING_INTERVAL in config: + cg.add(var.set_auto_cleaning_interval(config[CONF_AUTO_CLEANING_INTERVAL])) + + +SPS30_ACTION_SCHEMA = maybe_simple_id( + { + cv.Required(CONF_ID): cv.use_id(SPS30Component), + } +) + + +@automation.register_action( + "sps30.start_fan_autoclean", StartFanAction, SPS30_ACTION_SCHEMA +) +async def sps30_fan_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, paren) diff --git a/esphome/components/sps30/sps30.cpp b/esphome/components/sps30/sps30.cpp index 2885125a8a..cdcd4a6a54 100644 --- a/esphome/components/sps30/sps30.cpp +++ b/esphome/components/sps30/sps30.cpp @@ -1,5 +1,6 @@ -#include "sps30.h" +#include "esphome/core/hal.h" #include "esphome/core/log.h" +#include "sps30.h" namespace esphome { namespace sps30 { @@ -44,6 +45,22 @@ void SPS30Component::setup() { this->serial_number_[i * 2 + 1] = uint16_t(uint16_t(raw_serial_number[i] & 0xFF)); } ESP_LOGD(TAG, " Serial Number: '%s'", this->serial_number_); + + bool result; + if (this->fan_interval_.has_value()) { + // override default value + result = write_command(SPS30_CMD_SET_AUTOMATIC_CLEANING_INTERVAL_SECONDS, this->fan_interval_.value()); + } else { + result = write_command(SPS30_CMD_SET_AUTOMATIC_CLEANING_INTERVAL_SECONDS); + } + if (result) { + delay(20); + uint16_t secs[2]; + if (this->read_data(secs, 2)) { + fan_interval_ = secs[0] << 16 | secs[1]; + } + } + this->status_clear_warning(); this->skipped_data_read_cycles_ = 0; this->start_continuous_measurement_(); @@ -206,5 +223,16 @@ bool SPS30Component::start_continuous_measurement_() { return true; } +bool SPS30Component::start_fan_cleaning() { + if (!write_command(SPS30_CMD_START_FAN_CLEANING)) { + this->status_set_warning(); + ESP_LOGE(TAG, "write error start fan (%d)", this->last_error_); + return false; + } else { + ESP_LOGD(TAG, "Fan auto clean started"); + } + return true; +} + } // namespace sps30 } // namespace esphome diff --git a/esphome/components/sps30/sps30.h b/esphome/components/sps30/sps30.h index 9a93df8597..cf2e7a7d4f 100644 --- a/esphome/components/sps30/sps30.h +++ b/esphome/components/sps30/sps30.h @@ -22,12 +22,14 @@ class SPS30Component : public PollingComponent, public sensirion_common::Sensiri void set_pmc_10_0_sensor(sensor::Sensor *pmc_10_0) { pmc_10_0_sensor_ = pmc_10_0; } void set_pm_size_sensor(sensor::Sensor *pm_size) { pm_size_sensor_ = pm_size; } - + void set_auto_cleaning_interval(uint32_t auto_cleaning_interval) { fan_interval_ = auto_cleaning_interval; } void setup() override; void update() override; void dump_config() override; float get_setup_priority() const override { return setup_priority::DATA; } + bool start_fan_cleaning(); + protected: char serial_number_[17] = {0}; /// Terminating NULL character uint16_t raw_firmware_version_; @@ -54,6 +56,7 @@ class SPS30Component : public PollingComponent, public sensirion_common::Sensiri sensor::Sensor *pmc_4_0_sensor_{nullptr}; sensor::Sensor *pmc_10_0_sensor_{nullptr}; sensor::Sensor *pm_size_sensor_{nullptr}; + optional fan_interval_; }; } // namespace sps30 From 3346bc8bba12add5050eb8b10078e7dc7872ed85 Mon Sep 17 00:00:00 2001 From: quentin9696 Date: Mon, 25 Apr 2022 18:09:49 -0400 Subject: [PATCH 15/56] feat: add openssh-client on docker image (#1681) (#3319) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- docker/Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/docker/Dockerfile b/docker/Dockerfile index 610b689298..dc8ba03f48 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -30,6 +30,7 @@ RUN \ iputils-ping=3:20210202-1 \ git=1:2.30.2-1 \ curl=7.74.0-1.3+deb11u1 \ + openssh-client=1:8.4p1-5 \ && rm -rf \ /tmp/* \ /var/{cache,log}/* \ From 256395c28d615197e129ebafccd4e530a32f8d5b Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 26 Apr 2022 21:02:08 +1200 Subject: [PATCH 16/56] Add duration device class for sensors (#3421) --- esphome/components/sensor/__init__.py | 2 ++ esphome/const.py | 1 + 2 files changed, 3 insertions(+) diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index 0c38ceeb37..d01a594889 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -29,6 +29,7 @@ from esphome.const import ( CONF_WINDOW_SIZE, CONF_MQTT_ID, CONF_FORCE_UPDATE, + DEVICE_CLASS_DURATION, DEVICE_CLASS_EMPTY, DEVICE_CLASS_AQI, DEVICE_CLASS_BATTERY, @@ -70,6 +71,7 @@ DEVICE_CLASSES = [ DEVICE_CLASS_CARBON_DIOXIDE, DEVICE_CLASS_CARBON_MONOXIDE, DEVICE_CLASS_CURRENT, + DEVICE_CLASS_DURATION, DEVICE_CLASS_ENERGY, DEVICE_CLASS_GAS, DEVICE_CLASS_HUMIDITY, diff --git a/esphome/const.py b/esphome/const.py index fa5baf4fe2..9f2bed28d1 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -905,6 +905,7 @@ DEVICE_CLASS_AQI = "aqi" DEVICE_CLASS_CARBON_DIOXIDE = "carbon_dioxide" DEVICE_CLASS_CARBON_MONOXIDE = "carbon_monoxide" DEVICE_CLASS_CURRENT = "current" +DEVICE_CLASS_DURATION = "duration" DEVICE_CLASS_ENERGY = "energy" DEVICE_CLASS_HUMIDITY = "humidity" DEVICE_CLASS_ILLUMINANCE = "illuminance" From 2bff9937b761f61821c804703b021f1c7c910fc4 Mon Sep 17 00:00:00 2001 From: code-review-doctor <72647856+code-review-doctor@users.noreply.github.com> Date: Tue, 26 Apr 2022 20:43:35 +0100 Subject: [PATCH 17/56] Fix issue probably-meant-fstring found at https://codereview.doctor (#3415) --- esphome/components/esp32/gpio.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/esp32/gpio.py b/esphome/components/esp32/gpio.py index 5819943f37..6c3fa92fcd 100644 --- a/esphome/components/esp32/gpio.py +++ b/esphome/components/esp32/gpio.py @@ -107,7 +107,7 @@ def validate_gpio_pin(value): value = _translate_pin(value) variant = CORE.data[KEY_ESP32][KEY_VARIANT] if variant not in _esp32_validations: - raise cv.Invalid("Unsupported ESP32 variant {variant}") + raise cv.Invalid(f"Unsupported ESP32 variant {variant}") return _esp32_validations[variant].pin_validation(value) @@ -121,7 +121,7 @@ def validate_supports(value): is_pulldown = mode[CONF_PULLDOWN] variant = CORE.data[KEY_ESP32][KEY_VARIANT] if variant not in _esp32_validations: - raise cv.Invalid("Unsupported ESP32 variant {variant}") + raise cv.Invalid(f"Unsupported ESP32 variant {variant}") if is_open_drain and not is_output: raise cv.Invalid( From ebf13a0ba0a48453c2cc80bfc942d6a588859233 Mon Sep 17 00:00:00 2001 From: Trevor North Date: Tue, 26 Apr 2022 20:51:22 +0100 Subject: [PATCH 18/56] Queue sensor publishes so we don't block for too long (#3422) --- .../components/bme680_bsec/bme680_bsec.cpp | 44 ++++++++++++------- esphome/components/bme680_bsec/bme680_bsec.h | 8 +++- 2 files changed, 33 insertions(+), 19 deletions(-) diff --git a/esphome/components/bme680_bsec/bme680_bsec.cpp b/esphome/components/bme680_bsec/bme680_bsec.cpp index 0a8ca7f3c3..b84ca3318b 100644 --- a/esphome/components/bme680_bsec/bme680_bsec.cpp +++ b/esphome/components/bme680_bsec/bme680_bsec.cpp @@ -169,6 +169,14 @@ void BME680BSECComponent::loop() { } else { this->status_clear_warning(); } + + // Process a single action from the queue. These are primarily sensor state publishes + // that in totality take too long to send in a single call. + if (this->queue_.size()) { + auto action = std::move(this->queue_.front()); + this->queue_.pop(); + action(); + } } void BME680BSECComponent::run_() { @@ -306,37 +314,39 @@ void BME680BSECComponent::read_(int64_t trigger_time_ns, bsec_bme_settings_t bme } void BME680BSECComponent::publish_(const bsec_output_t *outputs, uint8_t num_outputs) { - ESP_LOGV(TAG, "Publishing sensor states"); + ESP_LOGV(TAG, "Queuing sensor state publish actions"); for (uint8_t i = 0; i < num_outputs; i++) { + float signal = outputs[i].signal; switch (outputs[i].sensor_id) { case BSEC_OUTPUT_IAQ: - case BSEC_OUTPUT_STATIC_IAQ: - uint8_t accuracy; - accuracy = outputs[i].accuracy; - this->publish_sensor_state_(this->iaq_sensor_, outputs[i].signal); - this->publish_sensor_state_(this->iaq_accuracy_text_sensor_, IAQ_ACCURACY_STATES[accuracy]); - this->publish_sensor_state_(this->iaq_accuracy_sensor_, accuracy, true); + case BSEC_OUTPUT_STATIC_IAQ: { + uint8_t accuracy = outputs[i].accuracy; + this->queue_push_([this, signal]() { this->publish_sensor_(this->iaq_sensor_, signal); }); + this->queue_push_([this, accuracy]() { + this->publish_sensor_(this->iaq_accuracy_text_sensor_, IAQ_ACCURACY_STATES[accuracy]); + }); + this->queue_push_([this, accuracy]() { this->publish_sensor_(this->iaq_accuracy_sensor_, accuracy, true); }); // Queue up an opportunity to save state - this->defer("save_state", [this, accuracy]() { this->save_state_(accuracy); }); - break; + this->queue_push_([this, accuracy]() { this->save_state_(accuracy); }); + } break; case BSEC_OUTPUT_CO2_EQUIVALENT: - this->publish_sensor_state_(this->co2_equivalent_sensor_, outputs[i].signal); + this->queue_push_([this, signal]() { this->publish_sensor_(this->co2_equivalent_sensor_, signal); }); break; case BSEC_OUTPUT_BREATH_VOC_EQUIVALENT: - this->publish_sensor_state_(this->breath_voc_equivalent_sensor_, outputs[i].signal); + this->queue_push_([this, signal]() { this->publish_sensor_(this->breath_voc_equivalent_sensor_, signal); }); break; case BSEC_OUTPUT_RAW_PRESSURE: - this->publish_sensor_state_(this->pressure_sensor_, outputs[i].signal / 100.0f); + this->queue_push_([this, signal]() { this->publish_sensor_(this->pressure_sensor_, signal / 100.0f); }); break; case BSEC_OUTPUT_RAW_GAS: - this->publish_sensor_state_(this->gas_resistance_sensor_, outputs[i].signal); + this->queue_push_([this, signal]() { this->publish_sensor_(this->gas_resistance_sensor_, signal); }); break; case BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_TEMPERATURE: - this->publish_sensor_state_(this->temperature_sensor_, outputs[i].signal); + this->queue_push_([this, signal]() { this->publish_sensor_(this->temperature_sensor_, signal); }); break; case BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_HUMIDITY: - this->publish_sensor_state_(this->humidity_sensor_, outputs[i].signal); + this->queue_push_([this, signal]() { this->publish_sensor_(this->humidity_sensor_, signal); }); break; } } @@ -352,14 +362,14 @@ int64_t BME680BSECComponent::get_time_ns_() { return (time_ms + ((int64_t) this->millis_overflow_counter_ << 32)) * INT64_C(1000000); } -void BME680BSECComponent::publish_sensor_state_(sensor::Sensor *sensor, float value, bool change_only) { +void BME680BSECComponent::publish_sensor_(sensor::Sensor *sensor, float value, bool change_only) { if (!sensor || (change_only && sensor->has_state() && sensor->state == value)) { return; } sensor->publish_state(value); } -void BME680BSECComponent::publish_sensor_state_(text_sensor::TextSensor *sensor, const std::string &value) { +void BME680BSECComponent::publish_sensor_(text_sensor::TextSensor *sensor, const std::string &value) { if (!sensor || (sensor->has_state() && sensor->state == value)) { return; } diff --git a/esphome/components/bme680_bsec/bme680_bsec.h b/esphome/components/bme680_bsec/bme680_bsec.h index 53bc5c3280..650b4d2413 100644 --- a/esphome/components/bme680_bsec/bme680_bsec.h +++ b/esphome/components/bme680_bsec/bme680_bsec.h @@ -70,12 +70,14 @@ class BME680BSECComponent : public Component, public i2c::I2CDevice { void publish_(const bsec_output_t *outputs, uint8_t num_outputs); int64_t get_time_ns_(); - void publish_sensor_state_(sensor::Sensor *sensor, float value, bool change_only = false); - void publish_sensor_state_(text_sensor::TextSensor *sensor, const std::string &value); + void publish_sensor_(sensor::Sensor *sensor, float value, bool change_only = false); + void publish_sensor_(text_sensor::TextSensor *sensor, const std::string &value); void load_state_(); void save_state_(uint8_t accuracy); + void queue_push_(std::function &&f) { this->queue_.push(std::move(f)); } + struct bme680_dev bme680_; bsec_library_return_t bsec_status_{BSEC_OK}; int8_t bme680_status_{BME680_OK}; @@ -84,6 +86,8 @@ class BME680BSECComponent : public Component, public i2c::I2CDevice { uint32_t millis_overflow_counter_{0}; int64_t next_call_ns_{0}; + std::queue> queue_; + ESPPreferenceObject bsec_state_; uint32_t state_save_interval_ms_{21600000}; // 6 hours - 4 times a day uint32_t last_state_save_ms_ = 0; From 68dfaf238b8d16f052d24086464fc5c4a476745d Mon Sep 17 00:00:00 2001 From: LuBeDa Date: Tue, 26 Apr 2022 22:41:10 +0200 Subject: [PATCH 19/56] added RGB565 image type (#3229) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/animation/__init__.py | 23 +++++++++++++ esphome/components/display/display_buffer.cpp | 32 +++++++++++++++++++ esphome/components/display/display_buffer.h | 3 ++ esphome/components/image/__init__.py | 16 ++++++++++ 4 files changed, 74 insertions(+) diff --git a/esphome/components/animation/__init__.py b/esphome/components/animation/__init__.py index 7c9ff07f97..4cf0b0ed7d 100644 --- a/esphome/components/animation/__init__.py +++ b/esphome/components/animation/__init__.py @@ -92,6 +92,29 @@ async def to_code(config): data[pos] = pix[2] pos += 1 + elif config[CONF_TYPE] == "RGB565": + data = [0 for _ in range(height * width * 2 * frames)] + pos = 0 + for frameIndex in range(frames): + image.seek(frameIndex) + frame = image.convert("RGB") + if CONF_RESIZE in config: + frame = frame.resize([width, height]) + pixels = list(frame.getdata()) + if len(pixels) != height * width: + raise core.EsphomeError( + f"Unexpected number of pixels in {path} frame {frameIndex}: ({len(pixels)} != {height*width})" + ) + for pix in pixels: + R = pix[0] >> 3 + G = pix[1] >> 2 + B = pix[2] >> 3 + rgb = (R << 11) | (G << 5) | B + data[pos] = rgb >> 8 + pos += 1 + data[pos] = rgb & 255 + pos += 1 + elif config[CONF_TYPE] == "BINARY": width8 = ((width + 7) // 8) * 8 data = [0 for _ in range((height * width8 // 8) * frames)] diff --git a/esphome/components/display/display_buffer.cpp b/esphome/components/display/display_buffer.cpp index 4ad353a254..d00fdd5240 100644 --- a/esphome/components/display/display_buffer.cpp +++ b/esphome/components/display/display_buffer.cpp @@ -242,6 +242,13 @@ void DisplayBuffer::image(int x, int y, Image *image, Color color_on, Color colo } } break; + case IMAGE_TYPE_RGB565: + for (int img_x = 0; img_x < image->get_width(); img_x++) { + for (int img_y = 0; img_y < image->get_height(); img_y++) { + this->draw_pixel_at(x + img_x, y + img_y, image->get_rgb565_pixel(img_x, img_y)); + } + } + break; } } @@ -497,6 +504,17 @@ Color Image::get_color_pixel(int x, int y) const { (progmem_read_byte(this->data_start_ + pos + 0) << 16); return Color(color32); } +Color Image::get_rgb565_pixel(int x, int y) const { + if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) + return Color::BLACK; + const uint32_t pos = (x + y * this->width_) * 2; + uint16_t rgb565 = + progmem_read_byte(this->data_start_ + pos + 0) << 8 | progmem_read_byte(this->data_start_ + pos + 1); + auto r = (rgb565 & 0xF800) >> 11; + auto g = (rgb565 & 0x07E0) >> 5; + auto b = rgb565 & 0x001F; + return Color((r << 3) | (r >> 2), (g << 2) | (g >> 4), (b << 3) | (b >> 2)); +} Color Image::get_grayscale_pixel(int x, int y) const { if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) return Color::BLACK; @@ -532,6 +550,20 @@ Color Animation::get_color_pixel(int x, int y) const { (progmem_read_byte(this->data_start_ + pos + 0) << 16); return Color(color32); } +Color Animation::get_rgb565_pixel(int x, int y) const { + if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) + return Color::BLACK; + const uint32_t frame_index = this->width_ * this->height_ * this->current_frame_; + if (frame_index >= (uint32_t)(this->width_ * this->height_ * this->animation_frame_count_)) + return Color::BLACK; + const uint32_t pos = (x + y * this->width_ + frame_index) * 2; + uint16_t rgb565 = + progmem_read_byte(this->data_start_ + pos + 0) << 8 | progmem_read_byte(this->data_start_ + pos + 1); + auto r = (rgb565 & 0xF800) >> 11; + auto g = (rgb565 & 0x07E0) >> 5; + auto b = rgb565 & 0x001F; + return Color((r << 3) | (r >> 2), (g << 2) | (g >> 4), (b << 3) | (b >> 2)); +} Color Animation::get_grayscale_pixel(int x, int y) const { if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) return Color::BLACK; diff --git a/esphome/components/display/display_buffer.h b/esphome/components/display/display_buffer.h index 8ee1cd8779..86221c5f96 100644 --- a/esphome/components/display/display_buffer.h +++ b/esphome/components/display/display_buffer.h @@ -82,6 +82,7 @@ enum ImageType { IMAGE_TYPE_GRAYSCALE = 1, IMAGE_TYPE_RGB24 = 2, IMAGE_TYPE_TRANSPARENT_BINARY = 3, + IMAGE_TYPE_RGB565 = 4, }; enum DisplayRotation { @@ -453,6 +454,7 @@ class Image { Image(const uint8_t *data_start, int width, int height, ImageType type); virtual bool get_pixel(int x, int y) const; virtual Color get_color_pixel(int x, int y) const; + virtual Color get_rgb565_pixel(int x, int y) const; virtual Color get_grayscale_pixel(int x, int y) const; int get_width() const; int get_height() const; @@ -470,6 +472,7 @@ class Animation : public Image { Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, ImageType type); bool get_pixel(int x, int y) const override; Color get_color_pixel(int x, int y) const override; + Color get_rgb565_pixel(int x, int y) const override; Color get_grayscale_pixel(int x, int y) const override; int get_animation_frame_count() const; diff --git a/esphome/components/image/__init__.py b/esphome/components/image/__init__.py index 70d77dfd14..0004391f20 100644 --- a/esphome/components/image/__init__.py +++ b/esphome/components/image/__init__.py @@ -25,6 +25,7 @@ IMAGE_TYPE = { "GRAYSCALE": ImageType.IMAGE_TYPE_GRAYSCALE, "RGB24": ImageType.IMAGE_TYPE_RGB24, "TRANSPARENT_BINARY": ImageType.IMAGE_TYPE_TRANSPARENT_BINARY, + "RGB565": ImageType.IMAGE_TYPE_RGB565, } Image_ = display.display_ns.class_("Image") @@ -89,6 +90,21 @@ async def to_code(config): data[pos] = pix[2] pos += 1 + elif config[CONF_TYPE] == "RGB565": + image = image.convert("RGB") + pixels = list(image.getdata()) + data = [0 for _ in range(height * width * 3)] + pos = 0 + for pix in pixels: + R = pix[0] >> 3 + G = pix[1] >> 2 + B = pix[2] >> 3 + rgb = (R << 11) | (G << 5) | B + data[pos] = rgb >> 8 + pos += 1 + data[pos] = rgb & 255 + pos += 1 + elif config[CONF_TYPE] == "BINARY": image = image.convert("1", dither=dither) width8 = ((width + 7) // 8) * 8 From 91895aa70c6f82866c182dee626b017ca2dd36a8 Mon Sep 17 00:00:00 2001 From: Dan Jackson Date: Tue, 3 May 2022 00:09:06 -0700 Subject: [PATCH 20/56] Allow wifi output_power down to 8.5dB (#3405) --- esphome/components/wifi/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index b56902df2f..c3f70506e2 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -270,7 +270,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_FAST_CONNECT, default=False): cv.boolean, cv.Optional(CONF_USE_ADDRESS): cv.string_strict, cv.SplitDefault(CONF_OUTPUT_POWER, esp8266=20.0): cv.All( - cv.decibel, cv.float_range(min=10.0, max=20.5) + cv.decibel, cv.float_range(min=8.5, max=20.5) ), cv.Optional("enable_mdns"): cv.invalid( "This option has been removed. Please use the [disabled] option under the " From 64fb39a653be8633942cce7733f8379b6b0550ca Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 9 May 2022 10:18:24 +1200 Subject: [PATCH 21/56] Add help text to rename command (#3442) --- esphome/__main__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index 00770d6f05..80e8455465 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -776,7 +776,10 @@ def parse_args(argv): "configuration", help="Your YAML configuration file(s).", nargs=1 ) - parser_rename = subparsers.add_parser("rename") + parser_rename = subparsers.add_parser( + "rename", + help="Rename a device in YAML, compile the binary and upload it.", + ) parser_rename.add_argument( "configuration", help="Your YAML configuration file.", nargs=1 ) From 7c30d6254e0ee2d9656525cd2345411522972ec4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 8 May 2022 18:53:34 -0700 Subject: [PATCH 22/56] Add rename command handler (#3443) --- esphome/dashboard/dashboard.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/esphome/dashboard/dashboard.py b/esphome/dashboard/dashboard.py index af68f2ae08..b78d22cf7c 100644 --- a/esphome/dashboard/dashboard.py +++ b/esphome/dashboard/dashboard.py @@ -283,6 +283,18 @@ class EsphomeLogsHandler(EsphomeCommandWebSocket): ] +class EsphomeRenameHandler(EsphomeCommandWebSocket): + def build_command(self, json_message): + config_file = settings.rel_path(json_message["configuration"]) + return [ + "esphome", + "--dashboard", + "rename", + config_file, + json_message["newName"], + ] + + class EsphomeUploadHandler(EsphomeCommandWebSocket): def build_command(self, json_message): config_file = settings.rel_path(json_message["configuration"]) @@ -971,6 +983,7 @@ def make_app(debug=get_bool_env(ENV_DEV)): (f"{rel}devices", ListDevicesHandler), (f"{rel}import", ImportRequestHandler), (f"{rel}secret_keys", SecretKeysRequestHandler), + (f"{rel}rename", EsphomeRenameHandler), ], **app_settings, ) From d2f37cf3f9a87576f7a1b069cf5d9774e9caffaa Mon Sep 17 00:00:00 2001 From: Jens-Christian Skibakk Date: Mon, 9 May 2022 06:17:22 +0200 Subject: [PATCH 23/56] Support for Arduino 2 and serial port on ESP32-S2 and ESP32-C3 (#3436) --- .../improv_serial/improv_serial_component.h | 2 +- esphome/components/logger/logger.cpp | 22 ++++++++++--------- esphome/components/logger/logger.h | 4 ++-- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/esphome/components/improv_serial/improv_serial_component.h b/esphome/components/improv_serial/improv_serial_component.h index c6b980ab99..6be5704b71 100644 --- a/esphome/components/improv_serial/improv_serial_component.h +++ b/esphome/components/improv_serial/improv_serial_component.h @@ -51,7 +51,7 @@ class ImprovSerialComponent : public Component { void write_data_(std::vector &data); #ifdef USE_ARDUINO - HardwareSerial *hw_serial_{nullptr}; + Stream *hw_serial_{nullptr}; #endif #ifdef USE_ESP_IDF uart_port_t uart_num_; diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index 11c0733701..3f4e4e7753 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -149,13 +149,25 @@ void Logger::pre_setup() { case UART_SELECTION_UART0_SWAP: #endif this->hw_serial_ = &Serial; + Serial.begin(this->baud_rate_); +#ifdef USE_ESP8266 + if (this->uart_ == UART_SELECTION_UART0_SWAP) { + Serial.swap(); + } + Serial.setDebugOutput(ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE); +#endif break; case UART_SELECTION_UART1: this->hw_serial_ = &Serial1; + Serial1.begin(this->baud_rate_); +#ifdef USE_ESP8266 + Serial1.setDebugOutput(ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE); +#endif break; #if defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32S2) case UART_SELECTION_UART2: this->hw_serial_ = &Serial2; + Serial2.begin(this->baud_rate_); break; #endif } @@ -186,16 +198,6 @@ void Logger::pre_setup() { // Install UART driver using an event queue here uart_driver_install(uart_num_, uart_buffer_size, uart_buffer_size, 10, nullptr, 0); #endif - -#ifdef USE_ARDUINO - this->hw_serial_->begin(this->baud_rate_); -#ifdef USE_ESP8266 - if (this->uart_ == UART_SELECTION_UART0_SWAP) { - this->hw_serial_->swap(); - } - this->hw_serial_->setDebugOutput(ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE); -#endif -#endif // USE_ARDUINO } #ifdef USE_ESP8266 else { diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index 8756bc2387..fa93972e19 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -40,7 +40,7 @@ class Logger : public Component { void set_baud_rate(uint32_t baud_rate); uint32_t get_baud_rate() const { return baud_rate_; } #ifdef USE_ARDUINO - HardwareSerial *get_hw_serial() const { return hw_serial_; } + Stream *get_hw_serial() const { return hw_serial_; } #endif #ifdef USE_ESP_IDF uart_port_t get_uart_num() const { return uart_num_; } @@ -119,7 +119,7 @@ class Logger : public Component { int tx_buffer_size_{0}; UARTSelection uart_{UART_SELECTION_UART0}; #ifdef USE_ARDUINO - HardwareSerial *hw_serial_{nullptr}; + Stream *hw_serial_{nullptr}; #endif #ifdef USE_ESP_IDF uart_port_t uart_num_; From 6f88f0ea3f229ef91652ba35d88d2d5116ca941c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 8 May 2022 22:17:21 -0700 Subject: [PATCH 24/56] Bump dashboard to 20220508.0 (#3448) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 465d961cb6..543999a9f3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,7 @@ pyserial==3.5 platformio==5.2.5 # When updating platformio, also update Dockerfile esptool==3.3 click==8.1.2 -esphome-dashboard==20220309.0 +esphome-dashboard==20220508.0 aioesphomeapi==10.8.2 zeroconf==0.38.4 From 8e3af515c9941d4cd9535cb4388ce31c724fce18 Mon Sep 17 00:00:00 2001 From: Patrick van der Leer Date: Mon, 9 May 2022 07:17:36 +0200 Subject: [PATCH 25/56] Waveshare epaper 7in5 v2alt (#3276) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- .../components/waveshare_epaper/display.py | 4 + .../waveshare_epaper/waveshare_epaper.cpp | 137 ++++++++++++++++++ .../waveshare_epaper/waveshare_epaper.h | 19 +++ 3 files changed, 160 insertions(+) diff --git a/esphome/components/waveshare_epaper/display.py b/esphome/components/waveshare_epaper/display.py index fe5b51290e..a9d8350404 100644 --- a/esphome/components/waveshare_epaper/display.py +++ b/esphome/components/waveshare_epaper/display.py @@ -50,6 +50,9 @@ WaveshareEPaper7P5InBV2 = waveshare_epaper_ns.class_( WaveshareEPaper7P5InV2 = waveshare_epaper_ns.class_( "WaveshareEPaper7P5InV2", WaveshareEPaper ) +WaveshareEPaper7P5InV2alt = waveshare_epaper_ns.class_( + "WaveshareEPaper7P5InV2alt", WaveshareEPaper +) WaveshareEPaper7P5InHDB = waveshare_epaper_ns.class_( "WaveshareEPaper7P5InHDB", WaveshareEPaper ) @@ -79,6 +82,7 @@ MODELS = { "7.50in-bv2": ("b", WaveshareEPaper7P5InBV2), "7.50in-bc": ("b", WaveshareEPaper7P5InBC), "7.50inv2": ("b", WaveshareEPaper7P5InV2), + "7.50inv2alt": ("b", WaveshareEPaper7P5InV2alt), "7.50in-hd-b": ("b", WaveshareEPaper7P5InHDB), "2.13in-ttgo-dke": ("c", WaveshareEPaper2P13InDKE), } diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.cpp b/esphome/components/waveshare_epaper/waveshare_epaper.cpp index 59b3e90b03..5580674c34 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.cpp +++ b/esphome/components/waveshare_epaper/waveshare_epaper.cpp @@ -1137,6 +1137,143 @@ void HOT WaveshareEPaper7P5InV2::display() { int WaveshareEPaper7P5InV2::get_width_internal() { return 800; } int WaveshareEPaper7P5InV2::get_height_internal() { return 480; } void WaveshareEPaper7P5InV2::dump_config() { + LOG_DISPLAY("", "Waveshare E-Paper", this); + ESP_LOGCONFIG(TAG, " Model: 7.5inV2rev2"); + LOG_PIN(" Reset Pin: ", this->reset_pin_); + LOG_PIN(" DC Pin: ", this->dc_pin_); + LOG_PIN(" Busy Pin: ", this->busy_pin_); + LOG_UPDATE_INTERVAL(this); +} + +/* 7.50inV2alt */ +bool WaveshareEPaper7P5InV2alt::wait_until_idle_() { + if (this->busy_pin_ == nullptr) { + return true; + } + + const uint32_t start = millis(); + while (this->busy_pin_->digital_read()) { + this->command(0x71); + if (millis() - start > this->idle_timeout_()) { + ESP_LOGI(TAG, "Timeout while displaying image!"); + return false; + } + delay(10); + } + return true; +} + +void WaveshareEPaper7P5InV2alt::initialize() { + this->reset_(); + + // COMMAND POWER SETTING + this->command(0x01); + + // 1-0=11: internal power + this->data(0x17); + + this->data(0x17); // VGH&VGL + this->data(0x3F); // VSH + this->data(0x3F); // VSL + this->data(0x11); // VSHR + + // VCOM DC Setting + this->command(0x82); + this->data(0x24); // VCOM + + // Booster Setting + this->command(0x06); + this->data(0x27); + this->data(0x27); + this->data(0x2F); + this->data(0x17); + + // OSC Setting + this->command(0x30); + this->data(0x06); // 2-0=100: N=4 ; 5-3=111: M=7 ; 3C=50Hz 3A=100HZ + + // POWER ON + this->command(0x04); + + delay(100); // NOLINT + this->wait_until_idle_(); + // COMMAND PANEL SETTING + this->command(0x00); + this->data(0x3F); // KW-3f KWR-2F BWROTP 0f BWOTP 1f + + // COMMAND RESOLUTION SETTING + this->command(0x61); + this->data(0x03); // source 800 + this->data(0x20); + this->data(0x01); // gate 480 + this->data(0xE0); + // COMMAND ...? + this->command(0x15); + this->data(0x00); + // COMMAND VCOM AND DATA INTERVAL SETTING + this->command(0x50); + this->data(0x10); + this->data(0x07); + // COMMAND TCON SETTING + this->command(0x60); + this->data(0x22); + // Resolution setting + this->command(0x65); + this->data(0x00); + this->data(0x00); // 800*480 + this->data(0x00); + this->data(0x00); + + this->wait_until_idle_(); + + uint8_t lut_vcom_7_i_n5_v2[] = { + 0x0, 0xF, 0xF, 0x0, 0x0, 0x1, 0x0, 0xF, 0x1, 0xF, 0x1, 0x2, 0x0, 0xF, 0xF, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + }; + + uint8_t lut_ww_7_i_n5_v2[] = { + 0x10, 0xF, 0xF, 0x0, 0x0, 0x1, 0x84, 0xF, 0x1, 0xF, 0x1, 0x2, 0x20, 0xF, 0xF, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + }; + + uint8_t lut_bw_7_i_n5_v2[] = { + 0x10, 0xF, 0xF, 0x0, 0x0, 0x1, 0x84, 0xF, 0x1, 0xF, 0x1, 0x2, 0x20, 0xF, 0xF, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + }; + + uint8_t lut_wb_7_i_n5_v2[] = { + 0x80, 0xF, 0xF, 0x0, 0x0, 0x3, 0x84, 0xF, 0x1, 0xF, 0x1, 0x4, 0x40, 0xF, 0xF, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + }; + + uint8_t lut_bb_7_i_n5_v2[] = { + 0x80, 0xF, 0xF, 0x0, 0x0, 0x1, 0x84, 0xF, 0x1, 0xF, 0x1, 0x2, 0x40, 0xF, 0xF, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + }; + + uint8_t count; + this->command(0x20); // VCOM + for (count = 0; count < 42; count++) + this->data(lut_vcom_7_i_n5_v2[count]); + + this->command(0x21); // LUTBW + for (count = 0; count < 42; count++) + this->data(lut_ww_7_i_n5_v2[count]); + + this->command(0x22); // LUTBW + for (count = 0; count < 42; count++) + this->data(lut_bw_7_i_n5_v2[count]); + + this->command(0x23); // LUTWB + for (count = 0; count < 42; count++) + this->data(lut_wb_7_i_n5_v2[count]); + + this->command(0x24); // LUTBB + for (count = 0; count < 42; count++) + this->data(lut_bb_7_i_n5_v2[count]); +} + +void WaveshareEPaper7P5InV2alt::dump_config() { LOG_DISPLAY("", "Waveshare E-Paper", this); ESP_LOGCONFIG(TAG, " Model: 7.5inV2"); LOG_PIN(" Reset Pin: ", this->reset_pin_); diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.h b/esphome/components/waveshare_epaper/waveshare_epaper.h index 41b93978ab..7a88fecbdb 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.h +++ b/esphome/components/waveshare_epaper/waveshare_epaper.h @@ -350,6 +350,25 @@ class WaveshareEPaper7P5InV2 : public WaveshareEPaper { int get_height_internal() override; }; +class WaveshareEPaper7P5InV2alt : public WaveshareEPaper7P5InV2 { + public: + bool wait_until_idle_(); + void initialize() override; + void dump_config() override; + + protected: + void reset_() { + if (this->reset_pin_ != nullptr) { + this->reset_pin_->digital_write(true); + delay(200); // NOLINT + this->reset_pin_->digital_write(false); + delay(2); + this->reset_pin_->digital_write(true); + delay(20); + } + }; +}; + class WaveshareEPaper7P5InHDB : public WaveshareEPaper { public: void initialize() override; From 2059283707fb0145dcf920d76e90afb6d80a20fb Mon Sep 17 00:00:00 2001 From: rainero84 Date: Mon, 9 May 2022 07:21:43 +0200 Subject: [PATCH 26/56] Early pin init (#3439) * Added early_pin_init configuration parameter for ESP8266 platform * Added #include to core * Updated test3.yaml to include early_pin_init parameter Co-authored-by: Rainer Oellermann --- esphome/components/esp8266/__init__.py | 5 +++++ esphome/components/esp8266/const.py | 1 + esphome/components/esp8266/core.cpp | 3 +++ tests/test3.yaml | 6 ++++-- 4 files changed, 13 insertions(+), 2 deletions(-) diff --git a/esphome/components/esp8266/__init__.py b/esphome/components/esp8266/__init__.py index 7b1be32e38..41d7688d44 100644 --- a/esphome/components/esp8266/__init__.py +++ b/esphome/components/esp8266/__init__.py @@ -19,6 +19,7 @@ from esphome.helpers import copy_file_if_changed from .const import ( CONF_RESTORE_FROM_FLASH, + CONF_EARLY_PIN_INIT, KEY_BOARD, KEY_ESP8266, KEY_PIN_INITIAL_STATES, @@ -148,6 +149,7 @@ CONFIG_SCHEMA = cv.All( cv.Required(CONF_BOARD): cv.string_strict, cv.Optional(CONF_FRAMEWORK, default={}): ARDUINO_FRAMEWORK_SCHEMA, cv.Optional(CONF_RESTORE_FROM_FLASH, default=False): cv.boolean, + cv.Optional(CONF_EARLY_PIN_INIT, default=True): cv.boolean, cv.Optional(CONF_BOARD_FLASH_MODE, default="dout"): cv.one_of( *BUILD_FLASH_MODES, lower=True ), @@ -197,6 +199,9 @@ async def to_code(config): if config[CONF_RESTORE_FROM_FLASH]: cg.add_define("USE_ESP8266_PREFERENCES_FLASH") + if config[CONF_EARLY_PIN_INIT]: + cg.add_define("USE_ESP8266_EARLY_PIN_INIT") + # Arduino 2 has a non-standards conformant new that returns a nullptr instead of failing when # out of memory and exceptions are disabled. Since Arduino 2.6.0, this flag can be used to make # new abort instead. Use it so that OOM fails early (on allocation) instead of on dereference of diff --git a/esphome/components/esp8266/const.py b/esphome/components/esp8266/const.py index 70429297e0..7740a97ff4 100644 --- a/esphome/components/esp8266/const.py +++ b/esphome/components/esp8266/const.py @@ -4,6 +4,7 @@ KEY_ESP8266 = "esp8266" KEY_BOARD = "board" KEY_PIN_INITIAL_STATES = "pin_initial_states" CONF_RESTORE_FROM_FLASH = "restore_from_flash" +CONF_EARLY_PIN_INIT = "early_pin_init" # esp8266 namespace is already defined by arduino, manually prefix esphome esp8266_ns = cg.global_ns.namespace("esphome").namespace("esp8266") diff --git a/esphome/components/esp8266/core.cpp b/esphome/components/esp8266/core.cpp index a9460f51f2..2d3959b031 100644 --- a/esphome/components/esp8266/core.cpp +++ b/esphome/components/esp8266/core.cpp @@ -1,6 +1,7 @@ #ifdef USE_ESP8266 #include "core.h" +#include "esphome/core/defines.h" #include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "preferences.h" @@ -55,6 +56,7 @@ extern "C" void resetPins() { // NOLINT // ourselves and this causes pins to toggle during reboot. force_link_symbols(); +#ifdef USE_ESP8266_EARLY_PIN_INIT for (int i = 0; i < 16; i++) { uint8_t mode = ESPHOME_ESP8266_GPIO_INITIAL_MODE[i]; uint8_t level = ESPHOME_ESP8266_GPIO_INITIAL_LEVEL[i]; @@ -63,6 +65,7 @@ extern "C" void resetPins() { // NOLINT if (level != 255) digitalWrite(i, level); // NOLINT } +#endif } } // namespace esphome diff --git a/tests/test3.yaml b/tests/test3.yaml index 29a70d3cc3..e3818d87ec 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -1,8 +1,6 @@ esphome: name: $device_name comment: $device_comment - platform: ESP8266 - board: d1_mini build_path: build/test3 on_boot: - if: @@ -15,6 +13,10 @@ esphome: includes: - custom.h +esp8266: + board: d1_mini + early_pin_init: True + substitutions: device_name: test3 device_comment: test3 device From 50a32b387e85f3f305b23a4d0c4d7e4fc1523045 Mon Sep 17 00:00:00 2001 From: Ingo Theiss Date: Mon, 9 May 2022 07:23:38 +0200 Subject: [PATCH 27/56] Add ENS210 Humidity & Temperature sensor component (#2942) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/ens210/__init__.py | 0 esphome/components/ens210/ens210.cpp | 230 ++++++++++++++++++++++++++ esphome/components/ens210/ens210.h | 39 +++++ esphome/components/ens210/sensor.py | 58 +++++++ tests/test1.yaml | 11 +- 6 files changed, 337 insertions(+), 2 deletions(-) create mode 100644 esphome/components/ens210/__init__.py create mode 100644 esphome/components/ens210/ens210.cpp create mode 100644 esphome/components/ens210/ens210.h create mode 100644 esphome/components/ens210/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index e2a356360a..51719ef1aa 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -60,6 +60,7 @@ esphome/components/dht/* @OttoWinter esphome/components/ds1307/* @badbadc0ffee esphome/components/dsmr/* @glmnet @zuidwijk esphome/components/ektf2232/* @jesserockz +esphome/components/ens210/* @itn3rd77 esphome/components/esp32/* @esphome/core esphome/components/esp32_ble/* @jesserockz esphome/components/esp32_ble_server/* @jesserockz diff --git a/esphome/components/ens210/__init__.py b/esphome/components/ens210/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/ens210/ens210.cpp b/esphome/components/ens210/ens210.cpp new file mode 100644 index 0000000000..9a89e85da2 --- /dev/null +++ b/esphome/components/ens210/ens210.cpp @@ -0,0 +1,230 @@ +// ENS210 relative humidity and temperature sensor with I2C interface from ScioSense +// +// Datasheet: https://www.sciosense.com/wp-content/uploads/2021/01/ENS210.pdf +// +// Implementation based on: +// https://github.com/maarten-pennings/ENS210 +// https://github.com/sciosense/ENS210_driver + +#include "ens210.h" +#include "esphome/core/log.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace ens210 { + +static const char *const TAG = "ens210"; + +// ENS210 chip constants +static const uint8_t ENS210_BOOTING_MS = 2; // Booting time in ms (also after reset, or going to high power) +static const uint8_t ENS210_SINGLE_MEASURMENT_CONVERSION_TIME_MS = + 130; // Conversion time in ms for single shot T/H measurement +static const uint16_t ENS210_PART_ID = 0x0210; // The expected part id of the ENS210 + +// Addresses of the ENS210 registers +static const uint8_t ENS210_REGISTER_PART_ID = 0x00; +static const uint8_t ENS210_REGISTER_UID = 0x04; +static const uint8_t ENS210_REGISTER_SYS_CTRL = 0x10; +static const uint8_t ENS210_REGISTER_SYS_STAT = 0x11; +static const uint8_t ENS210_REGISTER_SENS_RUN = 0x21; +static const uint8_t ENS210_REGISTER_SENS_START = 0x22; +static const uint8_t ENS210_REGISTER_SENS_STOP = 0x23; +static const uint8_t ENS210_REGISTER_SENS_STAT = 0x24; +static const uint8_t ENS210_REGISTER_T_VAL = 0x30; +static const uint8_t ENS210_REGISTER_H_VAL = 0x33; + +// CRC-7 constants +static const uint8_t CRC7_WIDTH = 7; // A 7 bits CRC has polynomial of 7th order, which has 8 terms +static const uint8_t CRC7_POLY = 0x89; // The 8 coefficients of the polynomial +static const uint8_t CRC7_IVEC = 0x7F; // Initial vector has all 7 bits high + +// Payload data constants +static const uint8_t DATA7_WIDTH = 17; +static const uint32_t DATA7_MASK = ((1UL << DATA7_WIDTH) - 1); // 0b 0 1111 1111 1111 1111 +static const uint32_t DATA7_MSB = (1UL << (DATA7_WIDTH - 1)); // 0b 1 0000 0000 0000 0000 + +// Converts a status to a human readable string +static const LogString *ens210_status_to_human(int status) { + switch (status) { + case ENS210Component::ENS210_STATUS_I2C_ERROR: + return LOG_STR("I2C error - communication with ENS210 failed!"); + case ENS210Component::ENS210_STATUS_CRC_ERROR: + return LOG_STR("CRC error"); + case ENS210Component::ENS210_STATUS_INVALID: + return LOG_STR("Invalid data"); + case ENS210Component::ENS210_STATUS_OK: + return LOG_STR("Status OK"); + case ENS210Component::ENS210_WRONG_CHIP_ID: + return LOG_STR("ENS210 has wrong chip ID! Is it a ENS210?"); + default: + return LOG_STR("Unknown"); + } +} + +// Compute the CRC-7 of 'value' (should only have 17 bits) +// https://en.wikipedia.org/wiki/Cyclic_redundancy_check#Computation +static uint32_t crc7(uint32_t value) { + // Setup polynomial + uint32_t polynomial = CRC7_POLY; + // Align polynomial with data + polynomial = polynomial << (DATA7_WIDTH - CRC7_WIDTH - 1); + // Loop variable (indicates which bit to test, start with highest) + uint32_t bit = DATA7_MSB; + // Make room for CRC value + value = value << CRC7_WIDTH; + bit = bit << CRC7_WIDTH; + polynomial = polynomial << CRC7_WIDTH; + // Insert initial vector + value |= CRC7_IVEC; + // Apply division until all bits done + while (bit & (DATA7_MASK << CRC7_WIDTH)) { + if (bit & value) + value ^= polynomial; + bit >>= 1; + polynomial >>= 1; + } + return value; +} + +void ENS210Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up ENS210..."); + uint8_t data[2]; + uint16_t part_id = 0; + // Reset + if (!this->write_byte(ENS210_REGISTER_SYS_CTRL, 0x80)) { + this->write_byte(ENS210_REGISTER_SYS_CTRL, 0x80); + this->error_code_ = ENS210_STATUS_I2C_ERROR; + this->mark_failed(); + return; + } + // Wait to boot after reset + delay(ENS210_BOOTING_MS); + // Must disable low power to read PART_ID + if (!set_low_power_(false)) { + // Try to go back to default mode (low power enabled) + set_low_power_(true); + this->error_code_ = ENS210_STATUS_I2C_ERROR; + this->mark_failed(); + return; + } + // Read the PART_ID + if (!this->read_bytes(ENS210_REGISTER_PART_ID, data, 2)) { + // Try to go back to default mode (low power enabled) + set_low_power_(true); + this->error_code_ = ENS210_STATUS_I2C_ERROR; + this->mark_failed(); + return; + } + // Pack bytes into partid + part_id = data[1] * 256U + data[0] * 1U; + // Check expected part id of the ENS210 + if (part_id != ENS210_PART_ID) { + this->error_code_ = ENS210_WRONG_CHIP_ID; + this->mark_failed(); + } + // Set default power mode (low power enabled) + set_low_power_(true); +} + +void ENS210Component::dump_config() { + ESP_LOGCONFIG(TAG, "ENS210:"); + LOG_I2C_DEVICE(this); + if (this->is_failed()) { + ESP_LOGE(TAG, "%s", LOG_STR_ARG(ens210_status_to_human(this->error_code_))); + } + LOG_UPDATE_INTERVAL(this); + LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); + LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); +} + +float ENS210Component::get_setup_priority() const { return setup_priority::DATA; } + +void ENS210Component::update() { + // Execute a single measurement + if (!this->write_byte(ENS210_REGISTER_SENS_RUN, 0x00)) { + ESP_LOGE(TAG, "Starting single measurement failed!"); + this->status_set_warning(); + return; + } + // Trigger measurement + if (!this->write_byte(ENS210_REGISTER_SENS_START, 0x03)) { + ESP_LOGE(TAG, "Trigger of measurement failed!"); + this->status_set_warning(); + return; + } + // Wait for measurement to complete + this->set_timeout("data", uint32_t(ENS210_SINGLE_MEASURMENT_CONVERSION_TIME_MS), [this]() { + int temperature_data, temperature_status, humidity_data, humidity_status; + uint8_t data[6]; + uint32_t h_val_data, t_val_data; + // Set default status for early bail out + temperature_status = ENS210_STATUS_I2C_ERROR; + humidity_status = ENS210_STATUS_I2C_ERROR; + + // Read T_VAL and H_VAL + if (!this->read_bytes(ENS210_REGISTER_T_VAL, data, 6)) { + ESP_LOGE(TAG, "Communication with ENS210 failed!"); + this->status_set_warning(); + return; + } + // Pack bytes for humidity + h_val_data = (uint32_t)((uint32_t) data[5] << 16 | (uint32_t) data[4] << 8 | (uint32_t) data[3]); + // Extract humidity data and update the status + extract_measurement_(h_val_data, &humidity_data, &humidity_status); + + if (humidity_status == ENS210_STATUS_OK) { + if (this->humidity_sensor_ != nullptr) { + float humidity = (humidity_data & 0xFFFF) / 512.0; + this->humidity_sensor_->publish_state(humidity); + } + } else { + ESP_LOGW(TAG, "Humidity status failure: %s", LOG_STR_ARG(ens210_status_to_human(humidity_status))); + this->status_set_warning(); + return; + } + // Pack bytes for temperature + t_val_data = (uint32_t)((uint32_t) data[2] << 16 | (uint32_t) data[1] << 8 | (uint32_t) data[0]); + // Extract temperature data and update the status + extract_measurement_(t_val_data, &temperature_data, &temperature_status); + + if (temperature_status == ENS210_STATUS_OK) { + if (this->temperature_sensor_ != nullptr) { + // Temperature in Celsius + float temperature = (temperature_data & 0xFFFF) / 64.0 - 27315L / 100.0; + this->temperature_sensor_->publish_state(temperature); + } + } else { + ESP_LOGW(TAG, "Temperature status failure: %s", LOG_STR_ARG(ens210_status_to_human(temperature_status))); + } + }); +} + +// Extracts measurement 'data' and 'status' from a 'val' obtained from measurment. +void ENS210Component::extract_measurement_(uint32_t val, int *data, int *status) { + *data = (val >> 0) & 0xffff; + int valid = (val >> 16) & 0x1; + uint32_t crc = (val >> 17) & 0x7f; + uint32_t payload = (val >> 0) & 0x1ffff; + // Check CRC + uint8_t crc_ok = crc7(payload) == crc; + + if (!crc_ok) { + *status = ENS210_STATUS_CRC_ERROR; + } else if (!valid) { + *status = ENS210_STATUS_INVALID; + } else { + *status = ENS210_STATUS_OK; + } +} + +// Sets ENS210 to low (true) or high (false) power. Returns false on I2C problems. +bool ENS210Component::set_low_power_(bool enable) { + uint8_t low_power_cmd = enable ? 0x01 : 0x00; + ESP_LOGD(TAG, "Enable low power: %s", enable ? "true" : "false"); + bool result = this->write_byte(ENS210_REGISTER_SYS_CTRL, low_power_cmd); + delay(ENS210_BOOTING_MS); + return result; +} + +} // namespace ens210 +} // namespace esphome diff --git a/esphome/components/ens210/ens210.h b/esphome/components/ens210/ens210.h new file mode 100644 index 0000000000..342be04799 --- /dev/null +++ b/esphome/components/ens210/ens210.h @@ -0,0 +1,39 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace ens210 { + +/// This class implements support for the ENS210 relative humidity and temperature i2c sensor. +class ENS210Component : public PollingComponent, public i2c::I2CDevice { + public: + float get_setup_priority() const override; + void dump_config() override; + void setup() override; + void update() override; + + void set_humidity_sensor(sensor::Sensor *humidity_sensor) { humidity_sensor_ = humidity_sensor; } + void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } + + enum ErrorCode { + ENS210_STATUS_OK = 0, // The value was read, the CRC matches, and data is valid + ENS210_STATUS_INVALID, // The value was read, the CRC matches, but the data is invalid (e.g. the measurement was + // not yet finished) + ENS210_STATUS_CRC_ERROR, // The value was read, but the CRC over the payload (valid and data) does not match + ENS210_STATUS_I2C_ERROR, // There was an I2C communication error + ENS210_WRONG_CHIP_ID // The read PART_ID is not the expected part id of the ENS210 + } error_code_{ENS210_STATUS_OK}; + + protected: + bool set_low_power_(bool enable); + void extract_measurement_(uint32_t val, int *data, int *status); + + sensor::Sensor *temperature_sensor_; + sensor::Sensor *humidity_sensor_; +}; + +} // namespace ens210 +} // namespace esphome diff --git a/esphome/components/ens210/sensor.py b/esphome/components/ens210/sensor.py new file mode 100644 index 0000000000..3037156e01 --- /dev/null +++ b/esphome/components/ens210/sensor.py @@ -0,0 +1,58 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import ( + CONF_HUMIDITY, + CONF_ID, + CONF_TEMPERATURE, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_PERCENT, +) + +CODEOWNERS = ["@itn3rd77"] +DEPENDENCIES = ["i2c"] + +ens210_ns = cg.esphome_ns.namespace("ens210") + +ENS210Component = ens210_ns.class_( + "ENS210Component", cg.PollingComponent, i2c.I2CDevice +) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(ENS210Component), + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x43)) +) + + +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_TEMPERATURE in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) + cg.add(var.set_temperature_sensor(sens)) + + if CONF_HUMIDITY in config: + sens = await sensor.new_sensor(config[CONF_HUMIDITY]) + cg.add(var.set_humidity_sensor(sens)) diff --git a/tests/test1.yaml b/tests/test1.yaml index 375499942b..aba37976aa 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -547,11 +547,18 @@ sensor: - platform: esp32_hall name: "ESP32 Hall Sensor" update_interval: 15s - - platform: hdc1080 + - platform: ens210 temperature: name: "Living Room Temperature 5" humidity: - name: "Living Room Pressure 5" + name: 'Living Room Humidity 5' + update_interval: 15s + i2c_id: i2c_bus + - platform: hdc1080 + temperature: + name: 'Living Room Temperature 6' + humidity: + name: 'Living Room Humidity 5' update_interval: 15s i2c_id: i2c_bus - platform: hlw8012 From 2e4645310b2b8a893d75e508b82e3f0ba176754f Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 9 May 2022 19:16:46 +1200 Subject: [PATCH 28/56] Also rename yaml filename with rename command (#3447) --- esphome/__main__.py | 138 ++++++++++++++++++++++---------------------- 1 file changed, 70 insertions(+), 68 deletions(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index 80e8455465..c336336f18 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -496,83 +496,85 @@ def command_rename(args, config): ) ) return 1 + # Load existing yaml file with open(CORE.config_path, mode="r+", encoding="utf-8") as raw_file: raw_contents = raw_file.read() - yaml = yaml_util.load_yaml(CORE.config_path) - if CONF_ESPHOME not in yaml or CONF_NAME not in yaml[CONF_ESPHOME]: - print( - color( - Fore.BOLD_RED, "Complex YAML files cannot be automatically renamed." + + yaml = yaml_util.load_yaml(CORE.config_path) + if CONF_ESPHOME not in yaml or CONF_NAME not in yaml[CONF_ESPHOME]: + print( + color(Fore.BOLD_RED, "Complex YAML files cannot be automatically renamed.") + ) + return 1 + old_name = yaml[CONF_ESPHOME][CONF_NAME] + match = re.match(r"^\$\{?([a-zA-Z0-9_]+)\}?$", old_name) + if match is None: + new_raw = re.sub( + rf"name:\s+[\"']?{old_name}[\"']?", + f'name: "{args.name}"', + raw_contents, + ) + else: + old_name = yaml[CONF_SUBSTITUTIONS][match.group(1)] + if ( + len( + re.findall( + rf"^\s+{match.group(1)}:\s+[\"']?{old_name}[\"']?", + raw_contents, + flags=re.MULTILINE, ) ) - return 1 - old_name = yaml[CONF_ESPHOME][CONF_NAME] - match = re.match(r"^\$\{?([a-zA-Z0-9_]+)\}?$", old_name) - if match is None: - new_raw = re.sub( - rf"name:\s+[\"']?{old_name}[\"']?", - f'name: "{args.name}"', - raw_contents, - ) - else: - old_name = yaml[CONF_SUBSTITUTIONS][match.group(1)] - if ( - len( - re.findall( - rf"^\s+{match.group(1)}:\s+[\"']?{old_name}[\"']?", - raw_contents, - flags=re.MULTILINE, - ) - ) - > 1 - ): - print(color(Fore.BOLD_RED, "Too many matches in YAML to safely rename")) - return 1 - - new_raw = re.sub( - rf"^(\s+{match.group(1)}):\s+[\"']?{old_name}[\"']?", - f'\\1: "{args.name}"', - raw_contents, - flags=re.MULTILINE, - ) - - raw_file.seek(0) - raw_file.write(new_raw) - raw_file.flush() - - print(f"Updating {color(Fore.CYAN, CORE.config_path)}") - print() - - rc = run_external_process("esphome", "config", CORE.config_path) - if rc != 0: - raw_file.seek(0) - raw_file.write(raw_contents) - print(color(Fore.BOLD_RED, "Rename failed. Reverting changes.")) + > 1 + ): + print(color(Fore.BOLD_RED, "Too many matches in YAML to safely rename")) return 1 - cli_args = [ - "run", - CORE.config_path, - "--no-logs", - "--device", - CORE.address, - ] + new_raw = re.sub( + rf"^(\s+{match.group(1)}):\s+[\"']?{old_name}[\"']?", + f'\\1: "{args.name}"', + raw_contents, + flags=re.MULTILINE, + ) - if args.dashboard: - cli_args.insert(0, "--dashboard") + new_path = os.path.join(CORE.config_dir, args.name + ".yaml") + print( + f"Updating {color(Fore.CYAN, CORE.config_path)} to {color(Fore.CYAN, new_path)}" + ) + print() - try: - rc = run_external_process("esphome", *cli_args) - except KeyboardInterrupt: - rc = 1 - if rc != 0: - raw_file.seek(0) - raw_file.write(raw_contents) - return 1 + with open(new_path, mode="w", encoding="utf-8") as new_file: + new_file.write(new_raw) - print(color(Fore.BOLD_GREEN, "SUCCESS")) - print() - return 0 + rc = run_external_process("esphome", "config", new_path) + if rc != 0: + print(color(Fore.BOLD_RED, "Rename failed. Reverting changes.")) + os.remove(new_path) + return 1 + + cli_args = [ + "run", + new_path, + "--no-logs", + "--device", + CORE.address, + ] + + if args.dashboard: + cli_args.insert(0, "--dashboard") + + try: + rc = run_external_process("esphome", *cli_args) + except KeyboardInterrupt: + rc = 1 + if rc != 0: + os.remove(new_path) + return 1 + + os.remove(CORE.config_path) + + print(color(Fore.BOLD_GREEN, "SUCCESS")) + print() + return 0 PRE_CONFIG_ACTIONS = { From e5b3625f73e7be85342071897f6d52463de3e010 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 May 2022 19:22:47 +1200 Subject: [PATCH 29/56] Bump click from 8.1.2 to 8.1.3 (#3426) Bumps [click](https://github.com/pallets/click) from 8.1.2 to 8.1.3. - [Release notes](https://github.com/pallets/click/releases) - [Changelog](https://github.com/pallets/click/blob/main/CHANGES.rst) - [Commits](https://github.com/pallets/click/compare/8.1.2...8.1.3) --- updated-dependencies: - dependency-name: click 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.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 543999a9f3..e62ef86765 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ tzdata>=2021.1 # from time pyserial==3.5 platformio==5.2.5 # When updating platformio, also update Dockerfile esptool==3.3 -click==8.1.2 +click==8.1.3 esphome-dashboard==20220508.0 aioesphomeapi==10.8.2 zeroconf==0.38.4 From 8236e840a76fc94ac6ae3f023f8d26f619567033 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 9 May 2022 19:24:27 +1200 Subject: [PATCH 30/56] Fix spi transfer with miso pin defined on espidf (#3450) --- esphome/components/spi/spi.h | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/esphome/components/spi/spi.h b/esphome/components/spi/spi.h index 6c3fd17e56..7f0b0f481a 100644 --- a/esphome/components/spi/spi.h +++ b/esphome/components/spi/spi.h @@ -156,15 +156,17 @@ class SPIComponent : public Component { template uint8_t transfer_byte(uint8_t data) { -#ifdef USE_SPI_ARDUINO_BACKEND if (this->miso_ != nullptr) { +#ifdef USE_SPI_ARDUINO_BACKEND if (this->hw_spi_ != nullptr) { return this->hw_spi_->transfer(data); } else { - return this->transfer_(data); - } - } #endif // USE_SPI_ARDUINO_BACKEND + return this->transfer_(data); +#ifdef USE_SPI_ARDUINO_BACKEND + } +#endif // USE_SPI_ARDUINO_BACKEND + } this->write_byte(data); return 0; } From df999723f86b42697b8018fa860289134f048afd Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 9 May 2022 19:43:09 +1200 Subject: [PATCH 31/56] Force using name substitution when adopting a device (#3451) --- esphome/components/dashboard_import/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/esphome/components/dashboard_import/__init__.py b/esphome/components/dashboard_import/__init__.py index 6194a55205..41b4a8bed1 100644 --- a/esphome/components/dashboard_import/__init__.py +++ b/esphome/components/dashboard_import/__init__.py @@ -64,7 +64,10 @@ def import_config(path: str, name: str, project_name: str, import_url: str) -> N config = { "substitutions": {"name": name}, "packages": {project_name: import_url}, - "esphome": {"name_add_mac_suffix": False}, + "esphome": { + "name": "${name}", + "name_add_mac_suffix": False, + }, } p.write_text( dump(config) + WIFI_CONFIG, From d13a397f8ee2e13c834977238f6f7f01c3858ff8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 May 2022 19:44:54 +1200 Subject: [PATCH 32/56] Bump pyupgrade from 2.32.0 to 2.32.1 (#3452) 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 083050252d..68da13aade 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,7 +1,7 @@ pylint==2.13.5 flake8==4.0.1 black==22.3.0 -pyupgrade==2.32.0 +pyupgrade==2.32.1 pre-commit # Unit tests From a35f36ad39ead5a9da7681fa76c1d3380b3eeab6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 May 2022 20:28:21 +1200 Subject: [PATCH 33/56] Bump pylint from 2.13.5 to 2.13.8 (#3432) 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 68da13aade..4b5db8ce87 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,4 +1,4 @@ -pylint==2.13.5 +pylint==2.13.8 flake8==4.0.1 black==22.3.0 pyupgrade==2.32.1 From 47898b527cd974068af04723f253b16009c72735 Mon Sep 17 00:00:00 2001 From: MFlasskamp Date: Mon, 9 May 2022 10:32:14 +0200 Subject: [PATCH 34/56] Esp32c3 deepsleep fix (#3433) --- .../components/deep_sleep/deep_sleep_component.cpp | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/esphome/components/deep_sleep/deep_sleep_component.cpp b/esphome/components/deep_sleep/deep_sleep_component.cpp index 1bb70e0d7e..23f2a7a70c 100644 --- a/esphome/components/deep_sleep/deep_sleep_component.cpp +++ b/esphome/components/deep_sleep/deep_sleep_component.cpp @@ -76,12 +76,14 @@ void DeepSleepComponent::set_sleep_duration(uint32_t time_ms) { this->sleep_dura void DeepSleepComponent::set_wakeup_pin_mode(WakeupPinMode wakeup_pin_mode) { this->wakeup_pin_mode_ = wakeup_pin_mode; } +#if !defined(USE_ESP32_VARIANT_ESP32C3) void DeepSleepComponent::set_ext1_wakeup(Ext1Wakeup ext1_wakeup) { this->ext1_wakeup_ = ext1_wakeup; } void DeepSleepComponent::set_touch_wakeup(bool touch_wakeup) { this->touch_wakeup_ = touch_wakeup; } void DeepSleepComponent::set_run_duration(WakeupCauseToRunDuration wakeup_cause_to_run_duration) { wakeup_cause_to_run_duration_ = wakeup_cause_to_run_duration; } #endif +#endif void DeepSleepComponent::set_run_duration(uint32_t time_ms) { this->run_duration_ = time_ms; } void DeepSleepComponent::begin_sleep(bool manual) { if (this->prevent_ && !manual) { @@ -107,7 +109,8 @@ void DeepSleepComponent::begin_sleep(bool manual) { App.run_safe_shutdown_hooks(); -#if defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C3) +#if defined(USE_ESP32) +#if !defined(USE_ESP32_VARIANT_ESP32C3) if (this->sleep_duration_.has_value()) esp_sleep_enable_timer_wakeup(*this->sleep_duration_); if (this->wakeup_pin_ != nullptr) { @@ -125,10 +128,7 @@ void DeepSleepComponent::begin_sleep(bool manual) { esp_sleep_enable_touchpad_wakeup(); esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); } - - esp_deep_sleep_start(); #endif - #ifdef USE_ESP32_VARIANT_ESP32C3 if (this->sleep_duration_.has_value()) esp_sleep_enable_timer_wakeup(*this->sleep_duration_); @@ -137,9 +137,12 @@ void DeepSleepComponent::begin_sleep(bool manual) { if (this->wakeup_pin_mode_ == WAKEUP_PIN_MODE_INVERT_WAKEUP && this->wakeup_pin_->digital_read()) { level = !level; } - esp_deep_sleep_enable_gpio_wakeup(gpio_num_t(this->wakeup_pin_->get_pin()), level); + esp_deep_sleep_enable_gpio_wakeup(gpio_num_t(this->wakeup_pin_->get_pin()), + static_cast(level)); } #endif + esp_deep_sleep_start(); +#endif #ifdef USE_ESP8266 ESP.deepSleep(*this->sleep_duration_); // NOLINT(readability-static-accessed-through-instance) From 3a3d97dfa79bd65b4eec7609d035299903baa717 Mon Sep 17 00:00:00 2001 From: Unai Date: Tue, 10 May 2022 03:28:22 +0200 Subject: [PATCH 35/56] Add SERIAL_JTAG/CDC logger option for ESP-IDF platform for ESP32-S2/S3/C3 (#3105) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/logger/__init__.py | 62 +++++++++++++++------ esphome/components/logger/logger.cpp | 79 ++++++++++++++++++++------- esphome/components/logger/logger.h | 12 +++- 3 files changed, 114 insertions(+), 39 deletions(-) diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index d11b00405d..43d87bcefe 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -19,8 +19,13 @@ from esphome.const import ( CONF_TX_BUFFER_SIZE, ) from esphome.core import CORE, EsphomeError, Lambda, coroutine_with_priority -from esphome.components.esp32 import get_esp32_variant -from esphome.components.esp32.const import VARIANT_ESP32S2, VARIANT_ESP32C3 +from esphome.components.esp32 import add_idf_sdkconfig_option, get_esp32_variant +from esphome.components.esp32.const import ( + VARIANT_ESP32, + VARIANT_ESP32S2, + VARIANT_ESP32C3, + VARIANT_ESP32S3, +) CODEOWNERS = ["@esphome/core"] logger_ns = cg.esphome_ns.namespace("logger") @@ -54,36 +59,51 @@ LOG_LEVEL_SEVERITY = [ "VERY_VERBOSE", ] -ESP32_REDUCED_VARIANTS = [VARIANT_ESP32C3, VARIANT_ESP32S2] +UART0 = "UART0" +UART1 = "UART1" +UART2 = "UART2" +UART0_SWAP = "UART0_SWAP" +USB_SERIAL_JTAG = "USB_SERIAL_JTAG" +USB_CDC = "USB_CDC" -UART_SELECTION_ESP32_REDUCED = ["UART0", "UART1"] +UART_SELECTION_ESP32 = { + VARIANT_ESP32: [UART0, UART1, UART2], + VARIANT_ESP32S2: [UART0, UART1, USB_CDC], + VARIANT_ESP32S3: [UART0, UART1, USB_CDC, USB_SERIAL_JTAG], + VARIANT_ESP32C3: [UART0, UART1, USB_SERIAL_JTAG], +} -UART_SELECTION_ESP32 = ["UART0", "UART1", "UART2"] +UART_SELECTION_ESP8266 = [UART0, UART0_SWAP, UART1] -UART_SELECTION_ESP8266 = ["UART0", "UART0_SWAP", "UART1"] +ESP_IDF_UARTS = [USB_CDC, USB_SERIAL_JTAG] HARDWARE_UART_TO_UART_SELECTION = { - "UART0": logger_ns.UART_SELECTION_UART0, - "UART0_SWAP": logger_ns.UART_SELECTION_UART0_SWAP, - "UART1": logger_ns.UART_SELECTION_UART1, - "UART2": logger_ns.UART_SELECTION_UART2, + UART0: logger_ns.UART_SELECTION_UART0, + UART0_SWAP: logger_ns.UART_SELECTION_UART0_SWAP, + UART1: logger_ns.UART_SELECTION_UART1, + UART2: logger_ns.UART_SELECTION_UART2, + USB_CDC: logger_ns.UART_SELECTION_USB_CDC, + USB_SERIAL_JTAG: logger_ns.UART_SELECTION_USB_SERIAL_JTAG, } HARDWARE_UART_TO_SERIAL = { - "UART0": cg.global_ns.Serial, - "UART0_SWAP": cg.global_ns.Serial, - "UART1": cg.global_ns.Serial1, - "UART2": cg.global_ns.Serial2, + UART0: cg.global_ns.Serial, + UART0_SWAP: cg.global_ns.Serial, + UART1: cg.global_ns.Serial1, + UART2: cg.global_ns.Serial2, } is_log_level = cv.one_of(*LOG_LEVELS, upper=True) def uart_selection(value): + if value.upper() in ESP_IDF_UARTS: + if not CORE.using_esp_idf: + raise cv.Invalid(f"Only esp-idf framework supports {value}.") if CORE.is_esp32: - if get_esp32_variant() in ESP32_REDUCED_VARIANTS: - return cv.one_of(*UART_SELECTION_ESP32_REDUCED, upper=True)(value) - return cv.one_of(*UART_SELECTION_ESP32, upper=True)(value) + variant = get_esp32_variant() + if variant in UART_SELECTION_ESP32: + return cv.one_of(*UART_SELECTION_ESP32[variant], upper=True)(value) if CORE.is_esp8266: return cv.one_of(*UART_SELECTION_ESP8266, upper=True)(value) raise NotImplementedError @@ -113,7 +133,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_BAUD_RATE, default=115200): cv.positive_int, cv.Optional(CONF_TX_BUFFER_SIZE, default=512): cv.validate_bytes, cv.Optional(CONF_DEASSERT_RTS_DTR, default=False): cv.boolean, - cv.Optional(CONF_HARDWARE_UART, default="UART0"): uart_selection, + cv.Optional(CONF_HARDWARE_UART, default=UART0): uart_selection, cv.Optional(CONF_LEVEL, default="DEBUG"): is_log_level, cv.Optional(CONF_LOGS, default={}): cv.Schema( { @@ -185,6 +205,12 @@ async def to_code(config): if config.get(CONF_ESP8266_STORE_LOG_STRINGS_IN_FLASH): cg.add_build_flag("-DUSE_STORE_LOG_STR_IN_FLASH") + if CORE.using_esp_idf: + if config[CONF_HARDWARE_UART] == USB_CDC: + add_idf_sdkconfig_option("CONFIG_ESP_CONSOLE_USB_CDC", True) + elif config[CONF_HARDWARE_UART] == USB_SERIAL_JTAG: + add_idf_sdkconfig_option("CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG", True) + # Register at end for safe mode await cg.register_component(log, config) diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index 3f4e4e7753..08c83035b6 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -116,8 +116,22 @@ void HOT Logger::log_message_(int level, const char *tag, int offset) { this->hw_serial_->println(msg); #endif // USE_ARDUINO #ifdef USE_ESP_IDF - uart_write_bytes(uart_num_, msg, strlen(msg)); - uart_write_bytes(uart_num_, "\n", 1); + if ( +#if defined(USE_ESP32_VARIANT_ESP32S2) + uart_ == UART_SELECTION_USB_CDC +#elif defined(USE_ESP32_VARIANT_ESP32C3) + uart_ == UART_SELECTION_USB_SERIAL_JTAG +#elif defined(USE_ESP32_VARIANT_ESP32S3) + uart_ == UART_SELECTION_USB_CDC || uart_ == UART_SELECTION_USB_SERIAL_JTAG +#else + /* DISABLES CODE */ (false) +#endif + ) { + puts(msg); + } else { + uart_write_bytes(uart_num_, msg, strlen(msg)); + uart_write_bytes(uart_num_, "\n", 1); + } #endif } @@ -181,29 +195,41 @@ void Logger::pre_setup() { case UART_SELECTION_UART1: uart_num_ = UART_NUM_1; break; -#if defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32S2) +#if !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32S2) && !defined(USE_ESP32_VARIANT_ESP32S3) case UART_SELECTION_UART2: uart_num_ = UART_NUM_2; break; -#endif +#endif // !USE_ESP32_VARIANT_ESP32C3 && !USE_ESP32_VARIANT_ESP32S2 && !USE_ESP32_VARIANT_ESP32S3 +#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) + case UART_SELECTION_USB_CDC: + uart_num_ = -1; + break; +#endif // USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 +#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S3) + case UART_SELECTION_USB_SERIAL_JTAG: + uart_num_ = -1; + break; +#endif // USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32S3 } - uart_config_t uart_config{}; - uart_config.baud_rate = (int) baud_rate_; - uart_config.data_bits = UART_DATA_8_BITS; - uart_config.parity = UART_PARITY_DISABLE; - uart_config.stop_bits = UART_STOP_BITS_1; - uart_config.flow_ctrl = UART_HW_FLOWCTRL_DISABLE; - uart_param_config(uart_num_, &uart_config); - const int uart_buffer_size = tx_buffer_size_; - // Install UART driver using an event queue here - uart_driver_install(uart_num_, uart_buffer_size, uart_buffer_size, 10, nullptr, 0); -#endif + if (uart_num_ >= 0) { + uart_config_t uart_config{}; + uart_config.baud_rate = (int) baud_rate_; + uart_config.data_bits = UART_DATA_8_BITS; + uart_config.parity = UART_PARITY_DISABLE; + uart_config.stop_bits = UART_STOP_BITS_1; + uart_config.flow_ctrl = UART_HW_FLOWCTRL_DISABLE; + uart_param_config(uart_num_, &uart_config); + const int uart_buffer_size = tx_buffer_size_; + // Install UART driver using an event queue here + uart_driver_install(uart_num_, uart_buffer_size, uart_buffer_size, 10, nullptr, 0); + } +#endif // USE_ESP_IDF } #ifdef USE_ESP8266 else { uart_set_debug(UART_NO); } -#endif +#endif // USE_ESP8266 global_logger = this; #if defined(USE_ESP_IDF) || defined(USE_ESP32_FRAMEWORK_ARDUINO) @@ -211,7 +237,7 @@ void Logger::pre_setup() { if (ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE) { esp_log_level_set("*", ESP_LOG_VERBOSE); } -#endif +#endif // USE_ESP_IDF || USE_ESP32_FRAMEWORK_ARDUINO ESP_LOGI(TAG, "Log initialized"); } @@ -226,11 +252,24 @@ void Logger::add_on_log_callback(std::function Date: Tue, 10 May 2022 06:41:16 +0200 Subject: [PATCH 36/56] Select enhancement (#3423) Co-authored-by: Maurice Makaay --- esphome/codegen.py | 1 + esphome/components/api/api_server.cpp | 2 +- esphome/components/api/api_server.h | 2 +- .../components/copy/select/copy_select.cpp | 2 +- esphome/components/mqtt/mqtt_select.cpp | 2 +- esphome/components/select/__init__.py | 126 +++++++++++++++++- esphome/components/select/automation.h | 40 +++++- esphome/components/select/select.cpp | 66 +++++---- esphome/components/select/select.h | 48 ++----- esphome/components/select/select_call.cpp | 122 +++++++++++++++++ esphome/components/select/select_call.h | 48 +++++++ esphome/components/select/select_traits.cpp | 11 ++ esphome/components/select/select_traits.h | 19 +++ esphome/components/web_server/web_server.cpp | 2 +- esphome/components/web_server/web_server.h | 2 +- esphome/const.py | 2 + esphome/core/controller.cpp | 6 +- esphome/core/controller.h | 2 +- esphome/cpp_types.py | 1 + tests/test5.yaml | 35 ++++- 20 files changed, 461 insertions(+), 78 deletions(-) create mode 100644 esphome/components/select/select_call.cpp create mode 100644 esphome/components/select/select_call.h create mode 100644 esphome/components/select/select_traits.cpp create mode 100644 esphome/components/select/select_traits.h diff --git a/esphome/codegen.py b/esphome/codegen.py index b862a8ce86..185e6599b1 100644 --- a/esphome/codegen.py +++ b/esphome/codegen.py @@ -64,6 +64,7 @@ from esphome.cpp_types import ( # noqa uint64, int32, int64, + size_t, const_char_ptr, NAN, esphome_ns, diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 4521cc5bfc..1f2800f298 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -255,7 +255,7 @@ void APIServer::on_number_update(number::Number *obj, float state) { #endif #ifdef USE_SELECT -void APIServer::on_select_update(select::Select *obj, const std::string &state) { +void APIServer::on_select_update(select::Select *obj, const std::string &state, size_t index) { if (obj->is_internal()) return; for (auto &c : this->clients_) diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index fdc46922ad..f03a83fc7b 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -64,7 +64,7 @@ class APIServer : public Component, public Controller { void on_number_update(number::Number *obj, float state) override; #endif #ifdef USE_SELECT - void on_select_update(select::Select *obj, const std::string &state) override; + void on_select_update(select::Select *obj, const std::string &state, size_t index) override; #endif #ifdef USE_LOCK void on_lock_update(lock::Lock *obj) override; diff --git a/esphome/components/copy/select/copy_select.cpp b/esphome/components/copy/select/copy_select.cpp index 0f01c2692c..bdcbd0b42c 100644 --- a/esphome/components/copy/select/copy_select.cpp +++ b/esphome/components/copy/select/copy_select.cpp @@ -7,7 +7,7 @@ namespace copy { static const char *const TAG = "copy.select"; void CopySelect::setup() { - source_->add_on_state_callback([this](const std::string &value) { this->publish_state(value); }); + source_->add_on_state_callback([this](const std::string &value, size_t index) { this->publish_state(value); }); traits.set_options(source_->traits.get_options()); diff --git a/esphome/components/mqtt/mqtt_select.cpp b/esphome/components/mqtt/mqtt_select.cpp index 7ecbf9425e..ea5130f823 100644 --- a/esphome/components/mqtt/mqtt_select.cpp +++ b/esphome/components/mqtt/mqtt_select.cpp @@ -21,7 +21,7 @@ void MQTTSelectComponent::setup() { call.set_option(state); call.perform(); }); - this->select_->add_on_state_callback([this](const std::string &state) { this->publish_state(state); }); + this->select_->add_on_state_callback([this](const std::string &state, size_t index) { this->publish_state(state); }); } void MQTTSelectComponent::dump_config() { diff --git a/esphome/components/select/__init__.py b/esphome/components/select/__init__.py index c15036e9f9..a1c73c385e 100644 --- a/esphome/components/select/__init__.py +++ b/esphome/components/select/__init__.py @@ -9,6 +9,10 @@ from esphome.const import ( CONF_OPTION, CONF_TRIGGER_ID, CONF_MQTT_ID, + CONF_CYCLE, + CONF_MODE, + CONF_OPERATION, + CONF_INDEX, ) from esphome.core import CORE, coroutine_with_priority from esphome.cpp_helpers import setup_entity @@ -22,14 +26,27 @@ SelectPtr = Select.operator("ptr") # Triggers SelectStateTrigger = select_ns.class_( - "SelectStateTrigger", automation.Trigger.template(cg.float_) + "SelectStateTrigger", + automation.Trigger.template(cg.std_string, cg.size_t), ) # Actions SelectSetAction = select_ns.class_("SelectSetAction", automation.Action) +SelectSetIndexAction = select_ns.class_("SelectSetIndexAction", automation.Action) +SelectOperationAction = select_ns.class_("SelectOperationAction", automation.Action) + +# Enums +SelectOperation = select_ns.enum("SelectOperation") +SELECT_OPERATION_OPTIONS = { + "NEXT": SelectOperation.SELECT_OP_NEXT, + "PREVIOUS": SelectOperation.SELECT_OP_PREVIOUS, + "FIRST": SelectOperation.SELECT_OP_FIRST, + "LAST": SelectOperation.SELECT_OP_LAST, +} icon = cv.icon + SELECT_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend( { cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTSelectComponent), @@ -50,7 +67,9 @@ async def setup_select_core_(var, config, *, options: List[str]): for conf in config.get(CONF_ON_VALUE, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [(cg.std_string, "x")], conf) + await automation.build_automation( + trigger, [(cg.std_string, "x"), (cg.size_t, "i")], conf + ) if CONF_MQTT_ID in config: mqtt_ = cg.new_Pvariable(config[CONF_MQTT_ID], var) @@ -76,12 +95,18 @@ async def to_code(config): cg.add_global(select_ns.using) +OPERATION_BASE_SCHEMA = cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(Select), + } +) + + @automation.register_action( "select.set", SelectSetAction, - cv.Schema( + OPERATION_BASE_SCHEMA.extend( { - cv.Required(CONF_ID): cv.use_id(Select), cv.Required(CONF_OPTION): cv.templatable(cv.string_strict), } ), @@ -92,3 +117,96 @@ async def select_set_to_code(config, action_id, template_arg, args): template_ = await cg.templatable(config[CONF_OPTION], args, cg.std_string) cg.add(var.set_option(template_)) return var + + +@automation.register_action( + "select.set_index", + SelectSetIndexAction, + OPERATION_BASE_SCHEMA.extend( + { + cv.Required(CONF_INDEX): cv.templatable(cv.positive_int), + } + ), +) +async def select_set_index_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + template_ = await cg.templatable(config[CONF_INDEX], args, cg.size_t) + cg.add(var.set_index(template_)) + return var + + +@automation.register_action( + "select.operation", + SelectOperationAction, + OPERATION_BASE_SCHEMA.extend( + { + cv.Required(CONF_OPERATION): cv.templatable( + cv.enum(SELECT_OPERATION_OPTIONS, upper=True) + ), + cv.Optional(CONF_CYCLE, default=True): cv.templatable(cv.boolean), + } + ), +) +@automation.register_action( + "select.next", + SelectOperationAction, + automation.maybe_simple_id( + OPERATION_BASE_SCHEMA.extend( + { + cv.Optional(CONF_MODE, default="NEXT"): cv.one_of("NEXT", upper=True), + cv.Optional(CONF_CYCLE, default=True): cv.boolean, + } + ) + ), +) +@automation.register_action( + "select.previous", + SelectOperationAction, + automation.maybe_simple_id( + OPERATION_BASE_SCHEMA.extend( + { + cv.Optional(CONF_MODE, default="PREVIOUS"): cv.one_of( + "PREVIOUS", upper=True + ), + cv.Optional(CONF_CYCLE, default=True): cv.boolean, + } + ) + ), +) +@automation.register_action( + "select.first", + SelectOperationAction, + automation.maybe_simple_id( + OPERATION_BASE_SCHEMA.extend( + { + cv.Optional(CONF_MODE, default="FIRST"): cv.one_of("FIRST", upper=True), + } + ) + ), +) +@automation.register_action( + "select.last", + SelectOperationAction, + automation.maybe_simple_id( + OPERATION_BASE_SCHEMA.extend( + { + cv.Optional(CONF_MODE, default="LAST"): cv.one_of("LAST", upper=True), + } + ) + ), +) +async def select_operation_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + if CONF_OPERATION in config: + op_ = await cg.templatable(config[CONF_OPERATION], args, SelectOperation) + cg.add(var.set_operation(op_)) + if CONF_CYCLE in config: + cycle_ = await cg.templatable(config[CONF_CYCLE], args, bool) + cg.add(var.set_cycle(cycle_)) + if CONF_MODE in config: + cg.add(var.set_operation(SELECT_OPERATION_OPTIONS[config[CONF_MODE]])) + if CONF_CYCLE in config: + cg.add(var.set_cycle(config[CONF_CYCLE])) + return var diff --git a/esphome/components/select/automation.h b/esphome/components/select/automation.h index 1e0bfed63d..1250665188 100644 --- a/esphome/components/select/automation.h +++ b/esphome/components/select/automation.h @@ -7,16 +7,16 @@ namespace esphome { namespace select { -class SelectStateTrigger : public Trigger { +class SelectStateTrigger : public Trigger { public: explicit SelectStateTrigger(Select *parent) { - parent->add_on_state_callback([this](const std::string &value) { this->trigger(value); }); + parent->add_on_state_callback([this](const std::string &value, size_t index) { this->trigger(value, index); }); } }; template class SelectSetAction : public Action { public: - SelectSetAction(Select *select) : select_(select) {} + explicit SelectSetAction(Select *select) : select_(select) {} TEMPLATABLE_VALUE(std::string, option) void play(Ts... x) override { @@ -29,5 +29,39 @@ template class SelectSetAction : public Action { Select *select_; }; +template class SelectSetIndexAction : public Action { + public: + explicit SelectSetIndexAction(Select *select) : select_(select) {} + TEMPLATABLE_VALUE(size_t, index) + + void play(Ts... x) override { + auto call = this->select_->make_call(); + call.set_index(this->index_.value(x...)); + call.perform(); + } + + protected: + Select *select_; +}; + +template class SelectOperationAction : public Action { + public: + explicit SelectOperationAction(Select *select) : select_(select) {} + TEMPLATABLE_VALUE(bool, cycle) + TEMPLATABLE_VALUE(SelectOperation, operation) + + void play(Ts... x) override { + auto call = this->select_->make_call(); + call.with_operation(this->operation_.value(x...)); + if (this->cycle_.has_value()) { + call.with_cycle(this->cycle_.value(x...)); + } + call.perform(); + } + + protected: + Select *select_; +}; + } // namespace select } // namespace esphome diff --git a/esphome/components/select/select.cpp b/esphome/components/select/select.cpp index 14f4d9277d..75edb5c8ba 100644 --- a/esphome/components/select/select.cpp +++ b/esphome/components/select/select.cpp @@ -6,37 +6,53 @@ namespace select { static const char *const TAG = "select"; -void SelectCall::perform() { - ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); - if (!this->option_.has_value()) { - ESP_LOGW(TAG, "No value set for SelectCall"); - return; - } - - const auto &traits = this->parent_->traits; - auto value = *this->option_; - auto options = traits.get_options(); - - if (std::find(options.begin(), options.end(), value) == options.end()) { - ESP_LOGW(TAG, " Option %s is not a valid option.", value.c_str()); - return; - } - - ESP_LOGD(TAG, " Option: %s", (*this->option_).c_str()); - this->parent_->control(*this->option_); -} - void Select::publish_state(const std::string &state) { - this->has_state_ = true; - this->state = state; - ESP_LOGD(TAG, "'%s': Sending state %s", this->get_name().c_str(), state.c_str()); - this->state_callback_.call(state); + auto index = this->index_of(state); + const auto *name = this->get_name().c_str(); + if (index.has_value()) { + this->has_state_ = true; + this->state = state; + ESP_LOGD(TAG, "'%s': Sending state %s (index %d)", name, state.c_str(), index.value()); + this->state_callback_.call(state, index.value()); + } else { + ESP_LOGE(TAG, "'%s': invalid state for publish_state(): %s", name, state.c_str()); + } } -void Select::add_on_state_callback(std::function &&callback) { +void Select::add_on_state_callback(std::function &&callback) { this->state_callback_.add(std::move(callback)); } +size_t Select::size() const { + auto options = traits.get_options(); + return options.size(); +} + +optional Select::index_of(const std::string &option) const { + auto options = traits.get_options(); + auto it = std::find(options.begin(), options.end(), option); + if (it == options.end()) { + return {}; + } + return std::distance(options.begin(), it); +} + +optional Select::active_index() const { + if (this->has_state()) { + return this->index_of(this->state); + } else { + return {}; + } +} + +optional Select::at(size_t index) const { + auto options = traits.get_options(); + if (index >= options.size()) { + return {}; + } + return options.at(index); +} + uint32_t Select::hash_base() { return 2812997003UL; } } // namespace select diff --git a/esphome/components/select/select.h b/esphome/components/select/select.h index db655ea34e..64870fc9a3 100644 --- a/esphome/components/select/select.h +++ b/esphome/components/select/select.h @@ -1,10 +1,10 @@ #pragma once -#include -#include #include "esphome/core/component.h" #include "esphome/core/entity_base.h" #include "esphome/core/helpers.h" +#include "select_call.h" +#include "select_traits.h" namespace esphome { namespace select { @@ -17,33 +17,6 @@ namespace select { } \ } -class Select; - -class SelectCall { - public: - explicit SelectCall(Select *parent) : parent_(parent) {} - void perform(); - - SelectCall &set_option(const std::string &option) { - option_ = option; - return *this; - } - const optional &get_option() const { return option_; } - - protected: - Select *const parent_; - optional option_; -}; - -class SelectTraits { - public: - void set_options(std::vector options) { this->options_ = std::move(options); } - std::vector get_options() const { return this->options_; } - - protected: - std::vector options_; -}; - /** Base-class for all selects. * * A select can use publish_state to send out a new value. @@ -51,18 +24,23 @@ class SelectTraits { class Select : public EntityBase { public: std::string state; + SelectTraits traits; void publish_state(const std::string &state); + /// Return whether this select has gotten a full state yet. + bool has_state() const { return has_state_; } + SelectCall make_call() { return SelectCall(this); } void set(const std::string &value) { make_call().set_option(value).perform(); } - void add_on_state_callback(std::function &&callback); + // Methods that provide an API to index-based access. + size_t size() const; + optional index_of(const std::string &option) const; + optional active_index() const; + optional at(size_t index) const; - SelectTraits traits; - - /// Return whether this select has gotten a full state yet. - bool has_state() const { return has_state_; } + void add_on_state_callback(std::function &&callback); protected: friend class SelectCall; @@ -77,7 +55,7 @@ class Select : public EntityBase { uint32_t hash_base() override; - CallbackManager state_callback_; + CallbackManager state_callback_; bool has_state_{false}; }; diff --git a/esphome/components/select/select_call.cpp b/esphome/components/select/select_call.cpp new file mode 100644 index 0000000000..9442598740 --- /dev/null +++ b/esphome/components/select/select_call.cpp @@ -0,0 +1,122 @@ +#include "select_call.h" +#include "select.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace select { + +static const char *const TAG = "select"; + +SelectCall &SelectCall::set_option(const std::string &option) { + return with_operation(SELECT_OP_SET).with_option(option); +} + +SelectCall &SelectCall::set_index(size_t index) { return with_operation(SELECT_OP_SET_INDEX).with_index(index); } + +const optional &SelectCall::get_option() const { return option_; } + +SelectCall &SelectCall::select_next(bool cycle) { return with_operation(SELECT_OP_NEXT).with_cycle(cycle); } + +SelectCall &SelectCall::select_previous(bool cycle) { return with_operation(SELECT_OP_PREVIOUS).with_cycle(cycle); } + +SelectCall &SelectCall::select_first() { return with_operation(SELECT_OP_FIRST); } + +SelectCall &SelectCall::select_last() { return with_operation(SELECT_OP_LAST); } + +SelectCall &SelectCall::with_operation(SelectOperation operation) { + this->operation_ = operation; + return *this; +} + +SelectCall &SelectCall::with_cycle(bool cycle) { + this->cycle_ = cycle; + return *this; +} + +SelectCall &SelectCall::with_option(const std::string &option) { + this->option_ = option; + return *this; +} + +SelectCall &SelectCall::with_index(size_t index) { + this->index_ = index; + return *this; +} + +void SelectCall::perform() { + auto *parent = this->parent_; + const auto *name = parent->get_name().c_str(); + const auto &traits = parent->traits; + auto options = traits.get_options(); + + if (this->operation_ == SELECT_OP_NONE) { + ESP_LOGW(TAG, "'%s' - SelectCall performed without selecting an operation", name); + return; + } + if (options.empty()) { + ESP_LOGW(TAG, "'%s' - Cannot perform SelectCall, select has no options", name); + return; + } + + std::string target_value; + + if (this->operation_ == SELECT_OP_SET) { + ESP_LOGD(TAG, "'%s' - Setting", name); + if (!this->option_.has_value()) { + ESP_LOGW(TAG, "'%s' - No option value set for SelectCall", name); + return; + } + target_value = this->option_.value(); + } else if (this->operation_ == SELECT_OP_SET_INDEX) { + if (!this->index_.has_value()) { + ESP_LOGW(TAG, "'%s' - No index value set for SelectCall", name); + return; + } + if (this->index_.value() >= options.size()) { + ESP_LOGW(TAG, "'%s' - Index value %d out of bounds", name, this->index_.value()); + return; + } + target_value = options[this->index_.value()]; + } else if (this->operation_ == SELECT_OP_FIRST) { + target_value = options.front(); + } else if (this->operation_ == SELECT_OP_LAST) { + target_value = options.back(); + } else if (this->operation_ == SELECT_OP_NEXT || this->operation_ == SELECT_OP_PREVIOUS) { + auto cycle = this->cycle_; + ESP_LOGD(TAG, "'%s' - Selecting %s, with%s cycling", name, this->operation_ == SELECT_OP_NEXT ? "next" : "previous", + cycle ? "" : "out"); + if (!parent->has_state()) { + target_value = this->operation_ == SELECT_OP_NEXT ? options.front() : options.back(); + } else { + auto index = parent->index_of(parent->state); + if (index.has_value()) { + auto size = options.size(); + if (cycle) { + auto use_index = (size + index.value() + (this->operation_ == SELECT_OP_NEXT ? +1 : -1)) % size; + target_value = options[use_index]; + } else { + if (this->operation_ == SELECT_OP_PREVIOUS && index.value() > 0) { + target_value = options[index.value() - 1]; + } else if (this->operation_ == SELECT_OP_NEXT && index.value() < options.size() - 1) { + target_value = options[index.value() + 1]; + } else { + return; + } + } + } else { + target_value = this->operation_ == SELECT_OP_NEXT ? options.front() : options.back(); + } + } + } + + if (std::find(options.begin(), options.end(), target_value) == options.end()) { + ESP_LOGW(TAG, "'%s' - Option %s is not a valid option", name, target_value.c_str()); + return; + } + + ESP_LOGD(TAG, "'%s' - Set selected option to: %s", name, target_value.c_str()); + parent->control(target_value); +} + +} // namespace select +} // namespace esphome diff --git a/esphome/components/select/select_call.h b/esphome/components/select/select_call.h new file mode 100644 index 0000000000..ea4d34ab5f --- /dev/null +++ b/esphome/components/select/select_call.h @@ -0,0 +1,48 @@ +#pragma once + +#include "esphome/core/helpers.h" + +namespace esphome { +namespace select { + +class Select; + +enum SelectOperation { + SELECT_OP_NONE, + SELECT_OP_SET, + SELECT_OP_SET_INDEX, + SELECT_OP_NEXT, + SELECT_OP_PREVIOUS, + SELECT_OP_FIRST, + SELECT_OP_LAST +}; + +class SelectCall { + public: + explicit SelectCall(Select *parent) : parent_(parent) {} + void perform(); + + SelectCall &set_option(const std::string &option); + SelectCall &set_index(size_t index); + const optional &get_option() const; + + SelectCall &select_next(bool cycle); + SelectCall &select_previous(bool cycle); + SelectCall &select_first(); + SelectCall &select_last(); + + SelectCall &with_operation(SelectOperation operation); + SelectCall &with_cycle(bool cycle); + SelectCall &with_option(const std::string &option); + SelectCall &with_index(size_t index); + + protected: + Select *const parent_; + optional option_; + optional index_; + SelectOperation operation_{SELECT_OP_NONE}; + bool cycle_; +}; + +} // namespace select +} // namespace esphome diff --git a/esphome/components/select/select_traits.cpp b/esphome/components/select/select_traits.cpp new file mode 100644 index 0000000000..89da30c405 --- /dev/null +++ b/esphome/components/select/select_traits.cpp @@ -0,0 +1,11 @@ +#include "select_traits.h" + +namespace esphome { +namespace select { + +void SelectTraits::set_options(std::vector options) { this->options_ = std::move(options); } + +std::vector SelectTraits::get_options() const { return this->options_; } + +} // namespace select +} // namespace esphome diff --git a/esphome/components/select/select_traits.h b/esphome/components/select/select_traits.h new file mode 100644 index 0000000000..ccf23dc6d0 --- /dev/null +++ b/esphome/components/select/select_traits.h @@ -0,0 +1,19 @@ +#pragma once + +#include +#include + +namespace esphome { +namespace select { + +class SelectTraits { + public: + void set_options(std::vector options); + std::vector get_options() const; + + protected: + std::vector options_; +}; + +} // namespace select +} // namespace esphome diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 0dfd608661..6822ce9953 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -755,7 +755,7 @@ std::string WebServer::number_json(number::Number *obj, float value, JsonDetail #endif #ifdef USE_SELECT -void WebServer::on_select_update(select::Select *obj, const std::string &state) { +void WebServer::on_select_update(select::Select *obj, const std::string &state, size_t index) { this->events_.send(this->select_json(obj, state, DETAIL_STATE).c_str(), "state"); } void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlMatch &match) { diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index bd7acd91a0..78d0597e61 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -185,7 +185,7 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { #endif #ifdef USE_SELECT - void on_select_update(select::Select *obj, const std::string &state) override; + void on_select_update(select::Select *obj, const std::string &state, size_t index) override; /// Handle a select request under '/select/'. void handle_select_request(AsyncWebServerRequest *request, const UrlMatch &match); diff --git a/esphome/const.py b/esphome/const.py index 9f2bed28d1..fc928dc530 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -138,6 +138,7 @@ CONF_CUSTOM_FAN_MODE = "custom_fan_mode" CONF_CUSTOM_FAN_MODES = "custom_fan_modes" CONF_CUSTOM_PRESET = "custom_preset" CONF_CUSTOM_PRESETS = "custom_presets" +CONF_CYCLE = "cycle" CONF_DALLAS_ID = "dallas_id" CONF_DATA = "data" CONF_DATA_PIN = "data_pin" @@ -458,6 +459,7 @@ CONF_OPEN_DRAIN = "open_drain" CONF_OPEN_DRAIN_INTERRUPT = "open_drain_interrupt" CONF_OPEN_DURATION = "open_duration" CONF_OPEN_ENDSTOP = "open_endstop" +CONF_OPERATION = "operation" CONF_OPTIMISTIC = "optimistic" CONF_OPTION = "option" CONF_OPTIONS = "options" diff --git a/esphome/core/controller.cpp b/esphome/core/controller.cpp index dfcef5e4c1..7d63a3d143 100644 --- a/esphome/core/controller.cpp +++ b/esphome/core/controller.cpp @@ -61,8 +61,10 @@ void Controller::setup_controller(bool include_internal) { #endif #ifdef USE_SELECT for (auto *obj : App.get_selects()) { - if (include_internal || !obj->is_internal()) - obj->add_on_state_callback([this, obj](const std::string &state) { this->on_select_update(obj, state); }); + if (include_internal || !obj->is_internal()) { + obj->add_on_state_callback( + [this, obj](const std::string &state, size_t index) { this->on_select_update(obj, state, index); }); + } } #endif #ifdef USE_LOCK diff --git a/esphome/core/controller.h b/esphome/core/controller.h index 0be854828b..419624a2ae 100644 --- a/esphome/core/controller.h +++ b/esphome/core/controller.h @@ -71,7 +71,7 @@ class Controller { virtual void on_number_update(number::Number *obj, float state){}; #endif #ifdef USE_SELECT - virtual void on_select_update(select::Select *obj, const std::string &state){}; + virtual void on_select_update(select::Select *obj, const std::string &state, size_t index){}; #endif #ifdef USE_LOCK virtual void on_lock_update(lock::Lock *obj){}; diff --git a/esphome/cpp_types.py b/esphome/cpp_types.py index 2323b2578f..aafe765111 100644 --- a/esphome/cpp_types.py +++ b/esphome/cpp_types.py @@ -16,6 +16,7 @@ uint32 = global_ns.namespace("uint32_t") uint64 = global_ns.namespace("uint64_t") int32 = global_ns.namespace("int32_t") int64 = global_ns.namespace("int64_t") +size_t = global_ns.namespace("size_t") const_char_ptr = global_ns.namespace("const char *") NAN = global_ns.namespace("NAN") esphome_ns = global_ns # using namespace esphome; diff --git a/tests/test5.yaml b/tests/test5.yaml index ee90cc1149..e38f39eab6 100644 --- a/tests/test5.yaml +++ b/tests/test5.yaml @@ -154,8 +154,8 @@ select: restore_value: true on_value: - logger.log: - format: "Select changed to %s" - args: ["x.c_str()"] + format: "Select changed to %s (index %d)" + args: ["x.c_str()", "i"] set_action: - logger.log: format: "Template Select set to %s" @@ -163,11 +163,42 @@ select: - select.set: id: template_select_id option: two + - select.first: template_select_id + - select.last: + id: template_select_id + - select.previous: template_select_id + - select.next: + id: template_select_id + cycle: false + - select.operation: + id: template_select_id + operation: Previous + cycle: false + - select.operation: + id: template_select_id + operation: !lambda "return SELECT_OP_PREVIOUS;" + cycle: !lambda "return true;" + - select.set_index: + id: template_select_id + index: 1 + - select.set_index: + id: template_select_id + index: !lambda "return 1 + 1;" options: - one - two - three + - platform: modbus_controller + name: "Modbus Select Register 1000" + address: 1000 + value_type: U_WORD + optionsmap: + "Zero": 0 + "One": 1 + "Two": 2 + "Three": 3 + sensor: - platform: selec_meter total_active_energy: From d9caab41081f5230ac99252bc2bd645638c43f1b Mon Sep 17 00:00:00 2001 From: Maurice Makaay Date: Tue, 10 May 2022 06:58:56 +0200 Subject: [PATCH 37/56] Number enhancement (#3429) Co-authored-by: Maurice Makaay Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/number/__init__.py | 104 ++++++++++++++++- esphome/components/number/automation.h | 19 ++++ esphome/components/number/number.cpp | 33 ------ esphome/components/number/number.h | 50 +-------- esphome/components/number/number_call.cpp | 118 ++++++++++++++++++++ esphome/components/number/number_call.h | 45 ++++++++ esphome/components/number/number_traits.cpp | 20 ++++ esphome/components/number/number_traits.h | 44 ++++++++ tests/test5.yaml | 35 +++++- 9 files changed, 380 insertions(+), 88 deletions(-) create mode 100644 esphome/components/number/number_call.cpp create mode 100644 esphome/components/number/number_call.h create mode 100644 esphome/components/number/number_traits.cpp create mode 100644 esphome/components/number/number_traits.h diff --git a/esphome/components/number/__init__.py b/esphome/components/number/__init__.py index 89788f1e98..f809fff529 100644 --- a/esphome/components/number/__init__.py +++ b/esphome/components/number/__init__.py @@ -14,6 +14,8 @@ from esphome.const import ( CONF_UNIT_OF_MEASUREMENT, CONF_MQTT_ID, CONF_VALUE, + CONF_OPERATION, + CONF_CYCLE, ) from esphome.core import CORE, coroutine_with_priority from esphome.cpp_helpers import setup_entity @@ -35,6 +37,7 @@ ValueRangeTrigger = number_ns.class_( # Actions NumberSetAction = number_ns.class_("NumberSetAction", automation.Action) +NumberOperationAction = number_ns.class_("NumberOperationAction", automation.Action) # Conditions NumberInRangeCondition = number_ns.class_( @@ -49,6 +52,15 @@ NUMBER_MODES = { "SLIDER": NumberMode.NUMBER_MODE_SLIDER, } +NumberOperation = number_ns.enum("NumberOperation") + +NUMBER_OPERATION_OPTIONS = { + "INCREMENT": NumberOperation.NUMBER_OP_INCREMENT, + "DECREMENT": NumberOperation.NUMBER_OP_DECREMENT, + "TO_MIN": NumberOperation.NUMBER_OP_TO_MIN, + "TO_MAX": NumberOperation.NUMBER_OP_TO_MAX, +} + icon = cv.icon NUMBER_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend( @@ -159,12 +171,18 @@ async def to_code(config): cg.add_global(number_ns.using) +OPERATION_BASE_SCHEMA = cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(Number), + } +) + + @automation.register_action( "number.set", NumberSetAction, - cv.Schema( + OPERATION_BASE_SCHEMA.extend( { - cv.Required(CONF_ID): cv.use_id(Number), cv.Required(CONF_VALUE): cv.templatable(cv.float_), } ), @@ -175,3 +193,85 @@ async def number_set_to_code(config, action_id, template_arg, args): template_ = await cg.templatable(config[CONF_VALUE], args, float) cg.add(var.set_value(template_)) return var + + +@automation.register_action( + "number.increment", + NumberOperationAction, + automation.maybe_simple_id( + OPERATION_BASE_SCHEMA.extend( + { + cv.Optional(CONF_MODE, default="INCREMENT"): cv.one_of( + "INCREMENT", upper=True + ), + cv.Optional(CONF_CYCLE, default=True): cv.boolean, + } + ) + ), +) +@automation.register_action( + "number.decrement", + NumberOperationAction, + automation.maybe_simple_id( + OPERATION_BASE_SCHEMA.extend( + { + cv.Optional(CONF_MODE, default="DECREMENT"): cv.one_of( + "DECREMENT", upper=True + ), + cv.Optional(CONF_CYCLE, default=True): cv.boolean, + } + ) + ), +) +@automation.register_action( + "number.to_min", + NumberOperationAction, + automation.maybe_simple_id( + OPERATION_BASE_SCHEMA.extend( + { + cv.Optional(CONF_MODE, default="TO_MIN"): cv.one_of( + "TO_MIN", upper=True + ), + } + ) + ), +) +@automation.register_action( + "number.to_max", + NumberOperationAction, + automation.maybe_simple_id( + OPERATION_BASE_SCHEMA.extend( + { + cv.Optional(CONF_MODE, default="TO_MAX"): cv.one_of( + "TO_MAX", upper=True + ), + } + ) + ), +) +@automation.register_action( + "number.operation", + NumberOperationAction, + OPERATION_BASE_SCHEMA.extend( + { + cv.Required(CONF_OPERATION): cv.templatable( + cv.enum(NUMBER_OPERATION_OPTIONS, upper=True) + ), + cv.Optional(CONF_CYCLE, default=True): cv.templatable(cv.boolean), + } + ), +) +async def number_to_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + if CONF_OPERATION in config: + to_ = await cg.templatable(config[CONF_OPERATION], args, NumberOperation) + cg.add(var.set_operation(to_)) + if CONF_CYCLE in config: + cycle_ = await cg.templatable(config[CONF_CYCLE], args, bool) + cg.add(var.set_cycle(cycle_)) + if CONF_MODE in config: + cg.add(var.set_operation(NUMBER_OPERATION_OPTIONS[config[CONF_MODE]])) + if CONF_CYCLE in config: + cg.add(var.set_cycle(config[CONF_CYCLE])) + return var diff --git a/esphome/components/number/automation.h b/esphome/components/number/automation.h index 98554a346a..33f0f9727e 100644 --- a/esphome/components/number/automation.h +++ b/esphome/components/number/automation.h @@ -29,6 +29,25 @@ template class NumberSetAction : public Action { Number *number_; }; +template class NumberOperationAction : public Action { + public: + explicit NumberOperationAction(Number *number) : number_(number) {} + TEMPLATABLE_VALUE(NumberOperation, operation) + TEMPLATABLE_VALUE(bool, cycle) + + void play(Ts... x) override { + auto call = this->number_->make_call(); + call.with_operation(this->operation_.value(x...)); + if (this->cycle_.has_value()) { + call.with_cycle(this->cycle_.value(x...)); + } + call.perform(); + } + + protected: + Number *number_; +}; + class ValueRangeTrigger : public Trigger, public Component { public: explicit ValueRangeTrigger(Number *parent) : parent_(parent) {} diff --git a/esphome/components/number/number.cpp b/esphome/components/number/number.cpp index 99a2c04a22..03a7cc6ce3 100644 --- a/esphome/components/number/number.cpp +++ b/esphome/components/number/number.cpp @@ -6,30 +6,6 @@ namespace number { static const char *const TAG = "number"; -void NumberCall::perform() { - ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); - if (!this->value_.has_value() || std::isnan(*this->value_)) { - ESP_LOGW(TAG, "No value set for NumberCall"); - return; - } - - const auto &traits = this->parent_->traits; - auto value = *this->value_; - - float min_value = traits.get_min_value(); - if (value < min_value) { - ESP_LOGW(TAG, " Value %f must not be less than minimum %f", value, min_value); - return; - } - float max_value = traits.get_max_value(); - if (value > max_value) { - ESP_LOGW(TAG, " Value %f must not be greater than maximum %f", value, max_value); - return; - } - ESP_LOGD(TAG, " Value: %f", *this->value_); - this->parent_->control(*this->value_); -} - void Number::publish_state(float state) { this->has_state_ = true; this->state = state; @@ -41,15 +17,6 @@ void Number::add_on_state_callback(std::function &&callback) { this->state_callback_.add(std::move(callback)); } -std::string NumberTraits::get_unit_of_measurement() { - if (this->unit_of_measurement_.has_value()) - return *this->unit_of_measurement_; - return ""; -} -void NumberTraits::set_unit_of_measurement(const std::string &unit_of_measurement) { - this->unit_of_measurement_ = unit_of_measurement; -} - uint32_t Number::hash_base() { return 2282307003UL; } } // namespace number diff --git a/esphome/components/number/number.h b/esphome/components/number/number.h index 40fdfceec1..8f9bf8c2e1 100644 --- a/esphome/components/number/number.h +++ b/esphome/components/number/number.h @@ -3,6 +3,8 @@ #include "esphome/core/component.h" #include "esphome/core/entity_base.h" #include "esphome/core/helpers.h" +#include "number_call.h" +#include "number_traits.h" namespace esphome { namespace number { @@ -20,54 +22,6 @@ namespace number { class Number; -class NumberCall { - public: - explicit NumberCall(Number *parent) : parent_(parent) {} - void perform(); - - NumberCall &set_value(float value) { - value_ = value; - return *this; - } - const optional &get_value() const { return value_; } - - protected: - Number *const parent_; - optional value_; -}; - -enum NumberMode : uint8_t { - NUMBER_MODE_AUTO = 0, - NUMBER_MODE_BOX = 1, - NUMBER_MODE_SLIDER = 2, -}; - -class NumberTraits { - public: - void set_min_value(float min_value) { min_value_ = min_value; } - float get_min_value() const { return min_value_; } - void set_max_value(float max_value) { max_value_ = max_value; } - float get_max_value() const { return max_value_; } - void set_step(float step) { step_ = step; } - float get_step() const { return step_; } - - /// Get the unit of measurement, using the manual override if set. - std::string get_unit_of_measurement(); - /// Manually set the unit of measurement. - void set_unit_of_measurement(const std::string &unit_of_measurement); - - // Get/set the frontend mode. - NumberMode get_mode() const { return this->mode_; } - void set_mode(NumberMode mode) { this->mode_ = mode; } - - protected: - float min_value_ = NAN; - float max_value_ = NAN; - float step_ = NAN; - optional unit_of_measurement_; ///< Unit of measurement override - NumberMode mode_{NUMBER_MODE_AUTO}; -}; - /** Base-class for all numbers. * * A number can use publish_state to send out a new value. diff --git a/esphome/components/number/number_call.cpp b/esphome/components/number/number_call.cpp new file mode 100644 index 0000000000..4219f85328 --- /dev/null +++ b/esphome/components/number/number_call.cpp @@ -0,0 +1,118 @@ +#include "number_call.h" +#include "number.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace number { + +static const char *const TAG = "number"; + +NumberCall &NumberCall::set_value(float value) { return this->with_operation(NUMBER_OP_SET).with_value(value); } + +NumberCall &NumberCall::number_increment(bool cycle) { + return this->with_operation(NUMBER_OP_INCREMENT).with_cycle(cycle); +} + +NumberCall &NumberCall::number_decrement(bool cycle) { + return this->with_operation(NUMBER_OP_DECREMENT).with_cycle(cycle); +} + +NumberCall &NumberCall::number_to_min() { return this->with_operation(NUMBER_OP_TO_MIN); } + +NumberCall &NumberCall::number_to_max() { return this->with_operation(NUMBER_OP_TO_MAX); } + +NumberCall &NumberCall::with_operation(NumberOperation operation) { + this->operation_ = operation; + return *this; +} + +NumberCall &NumberCall::with_value(float value) { + this->value_ = value; + return *this; +} + +NumberCall &NumberCall::with_cycle(bool cycle) { + this->cycle_ = cycle; + return *this; +} + +void NumberCall::perform() { + auto *parent = this->parent_; + const auto *name = parent->get_name().c_str(); + const auto &traits = parent->traits; + + if (this->operation_ == NUMBER_OP_NONE) { + ESP_LOGW(TAG, "'%s' - NumberCall performed without selecting an operation", name); + return; + } + + float target_value = NAN; + float min_value = traits.get_min_value(); + float max_value = traits.get_max_value(); + + if (this->operation_ == NUMBER_OP_SET) { + ESP_LOGD(TAG, "'%s' - Setting number value", name); + if (!this->value_.has_value() || std::isnan(*this->value_)) { + ESP_LOGW(TAG, "'%s' - No value set for NumberCall", name); + return; + } + target_value = this->value_.value(); + } else if (this->operation_ == NUMBER_OP_TO_MIN) { + if (std::isnan(min_value)) { + ESP_LOGW(TAG, "'%s' - Can't set to min value through NumberCall: no min_value defined", name); + } else { + target_value = min_value; + } + } else if (this->operation_ == NUMBER_OP_TO_MAX) { + if (std::isnan(max_value)) { + ESP_LOGW(TAG, "'%s' - Can't set to max value through NumberCall: no max_value defined", name); + } else { + target_value = max_value; + } + } else if (this->operation_ == NUMBER_OP_INCREMENT) { + ESP_LOGD(TAG, "'%s' - Increment number, with%s cycling", name, this->cycle_ ? "" : "out"); + if (!parent->has_state()) { + ESP_LOGW(TAG, "'%s' - Can't increment number through NumberCall: no active state to modify", name); + return; + } + auto step = traits.get_step(); + target_value = parent->state + (std::isnan(step) ? 1 : step); + if (target_value > max_value) { + if (this->cycle_ && !std::isnan(min_value)) { + target_value = min_value; + } else { + target_value = max_value; + } + } + } else if (this->operation_ == NUMBER_OP_DECREMENT) { + ESP_LOGD(TAG, "'%s' - Decrement number, with%s cycling", name, this->cycle_ ? "" : "out"); + if (!parent->has_state()) { + ESP_LOGW(TAG, "'%s' - Can't decrement number through NumberCall: no active state to modify", name); + return; + } + auto step = traits.get_step(); + target_value = parent->state - (std::isnan(step) ? 1 : step); + if (target_value < min_value) { + if (this->cycle_ && !std::isnan(max_value)) { + target_value = max_value; + } else { + target_value = min_value; + } + } + } + + if (target_value < min_value) { + ESP_LOGW(TAG, "'%s' - Value %f must not be less than minimum %f", name, target_value, min_value); + return; + } + if (target_value > max_value) { + ESP_LOGW(TAG, "'%s' - Value %f must not be greater than maximum %f", name, target_value, max_value); + return; + } + + ESP_LOGD(TAG, " New number value: %f", target_value); + this->parent_->control(target_value); +} + +} // namespace number +} // namespace esphome diff --git a/esphome/components/number/number_call.h b/esphome/components/number/number_call.h new file mode 100644 index 0000000000..9a3dad560f --- /dev/null +++ b/esphome/components/number/number_call.h @@ -0,0 +1,45 @@ +#pragma once + +#include "esphome/core/helpers.h" +#include "number_traits.h" + +namespace esphome { +namespace number { + +class Number; + +enum NumberOperation { + NUMBER_OP_NONE, + NUMBER_OP_SET, + NUMBER_OP_INCREMENT, + NUMBER_OP_DECREMENT, + NUMBER_OP_TO_MIN, + NUMBER_OP_TO_MAX, +}; + +class NumberCall { + public: + explicit NumberCall(Number *parent) : parent_(parent) {} + void perform(); + + NumberCall &set_value(float value); + const optional &get_value() const { return value_; } + + NumberCall &number_increment(bool cycle); + NumberCall &number_decrement(bool cycle); + NumberCall &number_to_min(); + NumberCall &number_to_max(); + + NumberCall &with_operation(NumberOperation operation); + NumberCall &with_value(float value); + NumberCall &with_cycle(bool cycle); + + protected: + Number *const parent_; + NumberOperation operation_{NUMBER_OP_NONE}; + optional value_; + bool cycle_; +}; + +} // namespace number +} // namespace esphome diff --git a/esphome/components/number/number_traits.cpp b/esphome/components/number/number_traits.cpp new file mode 100644 index 0000000000..dcd05daa2a --- /dev/null +++ b/esphome/components/number/number_traits.cpp @@ -0,0 +1,20 @@ +#include "esphome/core/log.h" +#include "number_traits.h" + +namespace esphome { +namespace number { + +static const char *const TAG = "number"; + +void NumberTraits::set_unit_of_measurement(const std::string &unit_of_measurement) { + this->unit_of_measurement_ = unit_of_measurement; +} + +std::string NumberTraits::get_unit_of_measurement() { + if (this->unit_of_measurement_.has_value()) + return *this->unit_of_measurement_; + return ""; +} + +} // namespace number +} // namespace esphome diff --git a/esphome/components/number/number_traits.h b/esphome/components/number/number_traits.h new file mode 100644 index 0000000000..47756ff66f --- /dev/null +++ b/esphome/components/number/number_traits.h @@ -0,0 +1,44 @@ +#pragma once + +#include "esphome/core/helpers.h" + +namespace esphome { +namespace number { + +enum NumberMode : uint8_t { + NUMBER_MODE_AUTO = 0, + NUMBER_MODE_BOX = 1, + NUMBER_MODE_SLIDER = 2, +}; + +class NumberTraits { + public: + // Set/get the number value boundaries. + void set_min_value(float min_value) { min_value_ = min_value; } + float get_min_value() const { return min_value_; } + void set_max_value(float max_value) { max_value_ = max_value; } + float get_max_value() const { return max_value_; } + + // Set/get the step size for incrementing or decrementing the number value. + void set_step(float step) { step_ = step; } + float get_step() const { return step_; } + + /// Manually set the unit of measurement. + void set_unit_of_measurement(const std::string &unit_of_measurement); + /// Get the unit of measurement, using the manual override if set. + std::string get_unit_of_measurement(); + + // Set/get the frontend mode. + void set_mode(NumberMode mode) { this->mode_ = mode; } + NumberMode get_mode() const { return this->mode_; } + + protected: + float min_value_ = NAN; + float max_value_ = NAN; + float step_ = NAN; + optional unit_of_measurement_; ///< Unit of measurement override + NumberMode mode_{NUMBER_MODE_AUTO}; +}; + +} // namespace number +} // namespace esphome diff --git a/tests/test5.yaml b/tests/test5.yaml index e38f39eab6..ffda860377 100644 --- a/tests/test5.yaml +++ b/tests/test5.yaml @@ -120,6 +120,11 @@ number: name: My template number id: template_number_id optimistic: true + max_value: 100 + min_value: 0 + step: 5 + unit_of_measurement: "%" + mode: slider on_value: - logger.log: format: "Number changed to %f" @@ -128,11 +133,31 @@ number: - logger.log: format: "Template Number set to %f" args: ["x"] - max_value: 100 - min_value: 0 - step: 5 - unit_of_measurement: "%" - mode: slider + - number.set: + id: template_number_id + value: 50 + - number.to_min: template_number_id + - number.to_min: + id: template_number_id + - number.to_max: template_number_id + - number.to_max: + id: template_number_id + - number.increment: template_number_id + - number.increment: + id: template_number_id + cycle: false + - number.decrement: template_number_id + - number.decrement: + id: template_number_id + cycle: false + - number.operation: + id: template_number_id + operation: Increment + cycle: false + - number.operation: + id: template_number_id + operation: !lambda "return NUMBER_OP_INCREMENT;" + cycle: !lambda "return false;" - id: modbus_numbertest platform: modbus_controller From d685fdf54a2cfbae32fa98eaf6b11ec1111e2912 Mon Sep 17 00:00:00 2001 From: MFlasskamp Date: Tue, 10 May 2022 07:16:16 +0200 Subject: [PATCH 38/56] mask deprecated adc_gpio_init() for esp32-s2 (#3445) --- esphome/components/adc/adc_sensor.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/adc/adc_sensor.cpp b/esphome/components/adc/adc_sensor.cpp index ad9cf29b6f..ca87c8d11f 100644 --- a/esphome/components/adc/adc_sensor.cpp +++ b/esphome/components/adc/adc_sensor.cpp @@ -51,8 +51,8 @@ void ADCSensor::setup() { } } - // adc_gpio_init doesn't exist on ESP32-C3 or ESP32-H2 -#if !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32H2) + // adc_gpio_init doesn't exist on ESP32-S2, ESP32-C3 or ESP32-H2 +#if !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32H2) && !defined(USE_ESP32_VARIANT_ESP32S2) adc_gpio_init(ADC_UNIT_1, (adc_channel_t) channel_); #endif #endif // USE_ESP32 From 86b52df839075e956f307c2666ae5719a992efbc Mon Sep 17 00:00:00 2001 From: Martin <25747549+martgras@users.noreply.github.com> Date: Tue, 10 May 2022 07:17:55 +0200 Subject: [PATCH 39/56] tca9548a fix channel selection (#3417) --- esphome/components/tca9548a/tca9548a.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/tca9548a/tca9548a.cpp b/esphome/components/tca9548a/tca9548a.cpp index de0d21b968..caa3dd0655 100644 --- a/esphome/components/tca9548a/tca9548a.cpp +++ b/esphome/components/tca9548a/tca9548a.cpp @@ -41,7 +41,7 @@ i2c::ErrorCode TCA9548AComponent::switch_to_channel(uint8_t channel) { return i2c::ERROR_OK; uint8_t channel_val = 1 << channel; - auto err = this->write_register(0x70, &channel_val, 1); + auto err = this->write(&channel_val, 1); if (err == i2c::ERROR_OK) { current_channel_ = channel; } From 0e547390da32bd773a2a6b0d575feaa981275ecf Mon Sep 17 00:00:00 2001 From: Martin <25747549+martgras@users.noreply.github.com> Date: Tue, 10 May 2022 10:15:02 +0200 Subject: [PATCH 40/56] add support for Sen5x sensor series (#3383) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/sen5x/__init__.py | 0 esphome/components/sen5x/automation.h | 21 ++ esphome/components/sen5x/sen5x.cpp | 413 ++++++++++++++++++++++++++ esphome/components/sen5x/sen5x.h | 128 ++++++++ esphome/components/sen5x/sensor.py | 241 +++++++++++++++ tests/test5.yaml | 43 +++ 7 files changed, 847 insertions(+) create mode 100644 esphome/components/sen5x/__init__.py create mode 100644 esphome/components/sen5x/automation.h create mode 100644 esphome/components/sen5x/sen5x.cpp create mode 100644 esphome/components/sen5x/sen5x.h create mode 100644 esphome/components/sen5x/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 51719ef1aa..77c9d30c5d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -173,6 +173,7 @@ esphome/components/sdm_meter/* @jesserockz @polyfaces esphome/components/sdp3x/* @Azimath esphome/components/selec_meter/* @sourabhjaiswal esphome/components/select/* @esphome/core +esphome/components/sen5x/* @martgras esphome/components/sensirion_common/* @martgras esphome/components/sensor/* @esphome/core esphome/components/sgp40/* @SenexCrenshaw diff --git a/esphome/components/sen5x/__init__.py b/esphome/components/sen5x/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/sen5x/automation.h b/esphome/components/sen5x/automation.h new file mode 100644 index 0000000000..423b942000 --- /dev/null +++ b/esphome/components/sen5x/automation.h @@ -0,0 +1,21 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "sen5x.h" + +namespace esphome { +namespace sen5x { + +template class StartFanAction : public Action { + public: + explicit StartFanAction(SEN5XComponent *sen5x) : sen5x_(sen5x) {} + + void play(Ts... x) override { this->sen5x_->start_fan_cleaning(); } + + protected: + SEN5XComponent *sen5x_; +}; + +} // namespace sen5x +} // namespace esphome diff --git a/esphome/components/sen5x/sen5x.cpp b/esphome/components/sen5x/sen5x.cpp new file mode 100644 index 0000000000..865fae373b --- /dev/null +++ b/esphome/components/sen5x/sen5x.cpp @@ -0,0 +1,413 @@ +#include "sen5x.h" +#include "esphome/core/hal.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace sen5x { + +static const char *const TAG = "sen5x"; + +static const uint16_t SEN5X_CMD_AUTO_CLEANING_INTERVAL = 0x8004; +static const uint16_t SEN5X_CMD_GET_DATA_READY_STATUS = 0x0202; +static const uint16_t SEN5X_CMD_GET_FIRMWARE_VERSION = 0xD100; +static const uint16_t SEN5X_CMD_GET_PRODUCT_NAME = 0xD014; +static const uint16_t SEN5X_CMD_GET_SERIAL_NUMBER = 0xD033; +static const uint16_t SEN5X_CMD_NOX_ALGORITHM_TUNING = 0x60E1; +static const uint16_t SEN5X_CMD_READ_MEASUREMENT = 0x03C4; +static const uint16_t SEN5X_CMD_RHT_ACCELERATION_MODE = 0x60F7; +static const uint16_t SEN5X_CMD_START_CLEANING_FAN = 0x5607; +static const uint16_t SEN5X_CMD_START_MEASUREMENTS = 0x0021; +static const uint16_t SEN5X_CMD_START_MEASUREMENTS_RHT_ONLY = 0x0037; +static const uint16_t SEN5X_CMD_STOP_MEASUREMENTS = 0x3f86; +static const uint16_t SEN5X_CMD_TEMPERATURE_COMPENSATION = 0x60B2; +static const uint16_t SEN5X_CMD_VOC_ALGORITHM_STATE = 0x6181; +static const uint16_t SEN5X_CMD_VOC_ALGORITHM_TUNING = 0x60D0; + +void SEN5XComponent::setup() { + ESP_LOGCONFIG(TAG, "Setting up sen5x..."); + + // the sensor needs 1000 ms to enter the idle state + this->set_timeout(1000, [this]() { + // Check if measurement is ready before reading the value + if (!this->write_command(SEN5X_CMD_GET_DATA_READY_STATUS)) { + ESP_LOGE(TAG, "Failed to write data ready status command"); + this->mark_failed(); + return; + } + + uint16_t raw_read_status; + if (!this->read_data(raw_read_status)) { + ESP_LOGE(TAG, "Failed to read data ready status"); + this->mark_failed(); + return; + } + + uint32_t stop_measurement_delay = 0; + // In order to query the device periodic measurement must be ceased + if (raw_read_status) { + ESP_LOGD(TAG, "Sensor has data available, stopping periodic measurement"); + if (!this->write_command(SEN5X_CMD_STOP_MEASUREMENTS)) { + ESP_LOGE(TAG, "Failed to stop measurements"); + this->mark_failed(); + return; + } + // According to the SEN5x datasheet the sensor will only respond to other commands after waiting 200 ms after + // issuing the stop_periodic_measurement command + stop_measurement_delay = 200; + } + this->set_timeout(stop_measurement_delay, [this]() { + uint16_t raw_serial_number[3]; + if (!this->get_register(SEN5X_CMD_GET_SERIAL_NUMBER, raw_serial_number, 3, 20)) { + ESP_LOGE(TAG, "Failed to read serial number"); + this->error_code_ = SERIAL_NUMBER_IDENTIFICATION_FAILED; + this->mark_failed(); + return; + } + this->serial_number_[0] = static_cast(uint16_t(raw_serial_number[0]) & 0xFF); + this->serial_number_[1] = static_cast(raw_serial_number[0] & 0xFF); + this->serial_number_[2] = static_cast(raw_serial_number[1] >> 8); + ESP_LOGD(TAG, "Serial number %02d.%02d.%02d", serial_number_[0], serial_number_[1], serial_number_[2]); + + uint16_t raw_product_name[16]; + if (!this->get_register(SEN5X_CMD_GET_PRODUCT_NAME, raw_product_name, 16, 20)) { + ESP_LOGE(TAG, "Failed to read product name"); + this->error_code_ = PRODUCT_NAME_FAILED; + this->mark_failed(); + return; + } + // 2 ASCII bytes are encoded in an int + const uint16_t *current_int = raw_product_name; + char current_char; + uint8_t max = 16; + do { + // first char + current_char = *current_int >> 8; + if (current_char) { + product_name_.push_back(current_char); + // second char + current_char = *current_int & 0xFF; + if (current_char) + product_name_.push_back(current_char); + } + current_int++; + } while (current_char && --max); + + Sen5xType sen5x_type = UNKNOWN; + if (product_name_ == "SEN50") { + sen5x_type = SEN50; + } else { + if (product_name_ == "SEN54") { + sen5x_type = SEN54; + } else { + if (product_name_ == "SEN55") { + sen5x_type = SEN55; + } + } + ESP_LOGD(TAG, "Productname %s", product_name_.c_str()); + } + if (this->humidity_sensor_ && sen5x_type == SEN50) { + ESP_LOGE(TAG, "For Relative humidity a SEN54 OR SEN55 is required. You are using a <%s> sensor", + this->product_name_.c_str()); + this->humidity_sensor_ = nullptr; // mark as not used + } + if (this->temperature_sensor_ && sen5x_type == SEN50) { + ESP_LOGE(TAG, "For Temperature a SEN54 OR SEN55 is required. You are using a <%s> sensor", + this->product_name_.c_str()); + this->temperature_sensor_ = nullptr; // mark as not used + } + if (this->voc_sensor_ && sen5x_type == SEN50) { + ESP_LOGE(TAG, "For VOC a SEN54 OR SEN55 is required. You are using a <%s> sensor", this->product_name_.c_str()); + this->voc_sensor_ = nullptr; // mark as not used + } + if (this->nox_sensor_ && sen5x_type != SEN55) { + ESP_LOGE(TAG, "For NOx a SEN55 is required. You are using a <%s> sensor", this->product_name_.c_str()); + this->nox_sensor_ = nullptr; // mark as not used + } + + if (!this->get_register(SEN5X_CMD_GET_FIRMWARE_VERSION, this->firmware_version_, 20)) { + ESP_LOGE(TAG, "Failed to read firmware version"); + this->error_code_ = FIRMWARE_FAILED; + this->mark_failed(); + return; + } + this->firmware_version_ >>= 8; + ESP_LOGD(TAG, "Firmware version %d", this->firmware_version_); + + if (this->voc_sensor_ && 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_)) { + 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); + uint16_t states[4]; + + states[0] = voc_baselines_storage_.state0 >> 16; + states[1] = voc_baselines_storage_.state0 & 0xFFFF; + states[2] = voc_baselines_storage_.state1 >> 16; + states[3] = voc_baselines_storage_.state1 & 0xFFFF; + + if (!this->write_command(SEN5X_CMD_VOC_ALGORITHM_STATE, states, 4)) { + ESP_LOGE(TAG, "Failed to set VOC baseline from saved state"); + } + } + } + bool result; + if (this->auto_cleaning_interval_.has_value()) { + // override default value + result = write_command(SEN5X_CMD_AUTO_CLEANING_INTERVAL, this->auto_cleaning_interval_.value()); + } else { + result = write_command(SEN5X_CMD_AUTO_CLEANING_INTERVAL); + } + if (result) { + delay(20); + uint16_t secs[2]; + if (this->read_data(secs, 2)) { + auto_cleaning_interval_ = secs[0] << 16 | secs[1]; + } + } + if (acceleration_mode_.has_value()) { + result = this->write_command(SEN5X_CMD_RHT_ACCELERATION_MODE, acceleration_mode_.value()); + } else { + result = this->write_command(SEN5X_CMD_RHT_ACCELERATION_MODE); + } + if (!result) { + ESP_LOGE(TAG, "Failed to set rh/t acceleration mode"); + this->error_code_ = COMMUNICATION_FAILED; + this->mark_failed(); + return; + } + delay(20); + if (!acceleration_mode_.has_value()) { + uint16_t mode; + if (this->read_data(mode)) { + this->acceleration_mode_ = RhtAccelerationMode(mode); + } else { + ESP_LOGE(TAG, "Failed to read RHT Acceleration mode"); + } + } + if (this->voc_tuning_params_.has_value()) + this->write_tuning_parameters_(SEN5X_CMD_VOC_ALGORITHM_TUNING, this->voc_tuning_params_.value()); + if (this->nox_tuning_params_.has_value()) + this->write_tuning_parameters_(SEN5X_CMD_NOX_ALGORITHM_TUNING, this->nox_tuning_params_.value()); + + if (this->temperature_compensation_.has_value()) + this->write_temperature_compensation_(this->temperature_compensation_.value()); + + // Finally start sensor measurements + auto cmd = SEN5X_CMD_START_MEASUREMENTS_RHT_ONLY; + if (this->pm_1_0_sensor_ || this->pm_2_5_sensor_ || this->pm_4_0_sensor_ || this->pm_10_0_sensor_) { + // if any of the gas sensors are active we need a full measurement + cmd = SEN5X_CMD_START_MEASUREMENTS; + } + + if (!this->write_command(cmd)) { + ESP_LOGE(TAG, "Error starting continuous measurements."); + this->error_code_ = MEASUREMENT_INIT_FAILED; + this->mark_failed(); + return; + } + initialized_ = true; + ESP_LOGD(TAG, "Sensor initialized"); + }); + }); +} + +void SEN5XComponent::dump_config() { + ESP_LOGCONFIG(TAG, "sen5x:"); + LOG_I2C_DEVICE(this); + if (this->is_failed()) { + switch (this->error_code_) { + case COMMUNICATION_FAILED: + ESP_LOGW(TAG, "Communication failed! Is the sensor connected?"); + break; + case MEASUREMENT_INIT_FAILED: + ESP_LOGW(TAG, "Measurement Initialization failed!"); + break; + case SERIAL_NUMBER_IDENTIFICATION_FAILED: + ESP_LOGW(TAG, "Unable to read sensor serial id"); + break; + case PRODUCT_NAME_FAILED: + ESP_LOGW(TAG, "Unable to read product name"); + break; + case FIRMWARE_FAILED: + ESP_LOGW(TAG, "Unable to read sensor firmware version"); + break; + default: + ESP_LOGW(TAG, "Unknown setup error!"); + break; + } + } + ESP_LOGCONFIG(TAG, " Productname: %s", this->product_name_.c_str()); + ESP_LOGCONFIG(TAG, " Firmware version: %d", this->firmware_version_); + ESP_LOGCONFIG(TAG, " Serial number %02d.%02d.%02d", serial_number_[0], serial_number_[1], serial_number_[2]); + if (this->auto_cleaning_interval_.has_value()) { + ESP_LOGCONFIG(TAG, " Auto auto cleaning interval %d seconds", auto_cleaning_interval_.value()); + } + if (this->acceleration_mode_.has_value()) { + switch (this->acceleration_mode_.value()) { + case LOW_ACCELERATION: + ESP_LOGCONFIG(TAG, " Low RH/T acceleration mode"); + break; + case MEDIUM_ACCELERATION: + ESP_LOGCONFIG(TAG, " Medium RH/T accelertion mode"); + break; + case HIGH_ACCELERATION: + ESP_LOGCONFIG(TAG, " High RH/T accelertion mode"); + break; + } + } + LOG_UPDATE_INTERVAL(this); + LOG_SENSOR(" ", "PM 1.0", this->pm_1_0_sensor_); + LOG_SENSOR(" ", "PM 2.5", this->pm_2_5_sensor_); + LOG_SENSOR(" ", "PM 4.0", this->pm_4_0_sensor_); + LOG_SENSOR(" ", "PM 10.0", this->pm_10_0_sensor_); + LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); + LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); + LOG_SENSOR(" ", "VOC", this->voc_sensor_); // SEN54 and SEN55 only + LOG_SENSOR(" ", "NOx", this->nox_sensor_); // SEN55 only +} + +void SEN5XComponent::update() { + if (!initialized_) { + return; + } + + // 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) { + if (this->write_command(SEN5X_CMD_VOC_ALGORITHM_STATE)) { + // run it a bit later to avoid adding a delay here + this->set_timeout(550, [this]() { + uint16_t states[4]; + if (this->read_data(states, 4)) { + uint32_t state0 = states[0] << 16 | states[1]; + uint32_t state1 = states[2] << 16 | states[3]; + if ((uint32_t) std::abs(static_cast(this->voc_baselines_storage_.state0 - state0)) > + MAXIMUM_STORAGE_DIFF || + (uint32_t) std::abs(static_cast(this->voc_baselines_storage_.state1 - state1)) > + MAXIMUM_STORAGE_DIFF) { + this->seconds_since_last_store_ = 0; + this->voc_baselines_storage_.state0 = state0; + this->voc_baselines_storage_.state1 = 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"); + } + } + } + }); + } + } + + if (!this->write_command(SEN5X_CMD_READ_MEASUREMENT)) { + this->status_set_warning(); + ESP_LOGD(TAG, "write error read measurement (%d)", this->last_error_); + return; + } + this->set_timeout(20, [this]() { + uint16_t measurements[8]; + + if (!this->read_data(measurements, 8)) { + this->status_set_warning(); + ESP_LOGD(TAG, "read data error (%d)", this->last_error_); + return; + } + float pm_1_0 = measurements[0] / 10.0; + if (measurements[0] == 0xFFFF) + pm_1_0 = NAN; + float pm_2_5 = measurements[1] / 10.0; + if (measurements[1] == 0xFFFF) + pm_2_5 = NAN; + float pm_4_0 = measurements[2] / 10.0; + if (measurements[2] == 0xFFFF) + pm_4_0 = NAN; + float pm_10_0 = measurements[3] / 10.0; + if (measurements[3] == 0xFFFF) + pm_10_0 = NAN; + float humidity = measurements[4] / 100.0; + if (measurements[4] == 0xFFFF) + humidity = NAN; + float temperature = measurements[5] / 200.0; + if (measurements[5] == 0xFFFF) + temperature = NAN; + float voc = measurements[6] / 10.0; + if (measurements[6] == 0xFFFF) + voc = NAN; + float nox = measurements[7] / 10.0; + if (measurements[7] == 0xFFFF) + nox = NAN; + + if (this->pm_1_0_sensor_ != nullptr) + this->pm_1_0_sensor_->publish_state(pm_1_0); + if (this->pm_2_5_sensor_ != nullptr) + this->pm_2_5_sensor_->publish_state(pm_2_5); + if (this->pm_4_0_sensor_ != nullptr) + this->pm_4_0_sensor_->publish_state(pm_4_0); + if (this->pm_10_0_sensor_ != nullptr) + this->pm_10_0_sensor_->publish_state(pm_10_0); + if (this->temperature_sensor_ != nullptr) + this->temperature_sensor_->publish_state(temperature); + if (this->humidity_sensor_ != nullptr) + this->humidity_sensor_->publish_state(humidity); + if (this->voc_sensor_ != nullptr) + this->voc_sensor_->publish_state(voc); + if (this->nox_sensor_ != nullptr) + this->nox_sensor_->publish_state(nox); + this->status_clear_warning(); + }); +} + +bool SEN5XComponent::write_tuning_parameters_(uint16_t i2c_command, const GasTuning &tuning) { + uint16_t params[6]; + params[0] = tuning.index_offset; + params[1] = tuning.learning_time_offset_hours; + params[2] = tuning.learning_time_gain_hours; + params[3] = tuning.gating_max_duration_minutes; + params[4] = tuning.std_initial; + params[5] = tuning.gain_factor; + auto result = write_command(i2c_command, params, 6); + if (!result) { + ESP_LOGE(TAG, "set tuning parameters failed. i2c command=%0xX, err=%d", i2c_command, this->last_error_); + } + return result; +} + +bool SEN5XComponent::write_temperature_compensation_(const TemperatureCompensation &compensation) { + uint16_t params[3]; + params[0] = compensation.offset; + params[1] = compensation.normalized_offset_slope; + params[2] = compensation.time_constant; + if (!write_command(SEN5X_CMD_TEMPERATURE_COMPENSATION, params, 3)) { + ESP_LOGE(TAG, "set temperature_compensation failed. Err=%d", this->last_error_); + return false; + } + return true; +} + +bool SEN5XComponent::start_fan_cleaning() { + if (!write_command(SEN5X_CMD_START_CLEANING_FAN)) { + this->status_set_warning(); + ESP_LOGE(TAG, "write error start fan (%d)", this->last_error_); + return false; + } else { + ESP_LOGD(TAG, "Fan auto clean started"); + } + return true; +} + +} // namespace sen5x +} // namespace esphome diff --git a/esphome/components/sen5x/sen5x.h b/esphome/components/sen5x/sen5x.h new file mode 100644 index 0000000000..f306003a82 --- /dev/null +++ b/esphome/components/sen5x/sen5x.h @@ -0,0 +1,128 @@ +#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" + +namespace esphome { +namespace sen5x { + +enum ERRORCODE { + COMMUNICATION_FAILED, + SERIAL_NUMBER_IDENTIFICATION_FAILED, + MEASUREMENT_INIT_FAILED, + PRODUCT_NAME_FAILED, + FIRMWARE_FAILED, + UNKNOWN +}; + +// 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; + +struct Sen5xBaselines { + int32_t state0; + int32_t state1; +} PACKED; // NOLINT + +enum RhtAccelerationMode : uint16_t { LOW_ACCELERATION = 0, MEDIUM_ACCELERATION = 1, HIGH_ACCELERATION = 2 }; + +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; +}; + +struct TemperatureCompensation { + uint16_t offset; + uint16_t normalized_offset_slope; + uint16_t time_constant; +}; + +class SEN5XComponent : public PollingComponent, public sensirion_common::SensirionI2CDevice { + public: + float get_setup_priority() const override { return setup_priority::DATA; } + void setup() override; + void dump_config() override; + void update() override; + + enum Sen5xType { SEN50, SEN54, SEN55, UNKNOWN }; + + void set_pm_1_0_sensor(sensor::Sensor *pm_1_0) { pm_1_0_sensor_ = pm_1_0; } + void set_pm_2_5_sensor(sensor::Sensor *pm_2_5) { pm_2_5_sensor_ = pm_2_5; } + void set_pm_4_0_sensor(sensor::Sensor *pm_4_0) { pm_4_0_sensor_ = pm_4_0; } + void set_pm_10_0_sensor(sensor::Sensor *pm_10_0) { pm_10_0_sensor_ = pm_10_0; } + + 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_humidity_sensor(sensor::Sensor *humidity_sensor) { humidity_sensor_ = humidity_sensor; } + void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } + void set_store_baseline(bool store_baseline) { store_baseline_ = store_baseline; } + void set_acceleration_mode(RhtAccelerationMode mode) { acceleration_mode_ = mode; } + void set_auto_cleaning_interval(uint32_t auto_cleaning_interval) { auto_cleaning_interval_ = auto_cleaning_interval; } + 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; + } + void set_temperature_compensation(float offset, float normalized_offset_slope, uint16_t time_constant) { + temperature_compensation_.value().offset = offset * 200; + temperature_compensation_.value().normalized_offset_slope = normalized_offset_slope * 100; + temperature_compensation_.value().time_constant = time_constant; + } + bool start_fan_cleaning(); + + protected: + bool write_tuning_parameters_(uint16_t i2c_command, const GasTuning &tuning); + bool write_temperature_compensation_(const TemperatureCompensation &compensation); + ERRORCODE error_code_; + bool initialized_{false}; + sensor::Sensor *pm_1_0_sensor_{nullptr}; + sensor::Sensor *pm_2_5_sensor_{nullptr}; + sensor::Sensor *pm_4_0_sensor_{nullptr}; + sensor::Sensor *pm_10_0_sensor_{nullptr}; + // SEN54 and SEN55 only + sensor::Sensor *temperature_sensor_{nullptr}; + sensor::Sensor *humidity_sensor_{nullptr}; + sensor::Sensor *voc_sensor_{nullptr}; + // SEN55 only + sensor::Sensor *nox_sensor_{nullptr}; + + std::string product_name_; + uint8_t serial_number_[4]; + uint16_t firmware_version_; + Sen5xBaselines voc_baselines_storage_; + bool store_baseline_; + uint32_t seconds_since_last_store_; + ESPPreferenceObject pref_; + optional acceleration_mode_; + optional auto_cleaning_interval_; + optional voc_tuning_params_; + optional nox_tuning_params_; + optional temperature_compensation_; +}; + +} // namespace sen5x +} // namespace esphome diff --git a/esphome/components/sen5x/sensor.py b/esphome/components/sen5x/sensor.py new file mode 100644 index 0000000000..489fda8335 --- /dev/null +++ b/esphome/components/sen5x/sensor.py @@ -0,0 +1,241 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor, sensirion_common +from esphome import automation +from esphome.automation import maybe_simple_id + +from esphome.const import ( + CONF_HUMIDITY, + CONF_ID, + CONF_OFFSET, + CONF_PM_1_0, + CONF_PM_10_0, + CONF_PM_2_5, + CONF_PM_4_0, + CONF_STORE_BASELINE, + CONF_TEMPERATURE, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_NITROUS_OXIDE, + DEVICE_CLASS_PM1, + DEVICE_CLASS_PM10, + DEVICE_CLASS_PM25, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, + ICON_CHEMICAL_WEAPON, + ICON_RADIATOR, + ICON_THERMOMETER, + ICON_WATER_PERCENT, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_MICROGRAMS_PER_CUBIC_METER, + UNIT_PERCENT, +) + +CODEOWNERS = ["@martgras"] +DEPENDENCIES = ["i2c"] +AUTO_LOAD = ["sensirion_common"] + +sen5x_ns = cg.esphome_ns.namespace("sen5x") +SEN5XComponent = sen5x_ns.class_( + "SEN5XComponent", cg.PollingComponent, sensirion_common.SensirionI2CDevice +) +RhtAccelerationMode = sen5x_ns.enum("RhtAccelerationMode") + +CONF_ACCELERATION_MODE = "acceleration_mode" +CONF_ALGORITHM_TUNING = "algorithm_tuning" +CONF_AUTO_CLEANING_INTERVAL = "auto_cleaning_interval" +CONF_GAIN_FACTOR = "gain_factor" +CONF_GATING_MAX_DURATION_MINUTES = "gating_max_duration_minutes" +CONF_INDEX_OFFSET = "index_offset" +CONF_LEARNING_TIME_GAIN_HOURS = "learning_time_gain_hours" +CONF_LEARNING_TIME_OFFSET_HOURS = "learning_time_offset_hours" +CONF_NORMALIZED_OFFSET_SLOPE = "normalized_offset_slope" +CONF_NOX = "nox" +CONF_STD_INITIAL = "std_initial" +CONF_TEMPERATURE_COMPENSATION = "temperature_compensation" +CONF_TIME_CONSTANT = "time_constant" +CONF_VOC = "voc" +CONF_VOC_BASELINE = "voc_baseline" + + +# Actions +StartFanAction = sen5x_ns.class_("StartFanAction", automation.Action) + +ACCELERATION_MODES = { + "low": RhtAccelerationMode.LOW_ACCELERATION, + "medium": RhtAccelerationMode.MEDIUM_ACCELERATION, + "high": RhtAccelerationMode.HIGH_ACCELERATION, +} + +GAS_SENSOR = cv.Schema( + { + cv.Optional(CONF_ALGORITHM_TUNING): cv.Schema( + { + cv.Optional(CONF_INDEX_OFFSET, default=100): cv.int_range(1, 250), + cv.Optional(CONF_LEARNING_TIME_OFFSET_HOURS, default=12): cv.int_range( + 1, 1000 + ), + cv.Optional(CONF_LEARNING_TIME_GAIN_HOURS, default=12): cv.int_range( + 1, 1000 + ), + cv.Optional( + CONF_GATING_MAX_DURATION_MINUTES, default=720 + ): cv.int_range(0, 3000), + cv.Optional(CONF_STD_INITIAL, default=50): cv.int_, + cv.Optional(CONF_GAIN_FACTOR, default=230): cv.int_range(1, 1000), + } + ) + } +) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(SEN5XComponent), + cv.Optional(CONF_PM_1_0): sensor.sensor_schema( + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=2, + device_class=DEVICE_CLASS_PM1, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PM_2_5): sensor.sensor_schema( + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=2, + device_class=DEVICE_CLASS_PM25, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PM_4_0): sensor.sensor_schema( + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PM_10_0): sensor.sensor_schema( + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=2, + device_class=DEVICE_CLASS_PM10, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_AUTO_CLEANING_INTERVAL): cv.time_period_in_seconds_, + 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_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=2, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + icon=ICON_WATER_PERCENT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_TEMPERATURE_COMPENSATION): cv.Schema( + { + cv.Optional(CONF_OFFSET, default=0): cv.float_, + cv.Optional(CONF_NORMALIZED_OFFSET_SLOPE, default=0): cv.percentage, + cv.Optional(CONF_TIME_CONSTANT, default=0): cv.int_, + } + ), + cv.Optional(CONF_ACCELERATION_MODE): cv.enum(ACCELERATION_MODES), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x69)) +) + +SENSOR_MAP = { + CONF_PM_1_0: "set_pm_1_0_sensor", + CONF_PM_2_5: "set_pm_2_5_sensor", + CONF_PM_4_0: "set_pm_4_0_sensor", + CONF_PM_10_0: "set_pm_10_0_sensor", + CONF_VOC: "set_voc_sensor", + CONF_NOX: "set_nox_sensor", + CONF_TEMPERATURE: "set_temperature_sensor", + CONF_HUMIDITY: "set_humidity_sensor", +} + +SETTING_MAP = { + CONF_AUTO_CLEANING_INTERVAL: "set_auto_cleaning_interval", + CONF_ACCELERATION_MODE: "set_acceleration_mode", +} + + +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) + + for key, funcName in SETTING_MAP.items(): + if key in config: + cg.add(getattr(var, funcName)(config[key])) + + for key, funcName in SENSOR_MAP.items(): + if key in config: + sens = await sensor.new_sensor(config[key]) + cg.add(getattr(var, funcName)(sens)) + + if CONF_VOC in config and 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 and 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], + ) + ) + if CONF_TEMPERATURE_COMPENSATION in config: + cg.add( + var.set_temperature_compensation( + config[CONF_TEMPERATURE_COMPENSATION][CONF_OFFSET], + config[CONF_TEMPERATURE_COMPENSATION][CONF_NORMALIZED_OFFSET_SLOPE], + config[CONF_TEMPERATURE_COMPENSATION][CONF_TIME_CONSTANT], + ) + ) + + +SEN5X_ACTION_SCHEMA = maybe_simple_id( + { + cv.Required(CONF_ID): cv.use_id(SEN5XComponent), + } +) + + +@automation.register_action( + "sen5x.start_fan_autoclean", StartFanAction, SEN5X_ACTION_SCHEMA +) +async def sen54_fan_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, paren) diff --git a/tests/test5.yaml b/tests/test5.yaml index ffda860377..35f6b14f2a 100644 --- a/tests/test5.yaml +++ b/tests/test5.yaml @@ -285,6 +285,49 @@ sensor: address: 0x77 iir_filter: 2X + - platform: sen5x + id: sen54 + temperature: + name: "Temperature" + accuracy_decimals: 1 + humidity: + name: "Humidity" + accuracy_decimals: 0 + pm_1_0: + name: " PM <1µm Weight concentration" + id: pm_1_0 + accuracy_decimals: 1 + pm_2_5: + name: " PM <2.5µm Weight concentration" + id: pm_2_5 + accuracy_decimals: 1 + pm_4_0: + name: " PM <4µm Weight concentration" + id: pm_4_0 + accuracy_decimals: 1 + pm_10_0: + name: " PM <10µm Weight concentration" + id: pm_10_0 + accuracy_decimals: 1 + nox: + name: "NOx" + voc: + name: "VOC" + 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 + temperature_compensation: + offset: 0 + normalized_offset_slope: 0 + time_constant: 0 + acceleration_mode: low + store_baseline: true + address: 0x69 + script: - id: automation_test then: From 53e0fe8e51c4f357e7e9a5974fc6c95143e3200d Mon Sep 17 00:00:00 2001 From: Andre Lengwenus Date: Tue, 10 May 2022 11:05:49 +0200 Subject: [PATCH 41/56] Add SML (Smart Message Language) platform for energy meters (#2396) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/sml/__init__.py | 38 +++++ esphome/components/sml/constants.h | 48 ++++++ esphome/components/sml/sensor/__init__.py | 30 ++++ esphome/components/sml/sensor/sml_sensor.cpp | 41 +++++ esphome/components/sml/sensor/sml_sensor.h | 16 ++ esphome/components/sml/sml.cpp | 146 ++++++++++++++++++ esphome/components/sml/sml.h | 47 ++++++ esphome/components/sml/sml_parser.cpp | 131 ++++++++++++++++ esphome/components/sml/sml_parser.h | 54 +++++++ .../components/sml/text_sensor/__init__.py | 43 ++++++ .../sml/text_sensor/sml_text_sensor.cpp | 54 +++++++ .../sml/text_sensor/sml_text_sensor.h | 21 +++ 13 files changed, 670 insertions(+) create mode 100644 esphome/components/sml/__init__.py create mode 100644 esphome/components/sml/constants.h create mode 100644 esphome/components/sml/sensor/__init__.py create mode 100644 esphome/components/sml/sensor/sml_sensor.cpp create mode 100644 esphome/components/sml/sensor/sml_sensor.h create mode 100644 esphome/components/sml/sml.cpp create mode 100644 esphome/components/sml/sml.h create mode 100644 esphome/components/sml/sml_parser.cpp create mode 100644 esphome/components/sml/sml_parser.h create mode 100644 esphome/components/sml/text_sensor/__init__.py create mode 100644 esphome/components/sml/text_sensor/sml_text_sensor.cpp create mode 100644 esphome/components/sml/text_sensor/sml_text_sensor.h diff --git a/CODEOWNERS b/CODEOWNERS index 77c9d30c5d..3a511275e1 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -182,6 +182,7 @@ esphome/components/sht4x/* @sjtrny esphome/components/shutdown/* @esphome/core @jsuanet esphome/components/sim800l/* @glmnet esphome/components/sm2135/* @BoukeHaarsma23 +esphome/components/sml/* @alengwenus esphome/components/socket/* @esphome/core esphome/components/sonoff_d1/* @anatoly-savchenkov esphome/components/spi/* @esphome/core diff --git a/esphome/components/sml/__init__.py b/esphome/components/sml/__init__.py new file mode 100644 index 0000000000..f3b6dd95ef --- /dev/null +++ b/esphome/components/sml/__init__.py @@ -0,0 +1,38 @@ +import re + +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import uart +from esphome.const import CONF_ID + +CODEOWNERS = ["@alengwenus"] + +DEPENDENCIES = ["uart"] + +sml_ns = cg.esphome_ns.namespace("sml") +Sml = sml_ns.class_("Sml", cg.Component, uart.UARTDevice) +MULTI_CONF = True + +CONF_SML_ID = "sml_id" +CONF_OBIS_CODE = "obis_code" +CONF_SERVER_ID = "server_id" + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(Sml), + } +).extend(uart.UART_DEVICE_SCHEMA) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await uart.register_uart_device(var, config) + + +def obis_code(value): + value = cv.string(value) + match = re.match(r"^\d{1,3}-\d{1,3}:\d{1,3}\.\d{1,3}\.\d{1,3}$", value) + if match is None: + raise cv.Invalid(f"{value} is not a valid OBIS code") + return value diff --git a/esphome/components/sml/constants.h b/esphome/components/sml/constants.h new file mode 100644 index 0000000000..22114fd233 --- /dev/null +++ b/esphome/components/sml/constants.h @@ -0,0 +1,48 @@ +#pragma once + +#include + +namespace esphome { +namespace sml { + +enum SmlType : uint8_t { + SML_OCTET = 0, + SML_BOOL = 4, + SML_INT = 5, + SML_UINT = 6, + SML_LIST = 7, + SML_HEX = 10, + SML_UNDEFINED = 255 +}; + +enum SmlMessageType : uint16_t { SML_PUBLIC_OPEN_RES = 0x0101, SML_GET_LIST_RES = 0x701 }; + +enum Crc16CheckResult : uint8_t { CHECK_CRC16_FAILED, CHECK_CRC16_X25_SUCCESS, CHECK_CRC16_KERMIT_SUCCESS }; + +// masks with two-bit mapping 0x1b -> 0b01; 0x01 -> 0b10; 0x1a -> 0b11 +const uint16_t START_MASK = 0x55aa; // 0x1b 1b 1b 1b 1b 01 01 01 01 +const uint16_t END_MASK = 0x0157; // 0x1b 1b 1b 1b 1a + +const uint16_t CRC16_X25_TABLE[256] = { + 0x0000, 0x1189, 0x2312, 0x329b, 0x4624, 0x57ad, 0x6536, 0x74bf, 0x8c48, 0x9dc1, 0xaf5a, 0xbed3, 0xca6c, 0xdbe5, + 0xe97e, 0xf8f7, 0x1081, 0x0108, 0x3393, 0x221a, 0x56a5, 0x472c, 0x75b7, 0x643e, 0x9cc9, 0x8d40, 0xbfdb, 0xae52, + 0xdaed, 0xcb64, 0xf9ff, 0xe876, 0x2102, 0x308b, 0x0210, 0x1399, 0x6726, 0x76af, 0x4434, 0x55bd, 0xad4a, 0xbcc3, + 0x8e58, 0x9fd1, 0xeb6e, 0xfae7, 0xc87c, 0xd9f5, 0x3183, 0x200a, 0x1291, 0x0318, 0x77a7, 0x662e, 0x54b5, 0x453c, + 0xbdcb, 0xac42, 0x9ed9, 0x8f50, 0xfbef, 0xea66, 0xd8fd, 0xc974, 0x4204, 0x538d, 0x6116, 0x709f, 0x0420, 0x15a9, + 0x2732, 0x36bb, 0xce4c, 0xdfc5, 0xed5e, 0xfcd7, 0x8868, 0x99e1, 0xab7a, 0xbaf3, 0x5285, 0x430c, 0x7197, 0x601e, + 0x14a1, 0x0528, 0x37b3, 0x263a, 0xdecd, 0xcf44, 0xfddf, 0xec56, 0x98e9, 0x8960, 0xbbfb, 0xaa72, 0x6306, 0x728f, + 0x4014, 0x519d, 0x2522, 0x34ab, 0x0630, 0x17b9, 0xef4e, 0xfec7, 0xcc5c, 0xddd5, 0xa96a, 0xb8e3, 0x8a78, 0x9bf1, + 0x7387, 0x620e, 0x5095, 0x411c, 0x35a3, 0x242a, 0x16b1, 0x0738, 0xffcf, 0xee46, 0xdcdd, 0xcd54, 0xb9eb, 0xa862, + 0x9af9, 0x8b70, 0x8408, 0x9581, 0xa71a, 0xb693, 0xc22c, 0xd3a5, 0xe13e, 0xf0b7, 0x0840, 0x19c9, 0x2b52, 0x3adb, + 0x4e64, 0x5fed, 0x6d76, 0x7cff, 0x9489, 0x8500, 0xb79b, 0xa612, 0xd2ad, 0xc324, 0xf1bf, 0xe036, 0x18c1, 0x0948, + 0x3bd3, 0x2a5a, 0x5ee5, 0x4f6c, 0x7df7, 0x6c7e, 0xa50a, 0xb483, 0x8618, 0x9791, 0xe32e, 0xf2a7, 0xc03c, 0xd1b5, + 0x2942, 0x38cb, 0x0a50, 0x1bd9, 0x6f66, 0x7eef, 0x4c74, 0x5dfd, 0xb58b, 0xa402, 0x9699, 0x8710, 0xf3af, 0xe226, + 0xd0bd, 0xc134, 0x39c3, 0x284a, 0x1ad1, 0x0b58, 0x7fe7, 0x6e6e, 0x5cf5, 0x4d7c, 0xc60c, 0xd785, 0xe51e, 0xf497, + 0x8028, 0x91a1, 0xa33a, 0xb2b3, 0x4a44, 0x5bcd, 0x6956, 0x78df, 0x0c60, 0x1de9, 0x2f72, 0x3efb, 0xd68d, 0xc704, + 0xf59f, 0xe416, 0x90a9, 0x8120, 0xb3bb, 0xa232, 0x5ac5, 0x4b4c, 0x79d7, 0x685e, 0x1ce1, 0x0d68, 0x3ff3, 0x2e7a, + 0xe70e, 0xf687, 0xc41c, 0xd595, 0xa12a, 0xb0a3, 0x8238, 0x93b1, 0x6b46, 0x7acf, 0x4854, 0x59dd, 0x2d62, 0x3ceb, + 0x0e70, 0x1ff9, 0xf78f, 0xe606, 0xd49d, 0xc514, 0xb1ab, 0xa022, 0x92b9, 0x8330, 0x7bc7, 0x6a4e, 0x58d5, 0x495c, + 0x3de3, 0x2c6a, 0x1ef1, 0x0f78}; + +} // namespace sml +} // namespace esphome diff --git a/esphome/components/sml/sensor/__init__.py b/esphome/components/sml/sensor/__init__.py new file mode 100644 index 0000000000..a1fcc5e7ae --- /dev/null +++ b/esphome/components/sml/sensor/__init__.py @@ -0,0 +1,30 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor +from esphome.const import CONF_ID + +from .. import CONF_OBIS_CODE, CONF_SERVER_ID, CONF_SML_ID, Sml, obis_code, sml_ns + +AUTO_LOAD = ["sml"] + +SmlSensor = sml_ns.class_("SmlSensor", sensor.Sensor, cg.Component) + + +CONFIG_SCHEMA = sensor.sensor_schema().extend( + { + cv.GenerateID(): cv.declare_id(SmlSensor), + cv.GenerateID(CONF_SML_ID): cv.use_id(Sml), + cv.Required(CONF_OBIS_CODE): obis_code, + cv.Optional(CONF_SERVER_ID, default=""): cv.string, + } +) + + +async def to_code(config): + var = cg.new_Pvariable( + config[CONF_ID], config[CONF_SERVER_ID], config[CONF_OBIS_CODE] + ) + await cg.register_component(var, config) + await sensor.register_sensor(var, config) + sml = await cg.get_variable(config[CONF_SML_ID]) + cg.add(sml.register_sml_listener(var)) diff --git a/esphome/components/sml/sensor/sml_sensor.cpp b/esphome/components/sml/sensor/sml_sensor.cpp new file mode 100644 index 0000000000..e9a384d275 --- /dev/null +++ b/esphome/components/sml/sensor/sml_sensor.cpp @@ -0,0 +1,41 @@ +#include "esphome/core/log.h" +#include "sml_sensor.h" +#include "../sml_parser.h" + +namespace esphome { +namespace sml { + +static const char *const TAG = "sml_sensor"; + +SmlSensor::SmlSensor(std::string server_id, std::string obis_code) + : SmlListener(std::move(server_id), std::move(obis_code)) {} + +void SmlSensor::publish_val(const ObisInfo &obis_info) { + switch (obis_info.value_type) { + case SML_INT: { + publish_state(bytes_to_int(obis_info.value)); + break; + } + case SML_BOOL: + case SML_UINT: { + publish_state(bytes_to_uint(obis_info.value)); + break; + } + case SML_OCTET: { + ESP_LOGW(TAG, "No number conversion for (%s) %s. Consider using SML TextSensor instead.", + bytes_repr(obis_info.server_id).c_str(), obis_info.code_repr().c_str()); + break; + } + } +} + +void SmlSensor::dump_config() { + LOG_SENSOR("", "SML", this); + if (!this->server_id.empty()) { + ESP_LOGCONFIG(TAG, " Server ID: %s", this->server_id.c_str()); + } + ESP_LOGCONFIG(TAG, " OBIS Code: %s", this->obis_code.c_str()); +} + +} // namespace sml +} // namespace esphome diff --git a/esphome/components/sml/sensor/sml_sensor.h b/esphome/components/sml/sensor/sml_sensor.h new file mode 100644 index 0000000000..eb7b108f94 --- /dev/null +++ b/esphome/components/sml/sensor/sml_sensor.h @@ -0,0 +1,16 @@ +#pragma once +#include "esphome/components/sml/sml.h" +#include "esphome/components/sensor/sensor.h" + +namespace esphome { +namespace sml { + +class SmlSensor : public SmlListener, public sensor::Sensor, public Component { + public: + SmlSensor(std::string server_id, std::string obis_code); + void publish_val(const ObisInfo &obis_info) override; + void dump_config() override; +}; + +} // namespace sml +} // namespace esphome diff --git a/esphome/components/sml/sml.cpp b/esphome/components/sml/sml.cpp new file mode 100644 index 0000000000..c6fe7d64cd --- /dev/null +++ b/esphome/components/sml/sml.cpp @@ -0,0 +1,146 @@ +#include "sml.h" +#include "esphome/core/log.h" +#include "sml_parser.h" + +namespace esphome { +namespace sml { + +static const char *const TAG = "sml"; + +const char START_BYTES_DETECTED = 1; +const char END_BYTES_DETECTED = 2; + +SmlListener::SmlListener(std::string server_id, std::string obis_code) + : server_id(std::move(server_id)), obis_code(std::move(obis_code)) {} + +char Sml::check_start_end_bytes_(uint8_t byte) { + this->incoming_mask_ = (this->incoming_mask_ << 2) | get_code(byte); + + if (this->incoming_mask_ == START_MASK) + return START_BYTES_DETECTED; + if ((this->incoming_mask_ >> 6) == END_MASK) + return END_BYTES_DETECTED; + return 0; +} + +void Sml::loop() { + while (available()) { + const char c = read(); + + if (this->record_) + this->sml_data_.emplace_back(c); + + switch (this->check_start_end_bytes_(c)) { + case START_BYTES_DETECTED: { + this->record_ = true; + this->sml_data_.clear(); + break; + }; + case END_BYTES_DETECTED: { + if (this->record_) { + this->record_ = false; + + if (!check_sml_data(this->sml_data_)) + break; + + // remove footer bytes + this->sml_data_.resize(this->sml_data_.size() - 8); + this->process_sml_file_(this->sml_data_); + } + break; + }; + }; + } +} + +void Sml::process_sml_file_(const bytes &sml_data) { + SmlFile sml_file = SmlFile(sml_data); + std::vector obis_info = sml_file.get_obis_info(); + this->publish_obis_info_(obis_info); + + this->log_obis_info_(obis_info); +} + +void Sml::log_obis_info_(const std::vector &obis_info_vec) { + ESP_LOGD(TAG, "OBIS info:"); + for (auto const &obis_info : obis_info_vec) { + std::string info; + info += " (" + bytes_repr(obis_info.server_id) + ") "; + info += obis_info.code_repr(); + info += " [0x" + bytes_repr(obis_info.value) + "]"; + ESP_LOGD(TAG, "%s", info.c_str()); + } +} + +void Sml::publish_obis_info_(const std::vector &obis_info_vec) { + for (auto const &obis_info : obis_info_vec) { + this->publish_value_(obis_info); + } +} + +void Sml::publish_value_(const ObisInfo &obis_info) { + for (auto const &sml_listener : sml_listeners_) { + if ((!sml_listener->server_id.empty()) && (bytes_repr(obis_info.server_id) != sml_listener->server_id)) + continue; + if (obis_info.code_repr() != sml_listener->obis_code) + continue; + sml_listener->publish_val(obis_info); + } +} + +void Sml::dump_config() { ESP_LOGCONFIG(TAG, "SML:"); } + +void Sml::register_sml_listener(SmlListener *listener) { sml_listeners_.emplace_back(listener); } + +bool check_sml_data(const bytes &buffer) { + if (buffer.size() < 2) { + ESP_LOGW(TAG, "Checksum error in received SML data."); + return false; + } + + uint16_t crc_received = (buffer.at(buffer.size() - 2) << 8) | buffer.at(buffer.size() - 1); + if (crc_received == calc_crc16_x25(buffer.begin(), buffer.end() - 2, 0x6e23)) { + ESP_LOGV(TAG, "Checksum verification successful with CRC16/X25."); + return true; + } + + if (crc_received == calc_crc16_kermit(buffer.begin(), buffer.end() - 2, 0xed50)) { + ESP_LOGV(TAG, "Checksum verification successful with CRC16/KERMIT."); + return true; + } + + ESP_LOGW(TAG, "Checksum error in received SML data."); + return false; +} + +uint16_t calc_crc16_p1021(bytes::const_iterator begin, bytes::const_iterator end, uint16_t crcsum) { + for (auto it = begin; it != end; it++) { + crcsum = (crcsum >> 8) ^ CRC16_X25_TABLE[(crcsum & 0xff) ^ *it]; + } + return crcsum; +} + +uint16_t calc_crc16_x25(bytes::const_iterator begin, bytes::const_iterator end, uint16_t crcsum = 0) { + crcsum = calc_crc16_p1021(begin, end, crcsum ^ 0xffff) ^ 0xffff; + return (crcsum >> 8) | ((crcsum & 0xff) << 8); +} + +uint16_t calc_crc16_kermit(bytes::const_iterator begin, bytes::const_iterator end, uint16_t crcsum = 0) { + return calc_crc16_p1021(begin, end, crcsum); +} + +uint8_t get_code(uint8_t byte) { + switch (byte) { + case 0x1b: + return 1; + case 0x01: + return 2; + case 0x1a: + return 3; + default: + return 0; + } +} + +} // namespace sml +} // namespace esphome diff --git a/esphome/components/sml/sml.h b/esphome/components/sml/sml.h new file mode 100644 index 0000000000..ac7befb043 --- /dev/null +++ b/esphome/components/sml/sml.h @@ -0,0 +1,47 @@ +#pragma once + +#include +#include +#include "esphome/core/component.h" +#include "esphome/components/uart/uart.h" +#include "sml_parser.h" + +namespace esphome { +namespace sml { + +class SmlListener { + public: + std::string server_id; + std::string obis_code; + SmlListener(std::string server_id, std::string obis_code); + virtual void publish_val(const ObisInfo &obis_info){}; +}; + +class Sml : public Component, public uart::UARTDevice { + public: + void register_sml_listener(SmlListener *listener); + void loop() override; + void dump_config() override; + std::vector sml_listeners_{}; + + protected: + void process_sml_file_(const bytes &sml_data); + void log_obis_info_(const std::vector &obis_info_vec); + void publish_obis_info_(const std::vector &obis_info_vec); + char check_start_end_bytes_(uint8_t byte); + void publish_value_(const ObisInfo &obis_info); + + // Serial parser + bool record_ = false; + uint16_t incoming_mask_ = 0; + bytes sml_data_; +}; + +bool check_sml_data(const bytes &buffer); +uint16_t calc_crc16_p1021(bytes::const_iterator begin, bytes::const_iterator end, uint16_t crcsum); +uint16_t calc_crc16_x25(bytes::const_iterator begin, bytes::const_iterator end, uint16_t crcsum); +uint16_t calc_crc16_kermit(bytes::const_iterator begin, bytes::const_iterator end, uint16_t crcsum); + +uint8_t get_code(uint8_t byte); +} // namespace sml +} // namespace esphome diff --git a/esphome/components/sml/sml_parser.cpp b/esphome/components/sml/sml_parser.cpp new file mode 100644 index 0000000000..ff7da4cabd --- /dev/null +++ b/esphome/components/sml/sml_parser.cpp @@ -0,0 +1,131 @@ +#include "esphome/core/helpers.h" +#include "constants.h" +#include "sml_parser.h" + +namespace esphome { +namespace sml { + +SmlFile::SmlFile(bytes buffer) : buffer_(std::move(buffer)) { + // extract messages + this->pos_ = 0; + while (this->pos_ < this->buffer_.size()) { + if (this->buffer_[this->pos_] == 0x00) + break; // fill byte detected -> no more messages + + SmlNode message = SmlNode(); + if (!this->setup_node(&message)) + break; + this->messages.emplace_back(message); + } +} + +bool SmlFile::setup_node(SmlNode *node) { + uint8_t type = this->buffer_[this->pos_] >> 4; // type including overlength info + uint8_t length = this->buffer_[this->pos_] & 0x0f; // length including TL bytes + bool is_list = (type & 0x07) == SML_LIST; + bool has_extended_length = type & 0x08; // we have a long list/value (>15 entries) + uint8_t parse_length = length; + if (has_extended_length) { + length = (length << 4) + (this->buffer_[this->pos_ + 1] & 0x0f); + parse_length = length - 1; + this->pos_ += 1; + } + + if (this->pos_ + parse_length >= this->buffer_.size()) + return false; + + node->type = type & 0x07; + node->nodes.clear(); + node->value_bytes.clear(); + if (this->buffer_[this->pos_] == 0x00) { // end of message + this->pos_ += 1; + } else if (is_list) { // list + this->pos_ += 1; + node->nodes.reserve(parse_length); + for (size_t i = 0; i != parse_length; i++) { + SmlNode child_node = SmlNode(); + if (!this->setup_node(&child_node)) + return false; + node->nodes.emplace_back(child_node); + } + } else { // value + node->value_bytes = + bytes(this->buffer_.begin() + this->pos_ + 1, this->buffer_.begin() + this->pos_ + parse_length); + this->pos_ += parse_length; + } + return true; +} + +std::vector SmlFile::get_obis_info() { + std::vector obis_info; + for (auto const &message : messages) { + SmlNode message_body = message.nodes[3]; + uint16_t message_type = bytes_to_uint(message_body.nodes[0].value_bytes); + if (message_type != SML_GET_LIST_RES) + continue; + + SmlNode get_list_response = message_body.nodes[1]; + bytes server_id = get_list_response.nodes[1].value_bytes; + SmlNode val_list = get_list_response.nodes[4]; + + for (auto const &val_list_entry : val_list.nodes) { + obis_info.emplace_back(server_id, val_list_entry); + } + } + return obis_info; +} + +std::string bytes_repr(const bytes &buffer) { + std::string repr; + for (auto const value : buffer) { + repr += str_sprintf("%02x", value & 0xff); + } + return repr; +} + +uint64_t bytes_to_uint(const bytes &buffer) { + uint64_t val = 0; + for (auto const value : buffer) { + val = (val << 8) + value; + } + return val; +} + +int64_t bytes_to_int(const bytes &buffer) { + uint64_t tmp = bytes_to_uint(buffer); + int64_t val; + + switch (buffer.size()) { + case 1: // int8 + val = (int8_t) tmp; + break; + case 2: // int16 + val = (int16_t) tmp; + break; + case 4: // int32 + val = (int32_t) tmp; + break; + default: // int64 + val = (int64_t) tmp; + } + return val; +} + +std::string bytes_to_string(const bytes &buffer) { return std::string(buffer.begin(), buffer.end()); } + +ObisInfo::ObisInfo(bytes server_id, SmlNode val_list_entry) : server_id(std::move(server_id)) { + this->code = val_list_entry.nodes[0].value_bytes; + this->status = val_list_entry.nodes[1].value_bytes; + this->unit = bytes_to_uint(val_list_entry.nodes[3].value_bytes); + this->scaler = bytes_to_int(val_list_entry.nodes[4].value_bytes); + SmlNode value_node = val_list_entry.nodes[5]; + this->value = value_node.value_bytes; + this->value_type = value_node.type; +} + +std::string ObisInfo::code_repr() const { + return str_sprintf("%d-%d:%d.%d.%d", this->code[0], this->code[1], this->code[2], this->code[3], this->code[4]); +} + +} // namespace sml +} // namespace esphome diff --git a/esphome/components/sml/sml_parser.h b/esphome/components/sml/sml_parser.h new file mode 100644 index 0000000000..fca859d4b8 --- /dev/null +++ b/esphome/components/sml/sml_parser.h @@ -0,0 +1,54 @@ +#pragma once + +#include +#include +#include +#include +#include "constants.h" + +namespace esphome { +namespace sml { + +using bytes = std::vector; + +class SmlNode { + public: + uint8_t type; + bytes value_bytes; + std::vector nodes; +}; + +class ObisInfo { + public: + ObisInfo(bytes server_id, SmlNode val_list_entry); + bytes server_id; + bytes code; + bytes status; + char unit; + char scaler; + bytes value; + uint16_t value_type; + std::string code_repr() const; +}; + +class SmlFile { + public: + SmlFile(bytes buffer); + bool setup_node(SmlNode *node); + std::vector messages; + std::vector get_obis_info(); + + protected: + const bytes buffer_; + size_t pos_; +}; + +std::string bytes_repr(const bytes &buffer); + +uint64_t bytes_to_uint(const bytes &buffer); + +int64_t bytes_to_int(const bytes &buffer); + +std::string bytes_to_string(const bytes &buffer); +} // namespace sml +} // namespace esphome diff --git a/esphome/components/sml/text_sensor/__init__.py b/esphome/components/sml/text_sensor/__init__.py new file mode 100644 index 0000000000..81564bf65b --- /dev/null +++ b/esphome/components/sml/text_sensor/__init__.py @@ -0,0 +1,43 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import text_sensor +from esphome.const import CONF_FORMAT, CONF_ID + +from .. import CONF_OBIS_CODE, CONF_SERVER_ID, CONF_SML_ID, Sml, obis_code, sml_ns + +AUTO_LOAD = ["sml"] + +SmlType = sml_ns.enum("SmlType") +SML_TYPES = { + "text": SmlType.SML_OCTET, + "bool": SmlType.SML_BOOL, + "int": SmlType.SML_INT, + "uint": SmlType.SML_UINT, + "hex": SmlType.SML_HEX, + "": SmlType.SML_UNDEFINED, +} + +SmlTextSensor = sml_ns.class_("SmlTextSensor", text_sensor.TextSensor, cg.Component) + +CONFIG_SCHEMA = text_sensor.TEXT_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(SmlTextSensor), + cv.GenerateID(CONF_SML_ID): cv.use_id(Sml), + cv.Required(CONF_OBIS_CODE): obis_code, + cv.Optional(CONF_SERVER_ID, default=""): cv.string, + cv.Optional(CONF_FORMAT, default=""): cv.enum(SML_TYPES, lower=True), + } +) + + +async def to_code(config): + var = cg.new_Pvariable( + config[CONF_ID], + config[CONF_SERVER_ID], + config[CONF_OBIS_CODE], + config[CONF_FORMAT], + ) + await cg.register_component(var, config) + await text_sensor.register_text_sensor(var, config) + sml = await cg.get_variable(config[CONF_SML_ID]) + cg.add(sml.register_sml_listener(var)) diff --git a/esphome/components/sml/text_sensor/sml_text_sensor.cpp b/esphome/components/sml/text_sensor/sml_text_sensor.cpp new file mode 100644 index 0000000000..64f10698f0 --- /dev/null +++ b/esphome/components/sml/text_sensor/sml_text_sensor.cpp @@ -0,0 +1,54 @@ +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" +#include "sml_text_sensor.h" +#include "../sml_parser.h" + +namespace esphome { +namespace sml { + +static const char *const TAG = "sml_text_sensor"; + +SmlTextSensor::SmlTextSensor(std::string server_id, std::string obis_code, SmlType format) + : SmlListener(std::move(server_id), std::move(obis_code)), format_(format) {} + +void SmlTextSensor::publish_val(const ObisInfo &obis_info) { + uint8_t value_type; + if (this->format_ == SML_UNDEFINED) { + value_type = obis_info.value_type; + } else { + value_type = this->format_; + } + + switch (value_type) { + case SML_HEX: { + publish_state("0x" + bytes_repr(obis_info.value)); + break; + } + case SML_INT: { + publish_state(to_string(bytes_to_int(obis_info.value))); + break; + } + case SML_BOOL: + publish_state(bytes_to_uint(obis_info.value) ? "True" : "False"); + break; + case SML_UINT: { + publish_state(to_string(bytes_to_uint(obis_info.value))); + break; + } + case SML_OCTET: { + publish_state(std::string(obis_info.value.begin(), obis_info.value.end())); + break; + } + } +} + +void SmlTextSensor::dump_config() { + LOG_TEXT_SENSOR("", "SML", this); + if (!this->server_id.empty()) { + ESP_LOGCONFIG(TAG, " Server ID: %s", this->server_id.c_str()); + } + ESP_LOGCONFIG(TAG, " OBIS Code: %s", this->obis_code.c_str()); +} + +} // namespace sml +} // namespace esphome diff --git a/esphome/components/sml/text_sensor/sml_text_sensor.h b/esphome/components/sml/text_sensor/sml_text_sensor.h new file mode 100644 index 0000000000..20d27c9f71 --- /dev/null +++ b/esphome/components/sml/text_sensor/sml_text_sensor.h @@ -0,0 +1,21 @@ +#pragma once + +#include "esphome/components/sml/sml.h" +#include "esphome/components/text_sensor/text_sensor.h" +#include "../constants.h" + +namespace esphome { +namespace sml { + +class SmlTextSensor : public SmlListener, public text_sensor::TextSensor, public Component { + public: + SmlTextSensor(std::string server_id, std::string obis_code, SmlType format); + void publish_val(const ObisInfo &obis_info) override; + void dump_config() override; + + protected: + SmlType format_; +}; + +} // namespace sml +} // namespace esphome From 4e1f6518e829bcd80422cdf76b9d359603f23ae9 Mon Sep 17 00:00:00 2001 From: George Date: Tue, 10 May 2022 19:22:22 +1000 Subject: [PATCH 42/56] Delonghi Penguino PAC W120HP ir support (#3124) --- CODEOWNERS | 1 + esphome/components/delonghi/__init__.py | 1 + esphome/components/delonghi/climate.py | 20 +++ esphome/components/delonghi/delonghi.cpp | 186 +++++++++++++++++++++++ esphome/components/delonghi/delonghi.h | 64 ++++++++ tests/test1.yaml | 2 + 6 files changed, 274 insertions(+) create mode 100644 esphome/components/delonghi/__init__.py create mode 100644 esphome/components/delonghi/climate.py create mode 100644 esphome/components/delonghi/delonghi.cpp create mode 100644 esphome/components/delonghi/delonghi.h diff --git a/CODEOWNERS b/CODEOWNERS index 3a511275e1..16b9008379 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -55,6 +55,7 @@ esphome/components/current_based/* @djwmarcx esphome/components/daly_bms/* @s1lvi0 esphome/components/dashboard_import/* @esphome/core esphome/components/debug/* @OttoWinter +esphome/components/delonghi/* @grob6000 esphome/components/dfplayer/* @glmnet esphome/components/dht/* @OttoWinter esphome/components/ds1307/* @badbadc0ffee diff --git a/esphome/components/delonghi/__init__.py b/esphome/components/delonghi/__init__.py new file mode 100644 index 0000000000..0a81eb2da7 --- /dev/null +++ b/esphome/components/delonghi/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@grob6000"] diff --git a/esphome/components/delonghi/climate.py b/esphome/components/delonghi/climate.py new file mode 100644 index 0000000000..614706defe --- /dev/null +++ b/esphome/components/delonghi/climate.py @@ -0,0 +1,20 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import climate_ir +from esphome.const import CONF_ID + +AUTO_LOAD = ["climate_ir"] + +delonghi_ns = cg.esphome_ns.namespace("delonghi") +DelonghiClimate = delonghi_ns.class_("DelonghiClimate", climate_ir.ClimateIR) + +CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(DelonghiClimate), + } +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await climate_ir.register_climate_ir(var, config) diff --git a/esphome/components/delonghi/delonghi.cpp b/esphome/components/delonghi/delonghi.cpp new file mode 100644 index 0000000000..9bc0b5753d --- /dev/null +++ b/esphome/components/delonghi/delonghi.cpp @@ -0,0 +1,186 @@ +#include "delonghi.h" +#include "esphome/components/remote_base/remote_base.h" + +namespace esphome { +namespace delonghi { + +static const char *const TAG = "delonghi.climate"; + +void DelonghiClimate::transmit_state() { + uint8_t remote_state[DELONGHI_STATE_FRAME_SIZE] = {0}; + remote_state[0] = DELONGHI_ADDRESS; + remote_state[1] = this->temperature_(); + remote_state[1] |= (this->fan_speed_()) << 5; + remote_state[2] = this->operation_mode_(); + // Calculate checksum + for (int i = 0; i < DELONGHI_STATE_FRAME_SIZE - 1; i++) { + remote_state[DELONGHI_STATE_FRAME_SIZE - 1] += remote_state[i]; + } + + auto transmit = this->transmitter_->transmit(); + auto *data = transmit.get_data(); + data->set_carrier_frequency(DELONGHI_IR_FREQUENCY); + + data->mark(DELONGHI_HEADER_MARK); + data->space(DELONGHI_HEADER_SPACE); + for (unsigned char b : remote_state) { + for (uint8_t mask = 1; mask > 0; mask <<= 1) { // iterate through bit mask + data->mark(DELONGHI_BIT_MARK); + bool bit = b & mask; + data->space(bit ? DELONGHI_ONE_SPACE : DELONGHI_ZERO_SPACE); + } + } + data->mark(DELONGHI_BIT_MARK); + data->space(0); + + transmit.perform(); +} + +uint8_t DelonghiClimate::operation_mode_() { + uint8_t operating_mode = DELONGHI_MODE_ON; + switch (this->mode) { + case climate::CLIMATE_MODE_COOL: + operating_mode |= DELONGHI_MODE_COOL; + break; + case climate::CLIMATE_MODE_DRY: + operating_mode |= DELONGHI_MODE_DRY; + break; + case climate::CLIMATE_MODE_HEAT: + operating_mode |= DELONGHI_MODE_HEAT; + break; + case climate::CLIMATE_MODE_HEAT_COOL: + operating_mode |= DELONGHI_MODE_AUTO; + break; + case climate::CLIMATE_MODE_FAN_ONLY: + operating_mode |= DELONGHI_MODE_FAN; + break; + case climate::CLIMATE_MODE_OFF: + default: + operating_mode = DELONGHI_MODE_OFF; + break; + } + return operating_mode; +} + +uint16_t DelonghiClimate::fan_speed_() { + uint16_t fan_speed; + switch (this->fan_mode.value()) { + case climate::CLIMATE_FAN_LOW: + fan_speed = DELONGHI_FAN_LOW; + break; + case climate::CLIMATE_FAN_MEDIUM: + fan_speed = DELONGHI_FAN_MEDIUM; + break; + case climate::CLIMATE_FAN_HIGH: + fan_speed = DELONGHI_FAN_HIGH; + break; + case climate::CLIMATE_FAN_AUTO: + default: + fan_speed = DELONGHI_FAN_AUTO; + } + return fan_speed; +} + +uint8_t DelonghiClimate::temperature_() { + // Force special temperatures depending on the mode + uint8_t temperature = 0b0001; + switch (this->mode) { + case climate::CLIMATE_MODE_HEAT: + temperature = (uint8_t) roundf(this->target_temperature) - DELONGHI_TEMP_OFFSET_HEAT; + break; + case climate::CLIMATE_MODE_COOL: + case climate::CLIMATE_MODE_DRY: + case climate::CLIMATE_MODE_HEAT_COOL: + case climate::CLIMATE_MODE_FAN_ONLY: + case climate::CLIMATE_MODE_OFF: + default: + temperature = (uint8_t) roundf(this->target_temperature) - DELONGHI_TEMP_OFFSET_COOL; + } + if (temperature > 0x0F) { + temperature = 0x0F; // clamp maximum + } + return temperature; +} + +bool DelonghiClimate::parse_state_frame_(const uint8_t frame[]) { + uint8_t checksum = 0; + for (int i = 0; i < (DELONGHI_STATE_FRAME_SIZE - 1); i++) { + checksum += frame[i]; + } + if (frame[DELONGHI_STATE_FRAME_SIZE - 1] != checksum) { + return false; + } + uint8_t mode = frame[2] & 0x0F; + if (mode & DELONGHI_MODE_ON) { + switch (mode & 0x0E) { + case DELONGHI_MODE_COOL: + this->mode = climate::CLIMATE_MODE_COOL; + break; + case DELONGHI_MODE_DRY: + this->mode = climate::CLIMATE_MODE_DRY; + break; + case DELONGHI_MODE_HEAT: + this->mode = climate::CLIMATE_MODE_HEAT; + break; + case DELONGHI_MODE_AUTO: + this->mode = climate::CLIMATE_MODE_HEAT_COOL; + break; + case DELONGHI_MODE_FAN: + this->mode = climate::CLIMATE_MODE_FAN_ONLY; + break; + } + } else { + this->mode = climate::CLIMATE_MODE_OFF; + } + uint8_t temperature = frame[1] & 0x0F; + if (this->mode == climate::CLIMATE_MODE_HEAT) { + this->target_temperature = temperature + DELONGHI_TEMP_OFFSET_HEAT; + } else { + this->target_temperature = temperature + DELONGHI_TEMP_OFFSET_COOL; + } + uint8_t fan_mode = frame[1] >> 5; + switch (fan_mode) { + case DELONGHI_FAN_LOW: + this->fan_mode = climate::CLIMATE_FAN_LOW; + break; + case DELONGHI_FAN_MEDIUM: + this->fan_mode = climate::CLIMATE_FAN_MEDIUM; + break; + case DELONGHI_FAN_HIGH: + this->fan_mode = climate::CLIMATE_FAN_HIGH; + break; + case DELONGHI_FAN_AUTO: + this->fan_mode = climate::CLIMATE_FAN_AUTO; + break; + } + this->publish_state(); + return true; +} + +bool DelonghiClimate::on_receive(remote_base::RemoteReceiveData data) { + uint8_t state_frame[DELONGHI_STATE_FRAME_SIZE] = {}; + if (!data.expect_item(DELONGHI_HEADER_MARK, DELONGHI_HEADER_SPACE)) { + return false; + } + for (uint8_t pos = 0; pos < DELONGHI_STATE_FRAME_SIZE; pos++) { + uint8_t byte = 0; + for (int8_t bit = 0; bit < 8; bit++) { + if (data.expect_item(DELONGHI_BIT_MARK, DELONGHI_ONE_SPACE)) { + byte |= 1 << bit; + } else if (!data.expect_item(DELONGHI_BIT_MARK, DELONGHI_ZERO_SPACE)) { + return false; + } + } + state_frame[pos] = byte; + if (pos == 0) { + // frame header + if (byte != DELONGHI_ADDRESS) { + return false; + } + } + } + return this->parse_state_frame_(state_frame); +} + +} // namespace delonghi +} // namespace esphome diff --git a/esphome/components/delonghi/delonghi.h b/esphome/components/delonghi/delonghi.h new file mode 100644 index 0000000000..d310a58aee --- /dev/null +++ b/esphome/components/delonghi/delonghi.h @@ -0,0 +1,64 @@ +#pragma once + +#include "esphome/components/climate_ir/climate_ir.h" + +namespace esphome { +namespace delonghi { + +// Values for DELONGHI ARC43XXX IR Controllers +const uint8_t DELONGHI_ADDRESS = 83; + +// Temperature +const uint8_t DELONGHI_TEMP_MIN = 13; // Celsius +const uint8_t DELONGHI_TEMP_MAX = 32; // Celsius +const uint8_t DELONGHI_TEMP_OFFSET_COOL = 17; // Celsius +const uint8_t DELONGHI_TEMP_OFFSET_HEAT = 12; // Celsius + +// Modes +const uint8_t DELONGHI_MODE_AUTO = 0b1000; +const uint8_t DELONGHI_MODE_COOL = 0b0000; +const uint8_t DELONGHI_MODE_HEAT = 0b0110; +const uint8_t DELONGHI_MODE_DRY = 0b0010; +const uint8_t DELONGHI_MODE_FAN = 0b0100; +const uint8_t DELONGHI_MODE_OFF = 0b0000; +const uint8_t DELONGHI_MODE_ON = 0b0001; + +// Fan Speed +const uint8_t DELONGHI_FAN_AUTO = 0b00; +const uint8_t DELONGHI_FAN_HIGH = 0b01; +const uint8_t DELONGHI_FAN_MEDIUM = 0b10; +const uint8_t DELONGHI_FAN_LOW = 0b11; + +// IR Transmission - similar to NEC1 +const uint32_t DELONGHI_IR_FREQUENCY = 38000; +const uint32_t DELONGHI_HEADER_MARK = 9000; +const uint32_t DELONGHI_HEADER_SPACE = 4500; +const uint32_t DELONGHI_BIT_MARK = 465; +const uint32_t DELONGHI_ONE_SPACE = 1750; +const uint32_t DELONGHI_ZERO_SPACE = 670; + +// State Frame size +const uint8_t DELONGHI_STATE_FRAME_SIZE = 8; + +class DelonghiClimate : public climate_ir::ClimateIR { + public: + DelonghiClimate() + : climate_ir::ClimateIR(DELONGHI_TEMP_MIN, DELONGHI_TEMP_MAX, 1.0f, true, true, + {climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, + climate::CLIMATE_FAN_HIGH}, + {climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL, + climate::CLIMATE_SWING_HORIZONTAL, climate::CLIMATE_SWING_BOTH}) {} + + protected: + // Transmit via IR the state of this climate controller. + void transmit_state() override; + uint8_t operation_mode_(); + uint16_t fan_speed_(); + uint8_t temperature_(); + // Handle received IR Buffer + bool on_receive(remote_base::RemoteReceiveData data) override; + bool parse_state_frame_(const uint8_t frame[]); +}; + +} // namespace delonghi +} // namespace esphome diff --git a/tests/test1.yaml b/tests/test1.yaml index aba37976aa..deaf1c237e 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -1811,6 +1811,8 @@ climate: name: Fujitsu General Climate - platform: daikin name: Daikin Climate + - platform: delonghi + name: Delonghi Climate - platform: yashima name: Yashima Climate - platform: mitsubishi From 782186e13d98cf87e99fce261d92663feae17393 Mon Sep 17 00:00:00 2001 From: Martin <25747549+martgras@users.noreply.github.com> Date: Tue, 10 May 2022 11:25:44 +0200 Subject: [PATCH 43/56] extend scd4x (#3409) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 2 +- esphome/components/scd4x/automation.h | 28 ++++ esphome/components/scd4x/scd4x.cpp | 233 ++++++++++++++++++-------- esphome/components/scd4x/scd4x.h | 19 ++- esphome/components/scd4x/sensor.py | 69 +++++++- tests/test1.yaml | 12 ++ 6 files changed, 287 insertions(+), 76 deletions(-) create mode 100644 esphome/components/scd4x/automation.h diff --git a/CODEOWNERS b/CODEOWNERS index 16b9008379..e2b29547cb 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -168,7 +168,7 @@ esphome/components/rf_bridge/* @jesserockz esphome/components/rgbct/* @jesserockz esphome/components/rtttl/* @glmnet esphome/components/safe_mode/* @jsuanet @paulmonigatti -esphome/components/scd4x/* @sjtrny +esphome/components/scd4x/* @martgras @sjtrny esphome/components/script/* @esphome/core esphome/components/sdm_meter/* @jesserockz @polyfaces esphome/components/sdp3x/* @Azimath diff --git a/esphome/components/scd4x/automation.h b/esphome/components/scd4x/automation.h new file mode 100644 index 0000000000..21ecb2ea4c --- /dev/null +++ b/esphome/components/scd4x/automation.h @@ -0,0 +1,28 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "scd4x.h" + +namespace esphome { +namespace scd4x { + +template class PerformForcedCalibrationAction : public Action, public Parented { + public: + void play(Ts... x) override { + if (this->value_.has_value()) { + this->parent_->perform_forced_calibration(value_.value()); + } + } + + protected: + TEMPLATABLE_VALUE(uint16_t, value) +}; + +template class FactoryResetAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->factory_reset(); } +}; + +} // namespace scd4x +} // namespace esphome diff --git a/esphome/components/scd4x/scd4x.cpp b/esphome/components/scd4x/scd4x.cpp index 559c95df32..cbda996a4c 100644 --- a/esphome/components/scd4x/scd4x.cpp +++ b/esphome/components/scd4x/scd4x.cpp @@ -13,39 +13,32 @@ static const uint16_t SCD4X_CMD_ALTITUDE_COMPENSATION = 0x2427; static const uint16_t SCD4X_CMD_AMBIENT_PRESSURE_COMPENSATION = 0xe000; static const uint16_t SCD4X_CMD_AUTOMATIC_SELF_CALIBRATION = 0x2416; static const uint16_t SCD4X_CMD_START_CONTINUOUS_MEASUREMENTS = 0x21b1; +static const uint16_t SCD4X_CMD_START_LOW_POWER_CONTINUOUS_MEASUREMENTS = 0x21ac; +static const uint16_t SCD4X_CMD_START_LOW_POWER_SINGLE_SHOT = 0x219d; // SCD41 only +static const uint16_t SCD4X_CMD_START_LOW_POWER_SINGLE_SHOT_RHT_ONLY = 0x2196; static const uint16_t SCD4X_CMD_GET_DATA_READY_STATUS = 0xe4b8; static const uint16_t SCD4X_CMD_READ_MEASUREMENT = 0xec05; static const uint16_t SCD4X_CMD_PERFORM_FORCED_CALIBRATION = 0x362f; static const uint16_t SCD4X_CMD_STOP_MEASUREMENTS = 0x3f86; - +static const uint16_t SCD4X_CMD_FACTORY_RESET = 0x3632; +static const uint16_t SCD4X_CMD_GET_FEATURESET = 0x202f; static const float SCD4X_TEMPERATURE_OFFSET_MULTIPLIER = (1 << 16) / 175.0f; +static const uint16_t SCD41_ID = 0x1408; +static const uint16_t SCD40_ID = 0x440; void SCD4XComponent::setup() { ESP_LOGCONFIG(TAG, "Setting up scd4x..."); - // the sensor needs 1000 ms to enter the idle state this->set_timeout(1000, [this]() { - uint16_t raw_read_status; - if (!this->get_register(SCD4X_CMD_GET_DATA_READY_STATUS, raw_read_status)) { - ESP_LOGE(TAG, "Failed to read data ready status"); + this->status_clear_error(); + if (!this->write_command(SCD4X_CMD_STOP_MEASUREMENTS)) { + ESP_LOGE(TAG, "Failed to stop measurements"); this->mark_failed(); return; } - - uint32_t stop_measurement_delay = 0; - // In order to query the device periodic measurement must be ceased - if (raw_read_status) { - ESP_LOGD(TAG, "Sensor has data available, stopping periodic measurement"); - if (!this->write_command(SCD4X_CMD_STOP_MEASUREMENTS)) { - ESP_LOGE(TAG, "Failed to stop measurements"); - this->mark_failed(); - return; - } - // According to the SCD4x datasheet the sensor will only respond to other commands after waiting 500 ms after - // issuing the stop_periodic_measurement command - stop_measurement_delay = 500; - } - this->set_timeout(stop_measurement_delay, [this]() { + // According to the SCD4x datasheet the sensor will only respond to other commands after waiting 500 ms after + // issuing the stop_periodic_measurement command + this->set_timeout(500, [this]() { uint16_t raw_serial_number[3]; if (!this->get_register(SCD4X_CMD_GET_SERIAL_NUMBER, raw_serial_number, 3, 1)) { ESP_LOGE(TAG, "Failed to read serial number"); @@ -89,15 +82,9 @@ void SCD4XComponent::setup() { return; } - // Finally start sensor measurements - if (!this->write_command(SCD4X_CMD_START_CONTINUOUS_MEASUREMENTS)) { - ESP_LOGE(TAG, "Error starting continuous measurements."); - this->error_code_ = MEASUREMENT_INIT_FAILED; - this->mark_failed(); - return; - } - initialized_ = true; + // Finally start sensor measurements + this->start_measurement_(); ESP_LOGD(TAG, "Sensor initialized"); }); }); @@ -123,12 +110,31 @@ void SCD4XComponent::dump_config() { } } ESP_LOGCONFIG(TAG, " Automatic self calibration: %s", ONOFF(this->enable_asc_)); - if (this->ambient_pressure_compensation_) { - ESP_LOGCONFIG(TAG, " Altitude compensation disabled"); - ESP_LOGCONFIG(TAG, " Ambient pressure compensation: %dmBar", this->ambient_pressure_); + if (this->ambient_pressure_source_ != nullptr) { + ESP_LOGCONFIG(TAG, " Dynamic ambient pressure compensation using sensor '%s'", + this->ambient_pressure_source_->get_name().c_str()); } else { - ESP_LOGCONFIG(TAG, " Ambient pressure compensation disabled"); - ESP_LOGCONFIG(TAG, " Altitude compensation: %dm", this->altitude_compensation_); + if (this->ambient_pressure_compensation_) { + ESP_LOGCONFIG(TAG, " Altitude compensation disabled"); + ESP_LOGCONFIG(TAG, " Ambient pressure compensation: %dmBar", this->ambient_pressure_); + } else { + ESP_LOGCONFIG(TAG, " Ambient pressure compensation disabled"); + ESP_LOGCONFIG(TAG, " Altitude compensation: %dm", this->altitude_compensation_); + } + } + switch (this->measurement_mode_) { + case PERIODIC: + ESP_LOGCONFIG(TAG, " Measurement mode: periodic (5s)"); + break; + case LOW_POWER_PERIODIC: + ESP_LOGCONFIG(TAG, " Measurement mode: low power periodic (30s)"); + break; + case SINGLE_SHOT: + ESP_LOGCONFIG(TAG, " Measurement mode: single shot"); + break; + case SINGLE_SHOT_RHT_ONLY: + ESP_LOGCONFIG(TAG, " Measurement mode: single shot rht only"); + break; } ESP_LOGCONFIG(TAG, " Temperature offset: %.2f °C", this->temperature_offset_); LOG_UPDATE_INTERVAL(this); @@ -149,47 +155,105 @@ void SCD4XComponent::update() { } } - // Check if data is ready - if (!this->write_command(SCD4X_CMD_GET_DATA_READY_STATUS)) { - this->status_set_warning(); - return; + uint32_t wait_time = 0; + if (this->measurement_mode_ == SINGLE_SHOT || this->measurement_mode_ == SINGLE_SHOT_RHT_ONLY) { + start_measurement_(); + wait_time = + this->measurement_mode_ == SINGLE_SHOT ? 5000 : 50; // Single shot measurement takes 5 secs rht mode 50 ms } + this->set_timeout(wait_time, [this]() { + // Check if data is ready + if (!this->write_command(SCD4X_CMD_GET_DATA_READY_STATUS)) { + this->status_set_warning(); + return; + } - uint16_t raw_read_status; - if (!this->read_data(raw_read_status) || raw_read_status == 0x00) { - this->status_set_warning(); - ESP_LOGW(TAG, "Data not ready yet!"); - return; - } + uint16_t raw_read_status; - if (!this->write_command(SCD4X_CMD_READ_MEASUREMENT)) { - ESP_LOGW(TAG, "Error reading measurement!"); - this->status_set_warning(); - return; - } + if (!this->read_data(raw_read_status) || raw_read_status == 0x00) { + this->status_set_warning(); + ESP_LOGW(TAG, "Data not ready yet!"); + return; + } - // Read off sensor data - uint16_t raw_data[3]; - if (!this->read_data(raw_data, 3)) { - this->status_set_warning(); - return; - } + if (!this->write_command(SCD4X_CMD_READ_MEASUREMENT)) { + ESP_LOGW(TAG, "Error reading measurement!"); + this->status_set_warning(); + return; // NO RETRY + } + // Read off sensor data + uint16_t raw_data[3]; + if (!this->read_data(raw_data, 3)) { + this->status_set_warning(); + return; + } + if (this->co2_sensor_ != nullptr) + this->co2_sensor_->publish_state(raw_data[0]); - if (this->co2_sensor_ != nullptr) - this->co2_sensor_->publish_state(raw_data[0]); - - if (this->temperature_sensor_ != nullptr) { - const float temperature = -45.0f + (175.0f * (raw_data[1])) / (1 << 16); - this->temperature_sensor_->publish_state(temperature); - } - - if (this->humidity_sensor_ != nullptr) { - const float humidity = (100.0f * raw_data[2]) / (1 << 16); - this->humidity_sensor_->publish_state(humidity); - } - - this->status_clear_warning(); + if (this->temperature_sensor_ != nullptr) { + const float temperature = -45.0f + (175.0f * (raw_data[1])) / (1 << 16); + this->temperature_sensor_->publish_state(temperature); + } + if (this->humidity_sensor_ != nullptr) { + const float humidity = (100.0f * raw_data[2]) / (1 << 16); + this->humidity_sensor_->publish_state(humidity); + } + this->status_clear_warning(); + }); // set_timeout } + +bool SCD4XComponent::perform_forced_calibration(uint16_t current_co2_concentration) { + /* + Operate the SCD4x in the operation mode later used in normal sensor operation (periodic measurement, low power + periodic measurement or single shot) for > 3 minutes in an environment with homogenous and constant CO2 + concentration before performing a forced recalibration. + */ + if (!this->write_command(SCD4X_CMD_STOP_MEASUREMENTS)) { + ESP_LOGE(TAG, "Failed to stop measurements"); + this->status_set_warning(); + } + this->set_timeout(500, [this, current_co2_concentration]() { + if (this->write_command(SCD4X_CMD_PERFORM_FORCED_CALIBRATION, current_co2_concentration)) { + ESP_LOGD(TAG, "setting forced calibration Co2 level %d ppm", current_co2_concentration); + // frc takes 400 ms + // because this method will be used very rarly + // the simple aproach with delay is ok + delay(400); // NOLINT' + if (!this->start_measurement_()) { + return false; + } else { + ESP_LOGD(TAG, "forced calibration complete"); + } + return true; + } else { + ESP_LOGE(TAG, "force calibration failed"); + this->error_code_ = FRC_FAILED; + this->status_set_warning(); + return false; + } + }); + return true; +} + +bool SCD4XComponent::factory_reset() { + if (!this->write_command(SCD4X_CMD_STOP_MEASUREMENTS)) { + ESP_LOGE(TAG, "Failed to stop measurements"); + this->status_set_warning(); + return false; + } + + this->set_timeout(500, [this]() { + if (!this->write_command(SCD4X_CMD_FACTORY_RESET)) { + ESP_LOGE(TAG, "Failed to send factory reset command"); + this->status_set_warning(); + return false; + } + ESP_LOGD(TAG, "Factory reset complete"); + return true; + }); + return true; +} + // Note pressure in bar here. Convert to hPa void SCD4XComponent::set_ambient_pressure_compensation(float pressure_in_bar) { ambient_pressure_compensation_ = true; @@ -213,5 +277,38 @@ bool SCD4XComponent::update_ambient_pressure_compensation_(uint16_t pressure_in_ } } +bool SCD4XComponent::start_measurement_() { + uint16_t measurement_command = SCD4X_CMD_START_CONTINUOUS_MEASUREMENTS; + switch (this->measurement_mode_) { + case PERIODIC: + measurement_command = SCD4X_CMD_START_CONTINUOUS_MEASUREMENTS; + break; + case LOW_POWER_PERIODIC: + measurement_command = SCD4X_CMD_START_LOW_POWER_CONTINUOUS_MEASUREMENTS; + break; + case SINGLE_SHOT: + measurement_command = SCD4X_CMD_START_LOW_POWER_SINGLE_SHOT; + break; + case SINGLE_SHOT_RHT_ONLY: + measurement_command = SCD4X_CMD_START_LOW_POWER_SINGLE_SHOT_RHT_ONLY; + break; + } + + static uint8_t remaining_retries = 3; + while (remaining_retries) { + if (!this->write_command(measurement_command)) { + ESP_LOGE(TAG, "Error starting measurements."); + this->error_code_ = MEASUREMENT_INIT_FAILED; + this->status_set_warning(); + if (--remaining_retries == 0) + return false; + delay(50); // NOLINT wait 50 ms and try again + } + this->status_clear_warning(); + return true; + } + return false; +} + } // namespace scd4x } // namespace esphome diff --git a/esphome/components/scd4x/scd4x.h b/esphome/components/scd4x/scd4x.h index 3e534bcf98..23c3766e60 100644 --- a/esphome/components/scd4x/scd4x.h +++ b/esphome/components/scd4x/scd4x.h @@ -1,5 +1,6 @@ #pragma once - +#include +#include "esphome/core/application.h" #include "esphome/core/component.h" #include "esphome/components/sensor/sensor.h" #include "esphome/components/sensirion_common/i2c_sensirion.h" @@ -7,7 +8,14 @@ namespace esphome { namespace scd4x { -enum ERRORCODE { COMMUNICATION_FAILED, SERIAL_NUMBER_IDENTIFICATION_FAILED, MEASUREMENT_INIT_FAILED, UNKNOWN }; +enum ERRORCODE { + COMMUNICATION_FAILED, + SERIAL_NUMBER_IDENTIFICATION_FAILED, + MEASUREMENT_INIT_FAILED, + FRC_FAILED, + UNKNOWN +}; +enum MeasurementMode { PERIODIC, LOW_POWER_PERIODIC, SINGLE_SHOT, SINGLE_SHOT_RHT_ONLY }; class SCD4XComponent : public PollingComponent, public sensirion_common::SensirionI2CDevice { public: @@ -25,10 +33,13 @@ class SCD4XComponent : public PollingComponent, public sensirion_common::Sensiri void set_co2_sensor(sensor::Sensor *co2) { co2_sensor_ = co2; } void set_temperature_sensor(sensor::Sensor *temperature) { temperature_sensor_ = temperature; }; void set_humidity_sensor(sensor::Sensor *humidity) { humidity_sensor_ = humidity; } + void set_measurement_mode(MeasurementMode mode) { measurement_mode_ = mode; } + bool perform_forced_calibration(uint16_t current_co2_concentration); + bool factory_reset(); protected: bool update_ambient_pressure_compensation_(uint16_t pressure_in_hpa); - + bool start_measurement_(); ERRORCODE error_code_; bool initialized_{false}; @@ -38,7 +49,7 @@ class SCD4XComponent : public PollingComponent, public sensirion_common::Sensiri bool ambient_pressure_compensation_; uint16_t ambient_pressure_; bool enable_asc_; - + MeasurementMode measurement_mode_{PERIODIC}; sensor::Sensor *co2_sensor_{nullptr}; sensor::Sensor *temperature_sensor_{nullptr}; sensor::Sensor *humidity_sensor_{nullptr}; diff --git a/esphome/components/scd4x/sensor.py b/esphome/components/scd4x/sensor.py index 6ab0e1ba99..4c94d4257f 100644 --- a/esphome/components/scd4x/sensor.py +++ b/esphome/components/scd4x/sensor.py @@ -2,11 +2,15 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import i2c, sensor from esphome.components import sensirion_common +from esphome import automation +from esphome.automation import maybe_simple_id + from esphome.const import ( CONF_ID, CONF_CO2, CONF_HUMIDITY, CONF_TEMPERATURE, + CONF_VALUE, DEVICE_CLASS_CARBON_DIOXIDE, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, @@ -19,7 +23,7 @@ from esphome.const import ( UNIT_PERCENT, ) -CODEOWNERS = ["@sjtrny"] +CODEOWNERS = ["@sjtrny", "@martgras"] DEPENDENCIES = ["i2c"] AUTO_LOAD = ["sensirion_common"] @@ -27,12 +31,29 @@ scd4x_ns = cg.esphome_ns.namespace("scd4x") SCD4XComponent = scd4x_ns.class_( "SCD4XComponent", cg.PollingComponent, sensirion_common.SensirionI2CDevice ) +MeasurementMode = scd4x_ns.enum("MEASUREMENT_MODE") +MEASUREMENT_MODE_OPTIONS = { + "periodic": MeasurementMode.PERIODIC, + "low_power_periodic": MeasurementMode.LOW_POWER_PERIODIC, + "single_shot": MeasurementMode.SINGLE_SHOT, + "single_shot_rht_only": MeasurementMode.SINGLE_SHOT_RHT_ONLY, +} + + +# Actions +PerformForcedCalibrationAction = scd4x_ns.class_( + "PerformForcedCalibrationAction", automation.Action +) +FactoryResetAction = scd4x_ns.class_("FactoryResetAction", automation.Action) + -CONF_AUTOMATIC_SELF_CALIBRATION = "automatic_self_calibration" CONF_ALTITUDE_COMPENSATION = "altitude_compensation" CONF_AMBIENT_PRESSURE_COMPENSATION = "ambient_pressure_compensation" -CONF_TEMPERATURE_OFFSET = "temperature_offset" CONF_AMBIENT_PRESSURE_COMPENSATION_SOURCE = "ambient_pressure_compensation_source" +CONF_AUTOMATIC_SELF_CALIBRATION = "automatic_self_calibration" +CONF_MEASUREMENT_MODE = "measurement_mode" +CONF_TEMPERATURE_OFFSET = "temperature_offset" + CONFIG_SCHEMA = ( cv.Schema( @@ -69,6 +90,9 @@ CONFIG_SCHEMA = ( cv.Optional(CONF_AMBIENT_PRESSURE_COMPENSATION_SOURCE): cv.use_id( sensor.Sensor ), + cv.Optional(CONF_MEASUREMENT_MODE, default="periodic"): cv.enum( + MEASUREMENT_MODE_OPTIONS, lower=True + ), } ) .extend(cv.polling_component_schema("60s")) @@ -106,3 +130,42 @@ async def to_code(config): if CONF_AMBIENT_PRESSURE_COMPENSATION_SOURCE in config: sens = await cg.get_variable(config[CONF_AMBIENT_PRESSURE_COMPENSATION_SOURCE]) cg.add(var.set_ambient_pressure_source(sens)) + + cg.add(var.set_measurement_mode(config[CONF_MEASUREMENT_MODE])) + + +SCD4X_ACTION_SCHEMA = maybe_simple_id( + { + cv.GenerateID(): cv.use_id(SCD4XComponent), + cv.Required(CONF_VALUE): cv.templatable(cv.positive_int), + } +) + + +@automation.register_action( + "scd4x.perform_forced_calibration", + PerformForcedCalibrationAction, + SCD4X_ACTION_SCHEMA, +) +async def scd4x_frc_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + template_ = await cg.templatable(config[CONF_VALUE], args, cg.uint16) + cg.add(var.set_value(template_)) + return var + + +SCD4X_RESET_ACTION_SCHEMA = maybe_simple_id( + { + cv.Required(CONF_ID): cv.use_id(SCD4XComponent), + } +) + + +@automation.register_action( + "scd4x.factory_reset", FactoryResetAction, SCD4X_RESET_ACTION_SCHEMA +) +async def scd4x_reset_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var diff --git a/tests/test1.yaml b/tests/test1.yaml index deaf1c237e..e15b2cf6b7 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -890,6 +890,7 @@ sensor: temperature_offset: 4.2C i2c_id: i2c_bus - platform: scd4x + id: scd40 co2: name: "SCD4X CO2" temperature: @@ -2822,3 +2823,14 @@ lock: - platform: copy source_id: test_lock2 name: Generic Output Lock Copy + +button: + - platform: template + name: "Start calibration" + on_press: + - scd4x.perform_forced_calibration: + value: 419 + id: scd40 + - scd4x.factory_reset: + id: scd40 + From 98c733108e430b7828dc7f884942ab063f5e616f Mon Sep 17 00:00:00 2001 From: Matthew Garrett Date: Tue, 10 May 2022 02:35:43 -0700 Subject: [PATCH 44/56] PMSX003: Add support for specifying the update interval and spinning down (#3053) Co-authored-by: Otto Winter --- esphome/components/pmsx003/pmsx003.cpp | 68 ++++++++++++++++++++++++++ esphome/components/pmsx003/pmsx003.h | 21 ++++++++ esphome/components/pmsx003/sensor.py | 28 +++++++++++ tests/test3.yaml | 11 +++-- 4 files changed, 125 insertions(+), 3 deletions(-) diff --git a/esphome/components/pmsx003/pmsx003.cpp b/esphome/components/pmsx003/pmsx003.cpp index 5de94699f0..43f2e12f55 100644 --- a/esphome/components/pmsx003/pmsx003.cpp +++ b/esphome/components/pmsx003/pmsx003.cpp @@ -49,6 +49,47 @@ void PMSX003Component::set_formaldehyde_sensor(sensor::Sensor *formaldehyde_sens void PMSX003Component::loop() { const uint32_t now = millis(); + + // If we update less often than it takes the device to stabilise, spin the fan down + // rather than running it constantly. It does take some time to stabilise, so we + // need to keep track of what state we're in. + if (this->update_interval_ > PMS_STABILISING_MS) { + if (this->initialised_ == 0) { + this->send_command_(PMS_CMD_AUTO_MANUAL, 0); + this->send_command_(PMS_CMD_ON_STANDBY, 1); + this->initialised_ = 1; + } + switch (this->state_) { + case PMSX003_STATE_IDLE: + // Power on the sensor now so it'll be ready when we hit the update time + if (now - this->last_update_ < (this->update_interval_ - PMS_STABILISING_MS)) + return; + + this->state_ = PMSX003_STATE_STABILISING; + this->send_command_(PMS_CMD_ON_STANDBY, 1); + this->fan_on_time_ = now; + return; + case PMSX003_STATE_STABILISING: + // wait for the sensor to be stable + if (now - this->fan_on_time_ < PMS_STABILISING_MS) + return; + // consume any command responses that are in the serial buffer + while (this->available()) + this->read_byte(&this->data_[0]); + // Trigger a new read + this->send_command_(PMS_CMD_TRIG_MANUAL, 0); + this->state_ = PMSX003_STATE_WAITING; + break; + case PMSX003_STATE_WAITING: + // Just go ahead and read stuff + break; + } + } else if (now - this->last_update_ < this->update_interval_) { + // Otherwise just leave the sensor powered up and come back when we hit the update + // time + return; + } + if (now - this->last_transmission_ >= 500) { // last transmission too long ago. Reset RX index. this->data_index_ = 0; @@ -65,6 +106,7 @@ void PMSX003Component::loop() { // finished this->parse_data_(); this->data_index_ = 0; + this->last_update_ = now; } else if (!*check) { // wrong data this->data_index_ = 0; @@ -131,6 +173,25 @@ optional PMSX003Component::check_byte_() { return {}; } +void PMSX003Component::send_command_(uint8_t cmd, uint16_t data) { + this->data_index_ = 0; + this->data_[data_index_++] = 0x42; + this->data_[data_index_++] = 0x4D; + this->data_[data_index_++] = cmd; + this->data_[data_index_++] = (data >> 8) & 0xFF; + this->data_[data_index_++] = (data >> 0) & 0xFF; + int sum = 0; + for (int i = 0; i < data_index_; i++) { + sum += this->data_[i]; + } + this->data_[data_index_++] = (sum >> 8) & 0xFF; + this->data_[data_index_++] = (sum >> 0) & 0xFF; + for (int i = 0; i < data_index_; i++) { + this->write_byte(this->data_[i]); + } + this->data_index_ = 0; +} + void PMSX003Component::parse_data_() { switch (this->type_) { case PMSX003_TYPE_5003ST: { @@ -218,6 +279,13 @@ void PMSX003Component::parse_data_() { } } + // Spin down the sensor again if we aren't going to need it until more time has + // passed than it takes to stabilise + if (this->update_interval_ > PMS_STABILISING_MS) { + this->send_command_(PMS_CMD_ON_STANDBY, 0); + this->state_ = PMSX003_STATE_IDLE; + } + this->status_clear_warning(); } uint16_t PMSX003Component::get_16_bit_uint_(uint8_t start_index) { diff --git a/esphome/components/pmsx003/pmsx003.h b/esphome/components/pmsx003/pmsx003.h index fd6364c70c..eb33f66909 100644 --- a/esphome/components/pmsx003/pmsx003.h +++ b/esphome/components/pmsx003/pmsx003.h @@ -7,6 +7,13 @@ namespace esphome { namespace pmsx003 { +// known command bytes +#define PMS_CMD_AUTO_MANUAL 0xE1 // data=0: perform measurement manually, data=1: perform measurement automatically +#define PMS_CMD_TRIG_MANUAL 0xE2 // trigger a manual measurement +#define PMS_CMD_ON_STANDBY 0xE4 // data=0: go to standby mode, data=1: go to normal mode + +static const uint16_t PMS_STABILISING_MS = 30000; // time taken for the sensor to become stable after power on + enum PMSX003Type { PMSX003_TYPE_X003 = 0, PMSX003_TYPE_5003T, @@ -14,6 +21,12 @@ enum PMSX003Type { PMSX003_TYPE_5003S, }; +enum PMSX003State { + PMSX003_STATE_IDLE = 0, + PMSX003_STATE_STABILISING, + PMSX003_STATE_WAITING, +}; + class PMSX003Component : public uart::UARTDevice, public Component { public: PMSX003Component() = default; @@ -23,6 +36,8 @@ class PMSX003Component : public uart::UARTDevice, public Component { void set_type(PMSX003Type type) { type_ = type; } + void set_update_interval(uint32_t val) { update_interval_ = val; }; + void set_pm_1_0_std_sensor(sensor::Sensor *pm_1_0_std_sensor); void set_pm_2_5_std_sensor(sensor::Sensor *pm_2_5_std_sensor); void set_pm_10_0_std_sensor(sensor::Sensor *pm_10_0_std_sensor); @@ -45,11 +60,17 @@ class PMSX003Component : public uart::UARTDevice, public Component { protected: optional check_byte_(); void parse_data_(); + void send_command_(uint8_t cmd, uint16_t data); uint16_t get_16_bit_uint_(uint8_t start_index); uint8_t data_[64]; uint8_t data_index_{0}; + uint8_t initialised_{0}; + uint32_t fan_on_time_{0}; + uint32_t last_update_{0}; uint32_t last_transmission_{0}; + uint32_t update_interval_{0}; + PMSX003State state_{PMSX003_STATE_IDLE}; PMSX003Type type_; // "Standard Particle" diff --git a/esphome/components/pmsx003/sensor.py b/esphome/components/pmsx003/sensor.py index b731e48e31..f3552f4081 100644 --- a/esphome/components/pmsx003/sensor.py +++ b/esphome/components/pmsx003/sensor.py @@ -1,6 +1,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import sensor, uart + from esphome.const import ( CONF_FORMALDEHYDE, CONF_HUMIDITY, @@ -17,6 +18,7 @@ from esphome.const import ( CONF_PM_2_5UM, CONF_PM_5_0UM, CONF_PM_10_0UM, + CONF_UPDATE_INTERVAL, CONF_TEMPERATURE, CONF_TYPE, DEVICE_CLASS_PM1, @@ -44,6 +46,7 @@ TYPE_PMS5003ST = "PMS5003ST" TYPE_PMS5003S = "PMS5003S" PMSX003Type = pmsx003_ns.enum("PMSX003Type") + PMSX003_TYPES = { TYPE_PMSX003: PMSX003Type.PMSX003_TYPE_X003, TYPE_PMS5003T: PMSX003Type.PMSX003_TYPE_5003T, @@ -68,6 +71,17 @@ def validate_pmsx003_sensors(value): return value +def validate_update_interval(value): + value = cv.positive_time_period_milliseconds(value) + if value == cv.time_period("0s"): + return value + if value < cv.time_period("30s"): + raise cv.Invalid( + "Update interval must be greater than or equal to 30 seconds if set." + ) + return value + + CONFIG_SCHEMA = ( cv.Schema( { @@ -157,6 +171,7 @@ CONFIG_SCHEMA = ( accuracy_decimals=0, state_class=STATE_CLASS_MEASUREMENT, ), + cv.Optional(CONF_UPDATE_INTERVAL, default="0s"): validate_update_interval, } ) .extend(cv.COMPONENT_SCHEMA) @@ -164,6 +179,17 @@ CONFIG_SCHEMA = ( ) +def final_validate(config): + require_tx = config[CONF_UPDATE_INTERVAL] > cv.time_period("0s") + schema = uart.final_validate_device_schema( + "pmsx003", baud_rate=9600, require_rx=True, require_tx=require_tx + ) + schema(config) + + +FINAL_VALIDATE_SCHEMA = final_validate + + async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) @@ -230,3 +256,5 @@ async def to_code(config): if CONF_FORMALDEHYDE in config: sens = await sensor.new_sensor(config[CONF_FORMALDEHYDE]) cg.add(var.set_formaldehyde_sensor(sens)) + + cg.add(var.set_update_interval(config[CONF_UPDATE_INTERVAL])) diff --git a/tests/test3.yaml b/tests/test3.yaml index e3818d87ec..ec768e17e5 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -266,6 +266,10 @@ uart: stop_bits: 2 # Specifically added for testing debug with no options at all. debug: + - id: uart8 + tx_pin: GPIO4 + rx_pin: GPIO5 + baud_rate: 9600 modbus: uart_id: uart1 @@ -559,7 +563,7 @@ sensor: name: 'AQI' calculation_type: 'AQI' - platform: pmsx003 - uart_id: uart2 + uart_id: uart8 type: PMSX003 pm_1_0: name: 'PM 1.0 Concentration' @@ -585,8 +589,9 @@ sensor: name: 'Particulate Count >5.0um' pm_10_0um: name: 'Particulate Count >10.0um' + update_interval: 30s - platform: pmsx003 - uart_id: uart2 + uart_id: uart5 type: PMS5003T pm_2_5: name: 'PM 2.5 Concentration' @@ -595,7 +600,7 @@ sensor: humidity: name: 'PMS Humidity' - platform: pmsx003 - uart_id: uart2 + uart_id: uart6 type: PMS5003ST pm_1_0: name: 'PM 1.0 Concentration' From 5fac67ce15dbbd8c9fc1da89264a71fcfe87258c Mon Sep 17 00:00:00 2001 From: Felix Storm Date: Tue, 10 May 2022 11:39:18 +0200 Subject: [PATCH 45/56] CAN bus: on_frame remote_transmission_request (#3376) --- esphome/components/canbus/__init__.py | 13 ++++++++++++- esphome/components/canbus/canbus.cpp | 6 ++++-- esphome/components/canbus/canbus.h | 8 +++++++- 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/esphome/components/canbus/__init__.py b/esphome/components/canbus/__init__.py index 20f2642144..1dbd743c75 100644 --- a/esphome/components/canbus/__init__.py +++ b/esphome/components/canbus/__init__.py @@ -78,6 +78,7 @@ CANBUS_SCHEMA = cv.Schema( min=0, max=0x1FFFFFFF ), cv.Optional(CONF_USE_EXTENDED_ID, default=False): cv.boolean, + cv.Optional(CONF_REMOTE_TRANSMISSION_REQUEST): cv.boolean, }, validate_id, ), @@ -100,10 +101,20 @@ async def setup_canbus_core_(var, config): trigger = cg.new_Pvariable( conf[CONF_TRIGGER_ID], var, can_id, can_id_mask, ext_id ) + if CONF_REMOTE_TRANSMISSION_REQUEST in conf: + cg.add( + trigger.set_remote_transmission_request( + conf[CONF_REMOTE_TRANSMISSION_REQUEST] + ) + ) await cg.register_component(trigger, conf) await automation.build_automation( trigger, - [(cg.std_vector.template(cg.uint8), "x"), (cg.uint32, "can_id")], + [ + (cg.std_vector.template(cg.uint8), "x"), + (cg.uint32, "can_id"), + (cg.bool_, "remote_transmission_request"), + ], conf, ) diff --git a/esphome/components/canbus/canbus.cpp b/esphome/components/canbus/canbus.cpp index 5d9084706b..3fe0d50f06 100644 --- a/esphome/components/canbus/canbus.cpp +++ b/esphome/components/canbus/canbus.cpp @@ -81,8 +81,10 @@ void Canbus::loop() { // fire all triggers for (auto *trigger : this->triggers_) { if ((trigger->can_id_ == (can_message.can_id & trigger->can_id_mask_)) && - (trigger->use_extended_id_ == can_message.use_extended_id)) { - trigger->trigger(data, can_message.can_id); + (trigger->use_extended_id_ == can_message.use_extended_id) && + (!trigger->remote_transmission_request_.has_value() || + trigger->remote_transmission_request_.value() == can_message.remote_transmission_request)) { + trigger->trigger(data, can_message.can_id, can_message.remote_transmission_request); } } } diff --git a/esphome/components/canbus/canbus.h b/esphome/components/canbus/canbus.h index 20c490c083..06b15c0db5 100644 --- a/esphome/components/canbus/canbus.h +++ b/esphome/components/canbus/canbus.h @@ -126,13 +126,18 @@ template class CanbusSendAction : public Action, public P std::vector data_static_{}; }; -class CanbusTrigger : public Trigger, uint32_t>, public Component { +class CanbusTrigger : public Trigger, uint32_t, bool>, public Component { friend class Canbus; public: explicit CanbusTrigger(Canbus *parent, const std::uint32_t can_id, const std::uint32_t can_id_mask, const bool use_extended_id) : parent_(parent), can_id_(can_id), can_id_mask_(can_id_mask), use_extended_id_(use_extended_id){}; + + void set_remote_transmission_request(bool remote_transmission_request) { + this->remote_transmission_request_ = remote_transmission_request; + } + void setup() override { this->parent_->add_trigger(this); } protected: @@ -140,6 +145,7 @@ class CanbusTrigger : public Trigger, uint32_t>, public Com uint32_t can_id_; uint32_t can_id_mask_; bool use_extended_id_; + optional remote_transmission_request_{}; }; } // namespace canbus From 7cba0c6fb0a95573f21e192921f130933b79162a Mon Sep 17 00:00:00 2001 From: Dennis <51165557+dennisvbussel@users.noreply.github.com> Date: Tue, 10 May 2022 11:42:31 +0200 Subject: [PATCH 46/56] =?UTF-8?q?Fix=20cover=20set=20position=20by=20force?= =?UTF-8?q?=20pushing=20position=5Fid=20datapoint=20(simila=E2=80=A6=20(#3?= =?UTF-8?q?435)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- esphome/components/tuya/cover/tuya_cover.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/tuya/cover/tuya_cover.cpp b/esphome/components/tuya/cover/tuya_cover.cpp index b63eb9109d..b55873c3c1 100644 --- a/esphome/components/tuya/cover/tuya_cover.cpp +++ b/esphome/components/tuya/cover/tuya_cover.cpp @@ -66,7 +66,7 @@ void TuyaCover::control(const cover::CoverCall &call) { auto position_int = static_cast(pos * this->value_range_); position_int = position_int + this->min_value_; - parent_->set_integer_datapoint_value(*this->position_id_, position_int); + parent_->force_set_integer_datapoint_value(*this->position_id_, position_int); } } if (call.get_position().has_value()) { @@ -82,7 +82,7 @@ void TuyaCover::control(const cover::CoverCall &call) { auto position_int = static_cast(pos * this->value_range_); position_int = position_int + this->min_value_; - parent_->set_integer_datapoint_value(*this->position_id_, position_int); + parent_->force_set_integer_datapoint_value(*this->position_id_, position_int); } } From 69118120d9145f24a40c70675de8d5dfda5483a8 Mon Sep 17 00:00:00 2001 From: LuBeDa Date: Tue, 10 May 2022 11:56:29 +0200 Subject: [PATCH 47/56] added prev_frame for animation (#3427) --- esphome/components/display/display_buffer.cpp | 6 ++++++ esphome/components/display/display_buffer.h | 1 + 2 files changed, 7 insertions(+) diff --git a/esphome/components/display/display_buffer.cpp b/esphome/components/display/display_buffer.cpp index d00fdd5240..ca866a43d2 100644 --- a/esphome/components/display/display_buffer.cpp +++ b/esphome/components/display/display_buffer.cpp @@ -584,6 +584,12 @@ void Animation::next_frame() { this->current_frame_ = 0; } } +void Animation::prev_frame() { + this->current_frame_--; + if (this->current_frame_ < 0) { + this->current_frame_ = this->animation_frame_count_ - 1; + } +} DisplayPage::DisplayPage(display_writer_t writer) : writer_(std::move(writer)) {} void DisplayPage::show() { this->parent_->show_page(this); } diff --git a/esphome/components/display/display_buffer.h b/esphome/components/display/display_buffer.h index 86221c5f96..33f55aa3d1 100644 --- a/esphome/components/display/display_buffer.h +++ b/esphome/components/display/display_buffer.h @@ -478,6 +478,7 @@ class Animation : public Image { int get_animation_frame_count() const; int get_current_frame() const; void next_frame(); + void prev_frame(); protected: int current_frame_; From b7e52812f8d713827c25299a76bdb4c7195a72ca Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 10 May 2022 22:02:58 +1200 Subject: [PATCH 48/56] Fix tests (#3455) --- tests/test3.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test3.yaml b/tests/test3.yaml index ec768e17e5..af0e784465 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -266,7 +266,7 @@ uart: stop_bits: 2 # Specifically added for testing debug with no options at all. debug: - - id: uart8 + - id: uart9 tx_pin: GPIO4 rx_pin: GPIO5 baud_rate: 9600 @@ -563,7 +563,7 @@ sensor: name: 'AQI' calculation_type: 'AQI' - platform: pmsx003 - uart_id: uart8 + uart_id: uart9 type: PMSX003 pm_1_0: name: 'PM 1.0 Concentration' From 4822abde860670ada7733338548be4ccf6e4c4f2 Mon Sep 17 00:00:00 2001 From: Massimo Cetra Date: Tue, 10 May 2022 12:03:40 +0200 Subject: [PATCH 49/56] Fix BLE280 setup when the sensor is marked as failed. (#3396) --- esphome/components/bme280/bme280.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/esphome/components/bme280/bme280.cpp b/esphome/components/bme280/bme280.cpp index fcb293afa0..a4ea8d608e 100644 --- a/esphome/components/bme280/bme280.cpp +++ b/esphome/components/bme280/bme280.cpp @@ -81,6 +81,11 @@ static const char *iir_filter_to_str(BME280IIRFilter filter) { void BME280Component::setup() { ESP_LOGCONFIG(TAG, "Setting up BME280..."); uint8_t chip_id = 0; + + // Mark as not failed before initializing. Some devices will turn off sensors to save on batteries + // and when they come back on, the COMPONENT_STATE_FAILED bit must be unset on the component. + this->component_state_ &= ~COMPONENT_STATE_FAILED; + if (!this->read_byte(BME280_REGISTER_CHIPID, &chip_id)) { this->error_code_ = COMMUNICATION_FAILED; this->mark_failed(); From e541ae400c3a5d43c2e3d621ac4c7c99799ecb36 Mon Sep 17 00:00:00 2001 From: MFlasskamp Date: Tue, 10 May 2022 12:03:59 +0200 Subject: [PATCH 50/56] Esp32c3 deepsleep fix (#3454) --- .../components/deep_sleep/deep_sleep_component.cpp | 14 ++++++++++++-- .../components/deep_sleep/deep_sleep_component.h | 6 ++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/esphome/components/deep_sleep/deep_sleep_component.cpp b/esphome/components/deep_sleep/deep_sleep_component.cpp index 23f2a7a70c..1e31cef33e 100644 --- a/esphome/components/deep_sleep/deep_sleep_component.cpp +++ b/esphome/components/deep_sleep/deep_sleep_component.cpp @@ -21,6 +21,7 @@ optional DeepSleepComponent::get_run_duration_() const { switch (wakeup_cause) { case ESP_SLEEP_WAKEUP_EXT0: case ESP_SLEEP_WAKEUP_EXT1: + case ESP_SLEEP_WAKEUP_GPIO: return this->wakeup_cause_to_run_duration_->gpio_cause; case ESP_SLEEP_WAKEUP_TOUCHPAD: return this->wakeup_cause_to_run_duration_->touch_cause; @@ -72,18 +73,27 @@ float DeepSleepComponent::get_loop_priority() const { return -100.0f; // run after everything else is ready } void DeepSleepComponent::set_sleep_duration(uint32_t time_ms) { this->sleep_duration_ = uint64_t(time_ms) * 1000; } -#ifdef USE_ESP32 +#if defined(USE_ESP32) void DeepSleepComponent::set_wakeup_pin_mode(WakeupPinMode wakeup_pin_mode) { this->wakeup_pin_mode_ = wakeup_pin_mode; } +#endif + +#if defined(USE_ESP32) #if !defined(USE_ESP32_VARIANT_ESP32C3) + void DeepSleepComponent::set_ext1_wakeup(Ext1Wakeup ext1_wakeup) { this->ext1_wakeup_ = ext1_wakeup; } + void DeepSleepComponent::set_touch_wakeup(bool touch_wakeup) { this->touch_wakeup_ = touch_wakeup; } + +#endif + void DeepSleepComponent::set_run_duration(WakeupCauseToRunDuration wakeup_cause_to_run_duration) { wakeup_cause_to_run_duration_ = wakeup_cause_to_run_duration; } + #endif -#endif + void DeepSleepComponent::set_run_duration(uint32_t time_ms) { this->run_duration_ = time_ms; } void DeepSleepComponent::begin_sleep(bool manual) { if (this->prevent_ && !manual) { diff --git a/esphome/components/deep_sleep/deep_sleep_component.h b/esphome/components/deep_sleep/deep_sleep_component.h index 5e90d4b89d..1ff681693f 100644 --- a/esphome/components/deep_sleep/deep_sleep_component.h +++ b/esphome/components/deep_sleep/deep_sleep_component.h @@ -70,17 +70,19 @@ class DeepSleepComponent : public Component { void set_wakeup_pin_mode(WakeupPinMode wakeup_pin_mode); #endif -#if defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C3) +#if defined(USE_ESP32) +#if !defined(USE_ESP32_VARIANT_ESP32C3) void set_ext1_wakeup(Ext1Wakeup ext1_wakeup); void set_touch_wakeup(bool touch_wakeup); +#endif // Set the duration in ms for how long the code should run before entering // deep sleep mode, according to the cause the ESP32 has woken. void set_run_duration(WakeupCauseToRunDuration wakeup_cause_to_run_duration); - #endif + /// Set a duration in ms for how long the code should run before entering deep sleep mode. void set_run_duration(uint32_t time_ms); From 235a97ea1014e13454db4693e57db64ae153c4bc Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Tue, 10 May 2022 21:54:00 +0200 Subject: [PATCH 51/56] Make retry scheduler efficient (#3225) --- esphome/core/component.h | 2 +- esphome/core/scheduler.cpp | 71 ++++++++++++++++++++------------------ esphome/core/scheduler.h | 19 +++------- 3 files changed, 43 insertions(+), 49 deletions(-) diff --git a/esphome/core/component.h b/esphome/core/component.h index c3a4ac3782..e394736653 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -61,7 +61,7 @@ extern const uint32_t STATUS_LED_OK; extern const uint32_t STATUS_LED_WARNING; extern const uint32_t STATUS_LED_ERROR; -enum RetryResult { DONE, RETRY }; +enum class RetryResult { DONE, RETRY }; class Component { public: diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 56f823556b..cc4074b94d 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -14,7 +14,7 @@ static const uint32_t MAX_LOGICALLY_DELETED_ITEMS = 10; // #define ESPHOME_DEBUG_SCHEDULER void HOT Scheduler::set_timeout(Component *component, const std::string &name, uint32_t timeout, - std::function &&func) { + std::function func) { const uint32_t now = this->millis_(); if (!name.empty()) @@ -32,7 +32,7 @@ void HOT Scheduler::set_timeout(Component *component, const std::string &name, u item->timeout = timeout; item->last_execution = now; item->last_execution_major = this->millis_major_; - item->void_callback = std::move(func); + item->callback = std::move(func); item->remove = false; this->push_(std::move(item)); } @@ -40,7 +40,7 @@ bool HOT Scheduler::cancel_timeout(Component *component, const std::string &name return this->cancel_item_(component, name, SchedulerItem::TIMEOUT); } void HOT Scheduler::set_interval(Component *component, const std::string &name, uint32_t interval, - std::function &&func) { + std::function func) { const uint32_t now = this->millis_(); if (!name.empty()) @@ -65,7 +65,7 @@ void HOT Scheduler::set_interval(Component *component, const std::string &name, item->last_execution_major = this->millis_major_; if (item->last_execution > now) item->last_execution_major--; - item->void_callback = std::move(func); + item->callback = std::move(func); item->remove = false; this->push_(std::move(item)); } @@ -73,37 +73,48 @@ bool HOT Scheduler::cancel_interval(Component *component, const std::string &nam return this->cancel_item_(component, name, SchedulerItem::INTERVAL); } -void HOT Scheduler::set_retry(Component *component, const std::string &name, uint32_t initial_wait_time, - uint8_t max_attempts, std::function &&func, - float backoff_increase_factor) { - const uint32_t now = this->millis_(); +struct RetryArgs { + std::function func; + uint8_t retry_countdown; + uint32_t current_interval; + Component *component; + std::string name; + float backoff_increase_factor; + Scheduler *scheduler; +}; +static void retry_handler(const std::shared_ptr &args) { + RetryResult retry_result = args->func(); + if (retry_result == RetryResult::DONE || --args->retry_countdown <= 0) + return; + args->current_interval *= args->backoff_increase_factor; + args->scheduler->set_timeout(args->component, args->name, args->current_interval, [args]() { retry_handler(args); }); +} + +void HOT Scheduler::set_retry(Component *component, const std::string &name, uint32_t initial_wait_time, + uint8_t max_attempts, std::function func, float backoff_increase_factor) { if (!name.empty()) this->cancel_retry(component, name); if (initial_wait_time == SCHEDULER_DONT_RUN) return; - ESP_LOGVV(TAG, "set_retry(name='%s', initial_wait_time=%u,max_attempts=%u, backoff_factor=%0.1f)", name.c_str(), + ESP_LOGVV(TAG, "set_retry(name='%s', initial_wait_time=%u, max_attempts=%u, backoff_factor=%0.1f)", name.c_str(), initial_wait_time, max_attempts, backoff_increase_factor); - auto item = make_unique(); - item->component = component; - item->name = name; - item->type = SchedulerItem::RETRY; - item->interval = initial_wait_time; - item->retry_countdown = max_attempts; - item->backoff_multiplier = backoff_increase_factor; - item->last_execution = now - initial_wait_time; - item->last_execution_major = this->millis_major_; - if (item->last_execution > now) - item->last_execution_major--; - item->retry_callback = std::move(func); - item->remove = false; - this->push_(std::move(item)); + auto args = std::make_shared(); + args->func = std::move(func); + args->retry_countdown = max_attempts; + args->current_interval = initial_wait_time; + args->component = component; + args->name = "retry$" + name; + args->backoff_increase_factor = backoff_increase_factor; + args->scheduler = this; + + this->set_timeout(component, args->name, initial_wait_time, [args]() { retry_handler(args); }); } bool HOT Scheduler::cancel_retry(Component *component, const std::string &name) { - return this->cancel_item_(component, name, SchedulerItem::RETRY); + return this->cancel_timeout(component, "retry$" + name); } optional HOT Scheduler::next_schedule_in() { @@ -162,7 +173,6 @@ void HOT Scheduler::call() { } while (!this->empty_()) { - RetryResult retry_result = RETRY; // use scoping to indicate visibility of `item` variable { // Don't copy-by value yet @@ -191,11 +201,7 @@ void HOT Scheduler::call() { // - timeouts/intervals get cancelled { WarnIfComponentBlockingGuard guard{item->component}; - if (item->type == SchedulerItem::RETRY) { - retry_result = item->retry_callback(); - } else { - item->void_callback(); - } + item->callback(); } } @@ -213,16 +219,13 @@ void HOT Scheduler::call() { continue; } - if (item->type == SchedulerItem::INTERVAL || - (item->type == SchedulerItem::RETRY && (--item->retry_countdown > 0 && retry_result != RetryResult::DONE))) { + if (item->type == SchedulerItem::INTERVAL) { if (item->interval != 0) { const uint32_t before = item->last_execution; const uint32_t amount = (now - item->last_execution) / item->interval; item->last_execution += amount * item->interval; if (item->last_execution < before) item->last_execution_major++; - if (item->type == SchedulerItem::RETRY) - item->interval *= item->backoff_multiplier; } this->push_(std::move(item)); } diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index dc96d58329..111bee1df2 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -10,13 +10,13 @@ class Component; class Scheduler { public: - void set_timeout(Component *component, const std::string &name, uint32_t timeout, std::function &&func); + void set_timeout(Component *component, const std::string &name, uint32_t timeout, std::function func); bool cancel_timeout(Component *component, const std::string &name); - void set_interval(Component *component, const std::string &name, uint32_t interval, std::function &&func); + void set_interval(Component *component, const std::string &name, uint32_t interval, std::function func); bool cancel_interval(Component *component, const std::string &name); void set_retry(Component *component, const std::string &name, uint32_t initial_wait_time, uint8_t max_attempts, - std::function &&func, float backoff_increase_factor = 1.0f); + std::function func, float backoff_increase_factor = 1.0f); bool cancel_retry(Component *component, const std::string &name); optional next_schedule_in(); @@ -29,20 +29,13 @@ class Scheduler { struct SchedulerItem { Component *component; std::string name; - enum Type { TIMEOUT, INTERVAL, RETRY } type; + enum Type { TIMEOUT, INTERVAL } type; union { uint32_t interval; uint32_t timeout; }; uint32_t last_execution; - // Ideally this should be a union or std::variant - // but unions don't work with object like std::function - // union CallBack_{ - std::function void_callback; - std::function retry_callback; - // }; - uint8_t retry_countdown{3}; - float backoff_multiplier{1.0f}; + std::function callback; bool remove; uint8_t last_execution_major; @@ -60,8 +53,6 @@ class Scheduler { switch (this->type) { case SchedulerItem::INTERVAL: return "interval"; - case SchedulerItem::RETRY: - return "retry"; case SchedulerItem::TIMEOUT: return "timeout"; default: From 62f9e181e03e6f3ae1aea18833b686b069fb0680 Mon Sep 17 00:00:00 2001 From: Maurice Makaay Date: Wed, 11 May 2022 00:58:28 +0200 Subject: [PATCH 52/56] Code cleanup fixes for the select component (#3457) Co-authored-by: Maurice Makaay --- esphome/components/select/select.cpp | 11 ++++++++--- esphome/components/select/select.h | 18 +++++++++++++++--- esphome/components/select/select_call.cpp | 2 -- esphome/components/select/select_call.h | 1 - .../template/select/template_select.cpp | 19 ++++++++++--------- 5 files changed, 33 insertions(+), 18 deletions(-) diff --git a/esphome/components/select/select.cpp b/esphome/components/select/select.cpp index 75edb5c8ba..35f1cfba46 100644 --- a/esphome/components/select/select.cpp +++ b/esphome/components/select/select.cpp @@ -23,6 +23,10 @@ void Select::add_on_state_callback(std::function &&ca this->state_callback_.add(std::move(callback)); } +bool Select::has_option(const std::string &option) const { return this->index_of(option).has_value(); } + +bool Select::has_index(size_t index) const { return index < this->size(); } + size_t Select::size() const { auto options = traits.get_options(); return options.size(); @@ -46,11 +50,12 @@ optional Select::active_index() const { } optional Select::at(size_t index) const { - auto options = traits.get_options(); - if (index >= options.size()) { + if (this->has_index(index)) { + auto options = traits.get_options(); + return options.at(index); + } else { return {}; } - return options.at(index); } uint32_t Select::hash_base() { return 2812997003UL; } diff --git a/esphome/components/select/select.h b/esphome/components/select/select.h index 64870fc9a3..b11c6404a0 100644 --- a/esphome/components/select/select.h +++ b/esphome/components/select/select.h @@ -28,16 +28,28 @@ class Select : public EntityBase { void publish_state(const std::string &state); - /// Return whether this select has gotten a full state yet. + /// Return whether this select component has gotten a full state yet. bool has_state() const { return has_state_; } + /// Instantiate a SelectCall object to modify this select component's state. SelectCall make_call() { return SelectCall(this); } - void set(const std::string &value) { make_call().set_option(value).perform(); } - // Methods that provide an API to index-based access. + /// Return whether this select component contains the provided option. + bool has_option(const std::string &option) const; + + /// Return whether this select component contains the provided index offset. + bool has_index(size_t index) const; + + /// Return the number of options in this select component. size_t size() const; + + /// Find the (optional) index offset of the provided option value. optional index_of(const std::string &option) const; + + /// Return the (optional) index offset of the currently active option. optional active_index() const; + + /// Return the (optional) option value at the provided index offset. optional at(size_t index) const; void add_on_state_callback(std::function &&callback); diff --git a/esphome/components/select/select_call.cpp b/esphome/components/select/select_call.cpp index 9442598740..6ee41b1029 100644 --- a/esphome/components/select/select_call.cpp +++ b/esphome/components/select/select_call.cpp @@ -13,8 +13,6 @@ SelectCall &SelectCall::set_option(const std::string &option) { SelectCall &SelectCall::set_index(size_t index) { return with_operation(SELECT_OP_SET_INDEX).with_index(index); } -const optional &SelectCall::get_option() const { return option_; } - SelectCall &SelectCall::select_next(bool cycle) { return with_operation(SELECT_OP_NEXT).with_cycle(cycle); } SelectCall &SelectCall::select_previous(bool cycle) { return with_operation(SELECT_OP_PREVIOUS).with_cycle(cycle); } diff --git a/esphome/components/select/select_call.h b/esphome/components/select/select_call.h index ea4d34ab5f..efc9a982ec 100644 --- a/esphome/components/select/select_call.h +++ b/esphome/components/select/select_call.h @@ -24,7 +24,6 @@ class SelectCall { SelectCall &set_option(const std::string &option); SelectCall &set_index(size_t index); - const optional &get_option() const; SelectCall &select_next(bool cycle); SelectCall &select_previous(bool cycle); diff --git a/esphome/components/template/select/template_select.cpp b/esphome/components/template/select/template_select.cpp index 219c341ec9..88a0d66ed6 100644 --- a/esphome/components/template/select/template_select.cpp +++ b/esphome/components/template/select/template_select.cpp @@ -20,9 +20,12 @@ void TemplateSelect::setup() { this->pref_ = global_preferences->make_preference(this->get_object_id_hash()); if (!this->pref_.load(&index)) { value = this->initial_option_; - ESP_LOGD(TAG, "State from initial (could not load): %s", value.c_str()); + ESP_LOGD(TAG, "State from initial (could not load stored index): %s", value.c_str()); + } else if (!this->has_index(index)) { + value = this->initial_option_; + ESP_LOGD(TAG, "State from initial (restored index %d out of bounds): %s", index, value.c_str()); } else { - value = this->traits.get_options().at(index); + value = this->at(index).value(); ESP_LOGD(TAG, "State from restore: %s", value.c_str()); } } @@ -38,9 +41,8 @@ void TemplateSelect::update() { if (!val.has_value()) return; - auto options = this->traits.get_options(); - if (std::find(options.begin(), options.end(), *val) == options.end()) { - ESP_LOGE(TAG, "lambda returned an invalid option %s", (*val).c_str()); + if (!this->has_option(*val)) { + ESP_LOGE(TAG, "Lambda returned an invalid option: %s", (*val).c_str()); return; } @@ -54,12 +56,11 @@ void TemplateSelect::control(const std::string &value) { this->publish_state(value); if (this->restore_value_) { - auto options = this->traits.get_options(); - size_t index = std::find(options.begin(), options.end(), value) - options.begin(); - - this->pref_.save(&index); + auto index = this->index_of(value); + this->pref_.save(&index.value()); } } + void TemplateSelect::dump_config() { LOG_SELECT("", "Template Select", this); LOG_UPDATE_INTERVAL(this); From c569f5ddcfbb301424860055f1ef14e7bf7c8850 Mon Sep 17 00:00:00 2001 From: Maurice Makaay Date: Wed, 11 May 2022 01:02:49 +0200 Subject: [PATCH 53/56] Code cleanup fixes for the number component (#3458) Co-authored-by: Maurice Makaay --- esphome/components/number/number.h | 1 - esphome/components/number/number_call.h | 2 -- 2 files changed, 3 deletions(-) diff --git a/esphome/components/number/number.h b/esphome/components/number/number.h index 8f9bf8c2e1..ad058e3a0e 100644 --- a/esphome/components/number/number.h +++ b/esphome/components/number/number.h @@ -33,7 +33,6 @@ class Number : public EntityBase { void publish_state(float state); NumberCall make_call() { return NumberCall(this); } - void set(float value) { make_call().set_value(value).perform(); } void add_on_state_callback(std::function &&callback); diff --git a/esphome/components/number/number_call.h b/esphome/components/number/number_call.h index 9a3dad560f..bd50170be5 100644 --- a/esphome/components/number/number_call.h +++ b/esphome/components/number/number_call.h @@ -23,8 +23,6 @@ class NumberCall { void perform(); NumberCall &set_value(float value); - const optional &get_value() const { return value_; } - NumberCall &number_increment(bool cycle); NumberCall &number_decrement(bool cycle); NumberCall &number_to_min(); From 0b69f7231585cd8d110efeb08a992647b1bbb6a3 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Wed, 11 May 2022 01:38:05 +0200 Subject: [PATCH 54/56] Enable api transport encryption for new projects (#3142) * Enable api transport encryption for new projects * Format --- esphome/dashboard/dashboard.py | 3 +++ esphome/wizard.py | 2 ++ 2 files changed, 5 insertions(+) diff --git a/esphome/dashboard/dashboard.py b/esphome/dashboard/dashboard.py index b78d22cf7c..1fadac968d 100644 --- a/esphome/dashboard/dashboard.py +++ b/esphome/dashboard/dashboard.py @@ -1,5 +1,6 @@ # pylint: disable=wrong-import-position +import base64 import codecs import collections import functools @@ -378,6 +379,8 @@ class WizardRequestHandler(BaseHandler): if k in ("name", "platform", "board", "ssid", "psk", "password") } kwargs["ota_password"] = secrets.token_hex(16) + noise_psk = secrets.token_bytes(32) + kwargs["api_encryption_key"] = base64.b64encode(noise_psk).decode() destination = settings.rel_path(f"{kwargs['name']}.yaml") wizard.wizard_write(path=destination, **kwargs) self.set_status(200) diff --git a/esphome/wizard.py b/esphome/wizard.py index 34930ff66f..469219300b 100644 --- a/esphome/wizard.py +++ b/esphome/wizard.py @@ -111,6 +111,8 @@ def wizard_file(**kwargs): # Configure API if "password" in kwargs: config += f" password: \"{kwargs['password']}\"\n" + if "api_encryption_key" in kwargs: + config += f" encryption:\n key: \"{kwargs['api_encryption_key']}\"\n" # Configure OTA config += "\nota:\n" From 4116caff6ae1262502dd7c4d6952a140105146f8 Mon Sep 17 00:00:00 2001 From: Ruben De Smet Date: Wed, 11 May 2022 01:44:52 +0200 Subject: [PATCH 55/56] Implement allow_deep_sleep (#3282) --- esphome/components/deep_sleep/deep_sleep_component.cpp | 1 + esphome/components/deep_sleep/deep_sleep_component.h | 1 + 2 files changed, 2 insertions(+) diff --git a/esphome/components/deep_sleep/deep_sleep_component.cpp b/esphome/components/deep_sleep/deep_sleep_component.cpp index 1e31cef33e..8db100f236 100644 --- a/esphome/components/deep_sleep/deep_sleep_component.cpp +++ b/esphome/components/deep_sleep/deep_sleep_component.cpp @@ -160,6 +160,7 @@ void DeepSleepComponent::begin_sleep(bool manual) { } float DeepSleepComponent::get_setup_priority() const { return setup_priority::LATE; } void DeepSleepComponent::prevent_deep_sleep() { this->prevent_ = true; } +void DeepSleepComponent::allow_deep_sleep() { this->prevent_ = false; } } // namespace deep_sleep } // namespace esphome diff --git a/esphome/components/deep_sleep/deep_sleep_component.h b/esphome/components/deep_sleep/deep_sleep_component.h index 1ff681693f..f384dee01d 100644 --- a/esphome/components/deep_sleep/deep_sleep_component.h +++ b/esphome/components/deep_sleep/deep_sleep_component.h @@ -96,6 +96,7 @@ class DeepSleepComponent : public Component { void begin_sleep(bool manual = false); void prevent_deep_sleep(); + void allow_deep_sleep(); protected: // Returns nullopt if no run duration is set. Otherwise, returns the run From 40ad9f491182c58453bc94b49d9a934d0c25d46c Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 11 May 2022 12:47:50 +1200 Subject: [PATCH 56/56] Add deep_sleep.allow YAML action (#3459) --- esphome/components/deep_sleep/__init__.py | 63 ++++++++++++------- .../deep_sleep/deep_sleep_component.h | 12 ++-- tests/test1.yaml | 2 +- 3 files changed, 48 insertions(+), 29 deletions(-) diff --git a/esphome/components/deep_sleep/__init__.py b/esphome/components/deep_sleep/__init__.py index 058358fa67..8b60b4eb5f 100644 --- a/esphome/components/deep_sleep/__init__.py +++ b/esphome/components/deep_sleep/__init__.py @@ -93,7 +93,14 @@ deep_sleep_ns = cg.esphome_ns.namespace("deep_sleep") DeepSleepComponent = deep_sleep_ns.class_("DeepSleepComponent", cg.Component) EnterDeepSleepAction = deep_sleep_ns.class_("EnterDeepSleepAction", automation.Action) PreventDeepSleepAction = deep_sleep_ns.class_( - "PreventDeepSleepAction", automation.Action + "PreventDeepSleepAction", + automation.Action, + cg.Parented.template(DeepSleepComponent), +) +AllowDeepSleepAction = deep_sleep_ns.class_( + "AllowDeepSleepAction", + automation.Action, + cg.Parented.template(DeepSleepComponent), ) WakeupPinMode = deep_sleep_ns.enum("WakeupPinMode") @@ -208,28 +215,32 @@ async def to_code(config): cg.add_define("USE_DEEP_SLEEP") -DEEP_SLEEP_ENTER_SCHEMA = cv.All( - automation.maybe_simple_id( - { - cv.GenerateID(): cv.use_id(DeepSleepComponent), - cv.Exclusive(CONF_SLEEP_DURATION, "time"): cv.templatable( - cv.positive_time_period_milliseconds - ), - # Only on ESP32 due to how long the RTC on ESP8266 can stay asleep - cv.Exclusive(CONF_UNTIL, "time"): cv.All(cv.only_on_esp32, cv.time_of_day), - cv.Optional(CONF_TIME_ID): cv.use_id(time.RealTimeClock), - } - ), - cv.has_none_or_all_keys(CONF_UNTIL, CONF_TIME_ID), -) - - -DEEP_SLEEP_PREVENT_SCHEMA = automation.maybe_simple_id( +DEEP_SLEEP_ACTION_SCHEMA = cv.Schema( { cv.GenerateID(): cv.use_id(DeepSleepComponent), } ) +DEEP_SLEEP_ENTER_SCHEMA = cv.All( + automation.maybe_simple_id( + DEEP_SLEEP_ACTION_SCHEMA.extend( + cv.Schema( + { + cv.Exclusive(CONF_SLEEP_DURATION, "time"): cv.templatable( + cv.positive_time_period_milliseconds + ), + # Only on ESP32 due to how long the RTC on ESP8266 can stay asleep + cv.Exclusive(CONF_UNTIL, "time"): cv.All( + cv.only_on_esp32, cv.time_of_day + ), + cv.Optional(CONF_TIME_ID): cv.use_id(time.RealTimeClock), + } + ) + ) + ), + cv.has_none_or_all_keys(CONF_UNTIL, CONF_TIME_ID), +) + @automation.register_action( "deep_sleep.enter", EnterDeepSleepAction, DEEP_SLEEP_ENTER_SCHEMA @@ -252,8 +263,16 @@ async def deep_sleep_enter_to_code(config, action_id, template_arg, args): @automation.register_action( - "deep_sleep.prevent", PreventDeepSleepAction, DEEP_SLEEP_PREVENT_SCHEMA + "deep_sleep.prevent", + PreventDeepSleepAction, + automation.maybe_simple_id(DEEP_SLEEP_ACTION_SCHEMA), ) -async def deep_sleep_prevent_to_code(config, action_id, template_arg, args): - paren = await cg.get_variable(config[CONF_ID]) - return cg.new_Pvariable(action_id, template_arg, paren) +@automation.register_action( + "deep_sleep.allow", + AllowDeepSleepAction, + automation.maybe_simple_id(DEEP_SLEEP_ACTION_SCHEMA), +) +async def deep_sleep_action_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var diff --git a/esphome/components/deep_sleep/deep_sleep_component.h b/esphome/components/deep_sleep/deep_sleep_component.h index f384dee01d..8dc87cece8 100644 --- a/esphome/components/deep_sleep/deep_sleep_component.h +++ b/esphome/components/deep_sleep/deep_sleep_component.h @@ -190,14 +190,14 @@ template class EnterDeepSleepAction : public Action { #endif }; -template class PreventDeepSleepAction : public Action { +template class PreventDeepSleepAction : public Action, public Parented { public: - PreventDeepSleepAction(DeepSleepComponent *deep_sleep) : deep_sleep_(deep_sleep) {} + void play(Ts... x) override { this->parent_->prevent_deep_sleep(); } +}; - void play(Ts... x) override { this->deep_sleep_->prevent_deep_sleep(); } - - protected: - DeepSleepComponent *deep_sleep_; +template class AllowDeepSleepAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->allow_deep_sleep(); } }; } // namespace deep_sleep diff --git a/tests/test1.yaml b/tests/test1.yaml index e15b2cf6b7..7bb1fbe954 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -121,6 +121,7 @@ mqtt: - topic: livingroom/ota_mode then: - deep_sleep.prevent + - deep_sleep.allow - topic: livingroom/ota_mode then: - deep_sleep.enter: @@ -2833,4 +2834,3 @@ button: id: scd40 - scd4x.factory_reset: id: scd40 -