From c59adf612f8ccdc3f415e99ce538ba1c8fc6c928 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 1/4] 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 d5134e88b16edaeb9e39339cdd1f3b7ddd645d52 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 2/4] 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 2243021b581ad1f855de5976e2152fcdc4e97f91 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 3/4] 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 dcb226b20221fd481d15e8f4925120b64b138fdf Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 14 Apr 2022 13:48:35 +1200 Subject: [PATCH 4/4] Bump version to 2022.4.0b2 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 01d2d59c3d..04fa5c2bf7 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2022.4.0b1" +__version__ = "2022.4.0b2" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"